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
Haseeb Ahmad 9Haseeb Ahmad 9 

Adding attachments in apex, convert the code to add Files

Hi Everyone, 

I have this Apex class which creates SOW and attached to "Notes & Attachments"  and I want to change that so when we generate SOW it attach to files instead.
 
/**
* This controller will generate a Quote PDF and attach it to the Quote's parent Opportunity.
*/
global class ZuoraDocumentGenerator {
    @testVisible private static final string noAmountResponse = 'Please add ACV to the opportunity before generating an SOW';
    @testVisible private static final string noOpportunitiesResponse = 'No opportunities found.';
    @testVisible private static final string notCorrectStage= 'You cannot generate an SOW before Stage 4, please fill out the required integration fields and move your Opportunity to Stage 4 to continue generating the SOW.';
    @testVisible private static final string oppOverThresholdResponse = 'Please chatter <a href="/_ui/core/chatter/groups/GroupProfilePage?g=0F90d0000008YEA" target="_blank">@Professional Services</a> in order to get an SOW generated';
    @testVisible private static final string noTemplateFoundResponse = 'No SOW templates are setup.';
    @testVisible private static final string errorContactingZuoraResponse = 'Error contacting Zuora';
    @testVisible private static integer testStatusCode;
    @testVisible private static string testSuccessMessage;
    
    // Generates PDF and attaches to Quote's parent Opportunity Object.
    @AuraEnabled
    webservice static String generateSOW(Id quoteId, String docType) {
        // query for quote to pull opportunity ID
        System.debug('** quoteid: '+ quoteId);
        System.debug('** doctype = '+ docType);
        List<zqu__quote__c> quotes = [select zqu__opportunity__c from zqu__quote__c where id = :quoteId limit 1];
        if (quotes.isEmpty()) {
            return 'No quotes found.';
        }
        
        // query for opp to pull licenses cost
        List<Opportunity> opps = [select name,Custom_SOW__c,Number_of_Seats__c, StageName, Record_Type_Name__c, sales_engineer__r.email, bundles__c,SOWException__c, account.billingCountry,rvpe__RVAccount__r.Name, Use_Case__c from opportunity where id = :quotes[0].zqu__opportunity__c limit 1];
        if (opps.isEmpty()) {
            return noOpportunitiesResponse;
        }
        if (opps[0].Custom_SOW__c == true) {
            return 'Unable to process Autogen request due to Custom SOW already generated. Please send a Chatter message to @proserv for assistance.';
        }
        
        //if the opportunity stage is not at least stage 4 throw this error.
        List<String> stageList = new List<String>{'Stage 4-Shortlist', 'Stage 5-Verbal', 'Stage 6-Legal / Contracting','Stage 7-Closed Won','Stage 8-Closed Won: Finance'};  
            if ((!stageList.contains(opps[0].StageName)) && opps[0].Record_Type_Name__c =='New Business'){
                return notCorrectStage;
            }
        
        // if there is no amount return error
        if (opps[0].Number_of_Seats__c == null) {
            return noAmountResponse;
        }
        
        // get template name based on amount and bundles;  if no template is returned, it is above the threshold --> return over threshold response
        String templateName = getTemplateName(opps[0]);        
        if (templateName == null) {
            return oppOverThresholdResponse;
        }
        
        // query for template using name, get the ID
        List<zqu__Quote_Template__c> quoteTemplateList = [select zqu__Template_Id__c from zqu__Quote_Template__c where name = :templateName limit 1];
        if (quoteTemplateList.isEmpty()) {
            return noTemplateFoundResponse;
        }
        
        // make HTTP call to zuora to generate the document
        HttpResponse res = generateSOW(quoteTemplateList, quoteId, docType);
       
        if (res.getStatusCode() != 200) {
            return errorContactingZuoraResponse;
        }
        
        String zuoraResponse = res.getBody();
        
        if(Test.isRunningTest()){
            zuoraResponse = testSuccessMessage != null ? testSuccessMessage : 'Quote PDF document has been successfully AttachmentID: 10101';
        }
        
        // if response is successful, update the attachment name and response for SOW template
        String successMessage = 'document has been successfully';
        
        // list of objects to update (opp and attachment)
        List<sObject> recordsForUpdate = new List<sObject>();
        
        // keep track of any dml errors
        String dmlErrors = '';
        
        if (zuoraResponse.contains(successMessage)) {
            
            // replace 'Quote' with 'SOW'
            zuoraResponse = zuoraResponse.replace('Quote', 'SOW');
            
            // update opportunity with 'SOW Generated' = true
            recordsForUpdate.add(updateOpportunity(opps[0]));
            
            // update attachment name
            Attachment attachment = updateAttachment(zuoraResponse, opps[0], docType);
            if (attachment != null) {
                recordsForUpdate.add(attachment);
            } else {
                dmlErrors += 'No attachment found for update.';                
            }
            
            // send email to solution engineer
            notifySolutionEngineer(opps[0]);
        }
        
        if (!recordsForUpdate.isEmpty()) {
            List<Database.saveResult> results = Utils.saveRecords(recordsForUpdate, 'Update');
            dmlErrors += Utils.getResultErrorString(results);
        }
        
        // if there were DML errors, send email to admin
        if (String.isNotBlank(dmlErrors)) {
            String subject = 'Error(s) in ZuoraDocumentGenerator ' + Date.today();
            Utils.sendEmailToAdmin(subject, dmlErrors);
        }
        
        return zuoraResponse;
    }
    
    private static String getTemplateName(Opportunity opp) {
        String results = null;
        Account account = [select id, billingCountry from Account where id =:opp.AccountId];
        if (opp.Number_of_Seats__c > 50 ) {
            return results;
        }
        Set<String>useCase = new Set<String>();
        useCase.addAll(opp.Use_Case__c.split(';'));
        boolean isSales = false;
        boolean isSupport = false;
        for(String st : useCase){
            System.debug('*** st ='+st);
            if(useCase.contains('Sales')){
                isSales = true;
            }
            if(useCase.contains('Support')){
                isSupport = true;
            }
        }
        if(opp.rvpe__RVAccount__r.Name !=null && opp.rvpe__RVAccount__r.Name.contains('Mitel')){
            if (opp.Use_Case__c == 'Sales'|| (isSales && isSupport)) {
                if (opp.Number_of_Seats__c > 15) {
                    results = 'SOW 2';
                } else if (opp.Number_of_Seats__c <= 14) {
                    results =  'SOW 1';           
                }
                else{
                    results =  'SOW 1';
                }
            } 
        }     
        if(account.billingCountry == 'United States' || account.billingCountry == 'USA' || account.billingCountry == 'US' || account.billingCountry == 'United States of America' || account.billingCountry == 'Canada' || account.billingCountry == 'CAN'){
            // Sales use case      
            if (opp.Use_Case__c == 'Sales'|| (isSales && isSupport)) {
                if (opp.Number_of_Seats__c > 15) {
                    results = 'PPT SOW 2';
                } else if (opp.Number_of_Seats__c <= 14) {
                    results =  'PPT SOW 1';           
                }
                else{
                    results =  'PPT SOW 2';
                }
            }           
            // Support use case
            else if(opp.Use_Case__c == 'Support') {
                if (opp.Number_of_Seats__c > 15) {
                    results = 'PPT SOW 2';
                } else if (opp.Number_of_Seats__c <= 14) {
                    results =  'PPT SOW 1';          
                }
                else{
                    results =  'PPT SOW 1';
                }        
            }
        }//this will be called if the billing country is not the US or Canada
        else{            
            if (opp.Use_Case__c == 'Sales'|| (isSales && isSupport)) {
                if (opp.Number_of_Seats__c > 15) {
                    results = 'SOW 2';
                } else if (opp.Number_of_Seats__c <= 14) {
                    results =  'SOW 1';           
                }
                else{
                    results =  'SOW 1';
                }
            } 
            
            // Support use case
            else if(opp.Use_Case__c == 'Support') {
                if (opp.Number_of_Seats__c > 15) {
                    results = 'SOW 2';
                } else if (opp.Number_of_Seats__c <= 14) {
                    results =  'SOW 1';          
                }
                else{
                    results =  'SOW 1';
                }        
            }
        }
        return results;  
    }
    
    public class SessionId {
        public String sessionId;
    }
    
    private static String getUserSessionId() {
        SessionId sessionJson = new SessionId();
        if(!Test.isRunningTest()) {
            sessionJson = (SessionId)JSON.deserialize(Page.ZuoraGenerateSOW.getContent().toString(), SessionId.class);
        }
        
        return sessionJson.sessionId;
    }
    
    private static HttpResponse generateSOW(List<zqu__Quote_Template__c> quoteTemplateList, Id quoteId, String docType) {
        // Generate Quote and attach to Opportunity
        Map<String,Object> zuoraConfigInfo = zqu.zQuoteUtil.getZuoraConfigInformation();
        
        Zuora.ZApi zApi = new Zuora.ZApi();
        Zuora.ZApi.LoginResult loginResult = new Zuora.ZApi.LoginResult();
        
        if(!Test.isRunningTest()){
            loginResult = zApi.zLogin();
        } else {
            loginResult.serverUrl = 'apisandbox';
        }
        
        String quoteServletUrl = loginResult.serverUrl.contains('apisandbox') ?
            'https://apisandbox.zuora.com/apps/servlet/GenerateQuote' :
        'https://zuora.com/apps/servlet/GenerateQuote';
        String sessionId = UserInfo.getSessionId();
        
        String sfdcUrl = URL.getSalesforceBaseUrl().toExternalForm() + '/services/Soap/u/10.0/' + UserInfo.getOrganizationId();
        
        PageReference generatePage = new PageReference(quoteServletUrl);
        generatePage.getParameters().put('templateId', quoteTemplateList[0].zqu__Template_Id__c);
        generatePage.getParameters().put('serverUrl', sfdcUrl);
        generatePage.getParameters().put('sessionId', getUserSessionId());
        generatePage.getParameters().put('quoteId', quoteId);
        generatePage.getParameters().put('attachToOpportunity', 'true');
        generatePage.getParameters().put('format', docType);
        generatePage.getParameters().put('ZSession', loginResult.session);
        generatePage.getParameters().put('useSFDCLocale', '1');
        generatePage.getParameters().put('locale', UserInfo.getLocale());
        
        // Zuora handles the attaching it to the opportunity through the https callout.         
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint(generatePage.getUrl());
        req.setMethod('GET');
        req.setTimeout(30000);
        
        if (!Test.isRunningTest()) {
            HttpResponse response = h.send(req);
            System.debug('response>>>' + response);
            
            return response;
        } else {
            HttpResponse res = new HttpResponse();
            Integer statusCode = testStatusCode != null ? testStatusCode : 200;
            res.setStatusCode(statusCode);
            return res;
        }
    }
    
    private static Opportunity updateOpportunity(Opportunity opportunity) {
        // update boolean on opportunity to indicate an SOW was generated
        opportunity.SOW_Generated__c = true;
        opportunity.Bypass_Opportunity_Validation__c = true;
        return opportunity;
    }
    
    private static Attachment updateAttachment(String zuoraResponse, Opportunity opportunity, String docType) {
        String errorMessage = '';
        
        String attachmentId = zuoraResponse.split('AttachmentID:', 0)[1].normalizeSpace();          
        List<Attachment> attachments = [select id, name from attachment where id = :attachmentId limit 1];
        if (!attachments.isEmpty()) {
            attachments[0].name = 'SOW for ' + opportunity.name + '.' + docType;
            return attachments[0];
        } else {
            return null;
        }
    }
    
    private static void notifySolutionEngineer(Opportunity opportunity) {
        List<String> toAddresses = new List<String>();
        if (opportunity.Sales_Engineer__c != null) {
            toAddresses.add(opportunity.Sales_Engineer__r.email);
        } else {
            toAddresses.add();
            toAddresses.add();
        }
        
        String subject = 'An SOW has been generated for ' + opportunity.name;
        String baseURL = URL.getSalesforceBaseUrl().toExternalForm();        
        String body = subject + '.\n\nHere is a link to the opportunity:  ' + baseURL + '/' + opportunity.id;
        
        Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage();
        message.setToAddresses(toAddresses);
        message.setSubject(subject);
        message.setPlainTextBody(body);
        message.setSaveAsActivity(false);        
        EmailUtils.sendEmails(new List<Messaging.SingleEmailMessage>{message}, false);
    }
}

This is Apex class which generate and attached SOW, how can change this to it be attached to Files. Thank you for your help. 

 
Best Answer chosen by Haseeb Ahmad 9
Haseeb Ahmad 9Haseeb Ahmad 9
Hi Swetha,

I get this to work with this code but now I have SOW getting links to files and attachments. how can delete the attachment from here? Where I can make that adjustment? 
 
private static Attachment updateAttachment(String zuoraResponse, Opportunity opportunity, String docType) {
        String errorMessage = '';
        
        String attachmentId = zuoraResponse.split('AttachmentID:', 0)[1].normalizeSpace();          
        List<Attachment> attachments = [select id, name, OwnerId, Body from attachment where id = :attachmentId limit 1];
        if (!attachments.isEmpty()) {
            attachments[0].name = 'SOW for ' + opportunity.name + '.' + docType;
            ContentVersion cVersion = new ContentVersion();
            cVersion.ContentLocation = 'S'; 
            cVersion.PathOnClient = attachments[0].Name;//File name with extension
            cVersion.Origin = 'H';//C-Content Origin. H-Chatter Origin.
            cVersion.OwnerId = attachments[0].OwnerId;//Owner of the file
            cVersion.Title = attachments[0].Name;//Name of the file
            cVersion.VersionData = attachments[0].Body;//File content
            insert cVersion;
           ContentDocumentLink cdl = new ContentDocumentLink();
           cdl.ContentDocumentId = [SELECT Id, ContentDocumentId FROM 
           ContentVersion WHERE Id =: cVersion.Id].ContentDocumentId;
           cdl.LinkedEntityId = attachments[0].parentid;
           cdl.ShareType = 'V';
           insert cdl;


            return attachments[0];
        } else {
            return null;
        }
    }

I only need to keep files and other copy from attachments can be deleted, how can I achieve that? 

All Answers

SwethaSwetha (Salesforce Developers) 
HI Haseeb ,
Though not exact for your requirement, the below links explain how you can code to add Files (ContentDocument in Lightning experience) so you can get started

https://www.biswajeetsamal.com/blog/convert-attachment-to-file-in-salesforce-using-apex/

https://salesforce.stackexchange.com/questions/234575/adding-attachments-in-apex-convert-the-code-to-add-files

Hope this helps you. Please mark this answer as best so that others facing the same issue will find this information useful. Thank you
Haseeb Ahmad 9Haseeb Ahmad 9
Hi Swetha,

Thank you for your help.

I have made the following changes as you mentioned but the attachment is still linking to Note&attachements. I have made changes to updateAttachemnt method.
 
private static Attachment updateAttachment(String zuoraResponse, Opportunity opportunity, String docType) {
        String errorMessage = '';
        
        String attachmentId = zuoraResponse.split('AttachmentID:', 0)[1].normalizeSpace();          
        List<Attachment> attachments = [select id, name, OwnerId, Body from attachment where id = :attachmentId limit 1];
        if (!attachments.isEmpty()) {
            attachments[0].name = 'SOW for ' + opportunity.name + '.' + docType;
            ContentVersion cVersion = new ContentVersion();
            cVersion.ContentLocation = 'S'; 
            cVersion.PathOnClient = attachments[0].Name;//File name with extension
            cVersion.Origin = 'H';//C-Content Origin. H-Chatter Origin.
            cVersion.OwnerId = attachments[0].OwnerId;//Owner of the file
            cVersion.Title = attachments[0].Name;//Name of the file
            cVersion.VersionData = attachments[0].Body;//File content
            insert cVersion;
            return attachments[0];
        } else {
            return null;
        }
    }

Can you please check thank you.
Haseeb Ahmad 9Haseeb Ahmad 9
Hi Swetha,

I get this to work with this code but now I have SOW getting links to files and attachments. how can delete the attachment from here? Where I can make that adjustment? 
 
private static Attachment updateAttachment(String zuoraResponse, Opportunity opportunity, String docType) {
        String errorMessage = '';
        
        String attachmentId = zuoraResponse.split('AttachmentID:', 0)[1].normalizeSpace();          
        List<Attachment> attachments = [select id, name, OwnerId, Body from attachment where id = :attachmentId limit 1];
        if (!attachments.isEmpty()) {
            attachments[0].name = 'SOW for ' + opportunity.name + '.' + docType;
            ContentVersion cVersion = new ContentVersion();
            cVersion.ContentLocation = 'S'; 
            cVersion.PathOnClient = attachments[0].Name;//File name with extension
            cVersion.Origin = 'H';//C-Content Origin. H-Chatter Origin.
            cVersion.OwnerId = attachments[0].OwnerId;//Owner of the file
            cVersion.Title = attachments[0].Name;//Name of the file
            cVersion.VersionData = attachments[0].Body;//File content
            insert cVersion;
           ContentDocumentLink cdl = new ContentDocumentLink();
           cdl.ContentDocumentId = [SELECT Id, ContentDocumentId FROM 
           ContentVersion WHERE Id =: cVersion.Id].ContentDocumentId;
           cdl.LinkedEntityId = attachments[0].parentid;
           cdl.ShareType = 'V';
           insert cdl;


            return attachments[0];
        } else {
            return null;
        }
    }

I only need to keep files and other copy from attachments can be deleted, how can I achieve that? 
This was selected as the best answer