• Ben Wild 8
  • NEWBIE
  • 10 Points
  • Member since 2016

  • Chatter
    Feed
  • 0
    Best Answers
  • 0
    Likes Received
  • 0
    Likes Given
  • 7
    Questions
  • 10
    Replies
Following the "ACTION REQUIRED: SOAP API Legacy Versions will retire in Summer '18" email that came out last week I have been to check my API version for our booking system integration. I've followed the instructions and according to the eventLog we are using v5 which will cease to be supported. This seems strange as we made some changes to the API fairly recently for the TLS requirements. Our requests send to https://login.salesforce.com/services/Soap/u/42.0 HTTP/1.1 which to me indicates v42. Am I missing something with the version numbers here?
I've written a sheduled batch apex class that I wasnt to run every day and send out an email to every user that has a custom Action object record that has passed its "Due Date". I have written the code below but the sendEmail method just throws an exception every time and despite catching it, the error doesnt really say anythinf useful. My code is below:
global class ActionOverdueNotification implements Database.Batchable<sObject>, Schedulable, Database.Stateful {

    //Variable Section
    global FINAL String strQuery;
    global List<String> errorMessages = new List<String>();
    
    global ActionOverdueNotification() { 
        this.strQuery = getBatchQuery();
    }
    
    //Returns the Query String to Batch constructor to fetch right records.
    private String getBatchQuery() {
        String strQuery = 'SELECT Id, Due_Date__c, Owner_Email__c FROM Action__c WHERE Due_Date__c < TODAY'; 
        return strQuery;
    }
    
    //Batch Start method
    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator(strQuery);
    }

    //Batch Execute method calls findCostForWoD method
    global void execute(Database.BatchableContext BC, List<sObject> scopeList) {
        System.debug(LoggingLevel.INFO, '== scopeList size ==' + scopeList.size());
        
        List<Action__c> actList = (List<Action__c>) scopeList;
        if(!actList.isEmpty()) { 
            List<Messaging.SingleEmailMessage> mailList = new List<Messaging.SingleEmailMessage>();
            EmailTemplate et=[Select id from EmailTemplate where name = 'Action Template' limit 1];
            for (Action__c act : actList)
            {               
                
                Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage(); 
                String[] toAddresses = new String[] {act.Owner_Email__c};
                Message.setTargetObjectId(act.id);
                Message.setSenderDisplayName('System Admin');
                Message.setTemplateId(et.id);
                Message.setToAddresses(toAddresses); 
                Message.SaveAsActivity = false;
                mailList.add(Message);
                
            }
            if(!mailList.isEmpty()) {
                try{
                    Messaging.sendEmail(mailList);
                }
                catch (Exception ex) {
                    errorMessages.add('Unable to send email: '+ ex.getStackTraceString());
                }
            }
        }
    }  

    //Batch Finish method for after execution of batch work
    global void finish(Database.BatchableContext BC) { 
        AsyncApexJob aaj = [Select Id, Status, NumberOfErrors, JobItemsProcessed, MethodName, TotalJobItems, CreatedBy.Email from AsyncApexJob where Id =:BC.getJobId()];
        
        // Send an email to the Apex job's submitter notifying of job completion.
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {aaj.CreatedBy.Email};
        mail.setToAddresses(toAddresses);
        mail.setSubject('JOB Salesforce Send Notification Batch: ' + aaj.Status);
        String bodyText='Total Job Items ' + aaj.TotalJobItems + ' Number of records processed ' + aaj.JobItemsProcessed + ' with '+ aaj.NumberOfErrors + ' failures.\n';
        bodyText += 'Number of Error Messages ' + errorMessages.size() + '\n';
        bodyText += 'Error Message' + String.join(errorMessages, '\n');
        mail.setPlainTextBody(bodyText);
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
    
    //Method which schedules the ProductDownloadBatch
    global void execute(SchedulableContext sc) {        
        ActionOverdueNotification snInstance = new ActionOverdueNotification();
        ID batchprocessid = Database.executeBatch(snInstance);
    }
}

The error message is as follows:

Total Job Items 1 Number of records processed 1 with 0 failures.
Number of Error Messages 1
Error MessageUnable to send email: Class.ActionOverdueNotification.execute: line 45, column 1

Any ideas what I'm doing wrong?
I have a controller extension for a custom create child object page to take me back to the correct page after editing and to allow me to have an erorr message wthout using a validation. The controller works exactly as it should but I am having issues writing the test class for it. Iget the error "system.queryexception: list has no rows for assignment to sobject" My code is as below:
Controller:
public with sharing class createController {
	public String oppId {get;set;}
	public Opportunity opp;
	public Opportunity_Forecast__c oppf {get;set;}
	private ApexPages.StandardController stdController; 
	
    public createController(ApexPages.StandardController stdController) {
    	this.stdController = stdController;
        oppf = (Opportunity_Forecast__c)stdController.getRecord();
        String oppid = (oppf.Opportunity__c);
        String ID = ApexPages.currentPage().getParameters().get('oppid');
        opp = [select Automatic_Forecasting__c from Opportunity where Id =:ID]; 
        if (ID != null){
        	oppf.Opportunity__c= ID;   
        }   
    }
	
	public PageReference cancelAndReturn() 
    {
    	PageReference cancel = new PageReference('/' + ApexPages.currentPage().getParameters().get('oppid'));
        cancel.setRedirect(true);
 		return cancel;	
    }
	
	public PageReference saveAndReturn()
    {
        try{
        	if(opp.Automatic_Forecasting__c == true){
        		ApexPages.Message myMsg = new ApexPages.Message(ApexPages.Severity.ERROR,'Error: Upside Automation must be disabled on the opportunity page before editing forecasts.');
        		ApexPages.addMessage(myMsg);
        		return null;
        	}
        	else{
        		upsert oppf;
        		PageReference save = new PageReference('/' + ApexPages.currentPage().getParameters().get('oppid'));
        		stdController.save();
        		save.setRedirect(true);
        		return save;
        	}
        }
        catch(DMLException ex){
        	ApexPages.addMessages(ex);
        	return null;
        }
    }   
}
Test Class:
@isTest 
public with sharing class CreateControllerTest {
    static testMethod void testCreateController(){
    	
    	Account testAcct = new Account(Name='Test Create Controller');
    	insert testAcct;
    	
    	Opportunity opp = new Opportunity(Name = 'Test Opportunity1',Account = testAcct,Project_Start_Date__c = Date.newInstance(2017, 12, 15),Delivery_End_Date__c = Date.newInstance(2018, 9, 15),Forecast_Amount__c = 50000, StageName = 'Continued Development of Project',Opportunity_Region__c = 'UK',CloseDate = Date.newInstance(2011, 12, 15), Is_Test__c =TRUE, Automatic_Forecasting__c = FALSE, Test_Date__c = Date.newInstance(2017, 12, 18));
    	insert opp;
    	
    	Opportunity_Forecast__c oppf = new Opportunity_Forecast__c(Opportunity__c = opp.id, Value__c = 2000, Forecast_Category__c = 'Upside', Forecast_Period__c = 'Next Quarter');
    	insert oppf;
    	
    	Test.StartTest();
    	
    	PageReference pageRef = Page.NewForecast;
		Test.setCurrentPage(pageRef);
		pageRef.getParameters().put('id',oppf.Id);
    	ApexPages.StandardController sc = new ApexPages.StandardController(oppf);
        CreateController testOppForecast = new CreateController(sc);
 
        testOppForecast.saveAndReturn();
        Test.StopTest();
    }
}
Any ideas what I'm doing wrong? I've done some reading on the error I'm getting but most cases are where the record hasn't been created. 
I have a custom object called Opportunity_Forecast__c that sits as a child object below Opportunity. I created a custom visualforce page for editing and creating new Opportunity_Forecast__c records. The reason for this was the navigation butotns on the standard edit page took me back to the wrong page. Editing is working as expected. I am however having issues with creating new forecasts, I get the error Error:
Id not specified in an update call
when saving. My code is as below;
VF Page:
<apex:page standardController="Opportunity_Forecast__c" extensions="createController">
<apex:form >
<apex:pageBlock title="Forecast" mode="Edit">
	  <apex:pageBlockButtons >
	  	<apex:commandButton action="{!saveAndReturn}" value="Save"/>
	  	<apex:commandButton action="{!cancelAndReturn}" value="Cancel"/>
	  </apex:pageBlockButtons>
	  <apex:pageMessages />
	  <apex:pageBlockSection title="Forecast Detail" columns="1">
      	<apex:inputField value="{!Opportunity_Forecast__c.Value__c}"/> 
      	<apex:inputField value="{!Opportunity_Forecast__c.Forecast_Category__c}"/> 
     	<apex:inputField value="{!Opportunity_Forecast__c.Forecast_Period__c}"/>
     	</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>
Controller Extension:
public with sharing class createController {
	Opportunity_Forecast__c oppf {get;set;}
	private ApexPages.StandardController stdController;
	
    public createController(ApexPages.StandardController stdController) {
    	this.stdController = stdController;
        Opportunity_Forecast__c oppf = (Opportunity_Forecast__c) stdController.getRecord();
        Map<String, String> m = ApexPages.currentPage().getParameters();
        oppf.Opportunity__c = (Id) m.get('oppId');
    }
	
	public PageReference cancelAndReturn()
    {
    	PageReference cancel = stdController.cancel();
        cancel.setRedirect(true);
 		return cancel;	
    }
	
	public PageReference saveAndReturn()
    {
        try{
        	insert oppf;
        	PageReference save = stdController.cancel();
        	stdController.save();
        	save.setRedirect(true);
        	return save;
        }
        catch(DMLException ex){
        	ApexPages.addMessages(ex);
        	return null;
        }
    }
    
}

Any ideas?
I am working on an automatic forecasting system that looks at the start, end date and value of a project and then automatically assigns the revenue as forecasts across the deployment months. Forecasts are created as custom objects below the opportunity object. Originally I would have my trigger delete all the forecasts and create new ones with each update. The issue is, with the trigger running after update, the old forecasts are triggering off validations as the forecasts update after the validations.

To get around this I created a before update trigger to delete the old forecasts before the validations run, my code is as follows;
trigger deleteExistingForecasts on Opportunity (before update) {
   
    Set<Id> ids = new set<Id>();
    
    for(Opportunity op: Trigger.new){	    
	    if(op.Automatic_Forecasting__c == true){
	    	ids.add(op.Id);	
	    }
    }
    delete [SELECT Id FROM Opportunity_Forecast__c WHERE Opportunity__c IN :ids];
}
My issue with this is that it just throws the error: Error:Apex trigger deleteExistingForecasts caused an unexpected exception, contact your administrator: deleteExistingForecasts: execution of BeforeUpdate caused by: System.DmlException: Delete failed. First exception on row 0 with id a099E000002GWg3QAG; first error: SELF_REFERENCE_FROM_TRIGGER, Object (id = 0069E000006JLS7) is currently in trigger deleteExistingForecasts, therefore it cannot recursively update itself: []: Trigger.deleteExistingForecasts: line 10, column 1

I'm a bit confused as to why though?
 
I have a trigger that queries the Period object for the FiscalYearSettings to use. This is to allow my trigger to automatically update is the Fiscal Year was ever changed in the Company Profile. The issue I have is that it seems to run an SOQL query for every run of the trigger meaning I exceed the SOQL limit. I've looked at bulkifying it but non of the guides I've read help as I only use the SOQL to get a date. My code is as below;
 
trigger SetProductQuarter on OpportunityLineItem (before insert, before update) 
{
    Date FYStartDate = [SELECT FiscalYearSettings.StartDate FROM Period WHERE Type = 'Year' AND StartDate <= TODAY AND EndDate >= TODAY].FiscalYearSettings.StartDate;
     
    for(OpportunityLineItem opitem : Trigger.new){
    	//Current Date Q1
    	if(opitem.Date_Today__c < FYStartDate.addMonths(3) && opitem.Date_Today__c > FYStartDate){
	    	if(opitem.ServiceDate < FYStartDate.addMonths(3) && opitem.ServiceDate > FYStartDate)
	    	{
	    		opitem.Product_Quarter__c = 'Q1';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(6) && opitem.ServiceDate > FYStartDate.addMonths(3))
	    	{
	    		opitem.Product_Quarter__c = 'Q2';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(9) && opitem.ServiceDate > FYStartDate.addMonths(6))
	    	{
	    		opitem.Product_Quarter__c = 'Q3';
	    	}      
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(12) && opitem.ServiceDate > FYStartDate.addMonths(9))
	    	{
	    		opitem.Product_Quarter__c = 'Q4';
	    	}      
	    	else
	    	{
	    		opitem.Product_Quarter__c = '=';
	    	}
    	}
    	//Current Date Q2
    	else if(opitem.Date_Today__c < FYStartDate.addMonths(6) && opitem.Date_Today__c > FYStartDate.addMonths(3)){
	    	if(opitem.ServiceDate < FYStartDate.addMonths(15) && opitem.ServiceDate > FYStartDate.addMonths(12))
	    	{
	    		opitem.Product_Quarter__c = 'Q1';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(6) && opitem.ServiceDate > FYStartDate.addMonths(3))
	    	{
	    		opitem.Product_Quarter__c = 'Q2';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(9) && opitem.ServiceDate > FYStartDate.addMonths(6))
	    	{
	    		opitem.Product_Quarter__c = 'Q3';
	    	}      
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(12) && opitem.ServiceDate > FYStartDate.addMonths(9))
	    	{
	    		opitem.Product_Quarter__c = 'Q4';
	    	} 
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(3) && opitem.ServiceDate > FYStartDate)
	    	{
	    		opitem.Product_Quarter__c = 'PQ1';
	    	}     
	    	else
	    	{
	    		opitem.Product_Quarter__c = '=';
	    	}
    	}
    	//Current Date Q3
    	else if(opitem.Date_Today__c < FYStartDate.addMonths(9) && opitem.Date_Today__c > FYStartDate.addMonths(6)){
	    	if(opitem.ServiceDate < FYStartDate.addMonths(15) && opitem.ServiceDate > FYStartDate.addMonths(12))
	    	{
	    		opitem.Product_Quarter__c = 'Q1';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(18) && opitem.ServiceDate > FYStartDate.addMonths(15))
	    	{
	    		opitem.Product_Quarter__c = 'Q2';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(9) && opitem.ServiceDate > FYStartDate.addMonths(6))
	    	{
	    		opitem.Product_Quarter__c = 'Q3';
	    	}      
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(12) && opitem.ServiceDate > FYStartDate.addMonths(9))
	    	{
	    		opitem.Product_Quarter__c = 'Q4';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(3) && opitem.ServiceDate > FYStartDate)
	    	{
	    		opitem.Product_Quarter__c = 'PQ1';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(6) && opitem.ServiceDate > FYStartDate.addMonths(3))
	    	{
	    		opitem.Product_Quarter__c = 'PQ2';
	    	}      
	    	else
	    	{
	    		opitem.Product_Quarter__c = '=';
	    	}
    	}
    	//Current Date Q4
    	else if(opitem.Date_Today__c < FYStartDate.addMonths(12) && opitem.Date_Today__c > FYStartDate.addMonths(9)){
	    	if(opitem.ServiceDate < FYStartDate.addMonths(15) && opitem.ServiceDate > FYStartDate.addMonths(12))
	    	{
	    		opitem.Product_Quarter__c = 'Q1';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(18) && opitem.ServiceDate > FYStartDate.addMonths(15))
	    	{
	    		opitem.Product_Quarter__c = 'Q2';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(21) && opitem.ServiceDate > FYStartDate.addMonths(18))
	    	{
	    		opitem.Product_Quarter__c = 'Q3';
	    	}      
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(12) && opitem.ServiceDate > FYStartDate.addMonths(9))
	    	{
	    		opitem.Product_Quarter__c = 'Q4';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(3) && opitem.ServiceDate > FYStartDate)
	    	{
	    		opitem.Product_Quarter__c = 'PQ1';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(6) && opitem.ServiceDate > FYStartDate.addMonths(3))
	    	{
	    		opitem.Product_Quarter__c = 'PQ2';
	    	}
	    	else if(opitem.ServiceDate < FYStartDate.addMonths(9) && opitem.ServiceDate > FYStartDate.addMonths(6))
	    	{
	    		opitem.Product_Quarter__c = 'PQ3';
	    	}      
	    	else
	    	{
	    		opitem.Product_Quarter__c = '=';
	    	}
    	}                        
    }  
}

 
I have a trigger that updates a field on my line items to say what quarter they are in relative to the current quarter (so that I can use roll up summaries to show totals in fields on the opportunity page). The trigger consists of a very long if statement in order to fit in all the criteria to allow for going round end of calendar years etc. The trigger runs before update and works very well if a record is edited through the UI or updated using the data loader.

The issue I am having is I want it to run across all records at the start of each quarter as the value becomes incorrect as we go in to a new quarter so the trigger has to run again to update. Currently I just run an update with the data loader but I would like to to be automated. I wrote a Batch apex class that I scheduled to run every day and if the date is the start of a quarter, run an update on all of the opportunity line items in an attempt to have the trigger run. The Batch class runs as it should but I cant seem to get the trigger to run, I'm not getting any errors so I'm a bit confused as to what is going wrong.

My code below (apolgies for any glaring errors or bad practice, batch scheduled apex is new to me):
 
//BATCH APEX CLASS
global class UpdateOpportunityLineItems implements 
    Database.Batchable<sObject>, Database.Stateful {
    
    // instance member to retain state across transactions
    global Integer recordsProcessed = 0;

    global Database.QueryLocator start(Database.BatchableContext bc) {
        //Get the opportunities from the database
        return Database.getQueryLocator(
            'SELECT ID, Trigger_Update__c, Date_Today__c FROM OpportunityLineItem ');
    }

    global void execute(Database.BatchableContext bc, List<OpportunityLineItem> opp){
        // Process each batch of records
        List<OpportunityLineItem> oppLineItemsToUpdate = new List<OpportunityLineItem>();
        for (OpportunityLineItem oppLineItem : opp) {
            if (oppLineItem.Date_Today__c == Date.newInstance(Date.Today().year(), 11, 15)){
                oppLineItem.Trigger_Update__c = TRUE;
                oppLineItemsToUpdate.add(oppLineItem);
            }
            else if (oppLineItem.Date_Today__c == Date.newInstance(Date.Today().year(), 8, 1)){
                oppLineItem.Trigger_Update__c = TRUE;
                oppLineItemsToUpdate.add(oppLineItem);
            }
            else if (oppLineItem.Date_Today__c == Date.newInstance(Date.Today().year(), 11, 1)){
                oppLineItem.Trigger_Update__c = TRUE;
                oppLineItemsToUpdate.add(oppLineItem);
            }
            else if (oppLineItem.Date_Today__c == Date.newInstance(Date.Today().year(), 2, 1)){
                oppLineItem.Trigger_Update__c = TRUE;
                oppLineItemsToUpdate.add(oppLineItem);
            }
        }
        update oppLineItemsToUpdate;
    }
        
    global void finish(Database.BatchableContext bc){
        System.debug(recordsProcessed + ' records processed.');
        AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, 
            JobItemsProcessed,
            TotalJobItems, CreatedBy.Email
            FROM AsyncApexJob
            WHERE Id = :bc.getJobId()];
        // call some utility to send email
        //EmailUtils.sendMessage(a, recordsProcessed);
    }    
}
//SCHEDULABLE CLASS
global class ScheduleOpportunityLineItemUpdate implements Schedulable {

    global void execute(SchedulableContext sc){
        UpdateOpportunityLineItems uol = new UpdateOpportunityLineItems();
        database.executebatch(uol,1);
    }
}
//TRIGGER
trigger SetProductQuarter on OpportunityLineItem (before insert, before update) 
{
    
    for(OpportunityLineItem opitem : Trigger.new)
    {
        if(opitem.Product_Quarter_Number__c == 1)
        {
            if(opitem.Date_Today__c.Month() < 5)
            {
                if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year())
                {
                    opitem.Product_Quarter__c = 'Q1';
                }
                else if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year() - 1)
                {
                    opitem.Product_Quarter__c = 'P1';
                }
                else
                {
                    opitem.Product_Quarter__c = '=';   
                }
            }
            else
            {
                if(opitem.Date_Today__c.Month() > 4)
                {
                    if(opitem.Date_Today__c.Month() > 7)
                    {
                        if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year() + 1)
                        {
                            opitem.Product_Quarter__c = 'Q1';
                        }
                        else
                        if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year())
                        {
                            opitem.Product_Quarter__c = 'P1';
                        }
                        else
                        {
                            opitem.Product_Quarter__c = '=';
                        }
                    }
                    else
                    if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year())
                    {
                        opitem.Product_Quarter__c = 'Q1';   
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';
                    }
                }
                else
                {
                    opitem.Product_Quarter__c = '=';
                }
            }
        }
        else if(opitem.Product_Quarter_Number__c == 2)
        {
            if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year())
            {
                if(opitem.Date_Today__c.Month() < 11)
                {
                    opitem.Product_Quarter__c = 'Q2';
                }
                else
                {
                    opitem.Product_Quarter__c = 'P2';
                }
            }
            else if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year() + 1)
            {
                if(opitem.Date_Today__c.Month() > 10)
                {
                    opitem.Product_Quarter__c = 'Q2';
                }
                else
                {
                    opitem.Product_Quarter__c = '=';
                }
            }
            else if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year() - 1)
            {
                if(opitem.Date_Today__c.Month() < 11)
                {
                    opitem.Product_Quarter__c = 'P2';
                }
                else
                {
                    opitem.Product_Quarter__c = '=';
                }
            }
            else
            {
                opitem.Product_Quarter__c = '=';
            }
        }
        else if(opitem.Product_Quarter_Number__c == 3)
        {
            if(opitem.Current_Quarter__c > 3)
            {
                if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year())
                {
                    if(opitem.ServiceDate.Month() >= 11)
                    {
                        opitem.Product_Quarter__c = 'Q3';
                    }
                    else if(opitem.ServiceDate.Month() < 5)
                    {
                        opitem.Product_Quarter__c = 'P3';
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';   
                    }
                }
                else if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year() - 1)
                {
                    if(opitem.ServiceDate.Month() >=11)
                    {
                        opitem.Product_Quarter__c = 'P3';
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';
                    }
                }
                else
                {
                    opitem.Product_Quarter__c = '=';   
                }
            }
            else if(opitem.Current_Quarter__c < 3)
            {
                if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year())
                {
                    if((opitem.ServiceDate.Month() < 2 && opitem.Date_Today__c.Month() < 2))
                    {
                        opitem.Product_Quarter__c = 'Q3';
                    }
                    else if(opitem.ServiceDate.Month() < 2 && opitem.Date_Today__c.Month() > 1)
                    {
                        opitem.Product_Quarter__c = 'P3';
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';
                    }
                }
                else if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year()+1)
                {
                    if(opitem.ServiceDate.Month() < 2)
                    {
                        opitem.Product_Quarter__c = 'Q3';
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';
                    } 
                }
                else if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year()-1)
                {
                    if(opitem.ServiceDate.Month() > 10)
                    {
                        opitem.Product_Quarter__c = 'P3';
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';
                    }
                }
                else
                {
                    opitem.Product_Quarter__c = '='; 
                }  
            }
            else if(opitem.Current_Quarter__c == 3)
            {
                if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year())
                {
                    if(opitem.ServiceDate.Month() > 10)
                    {
                        if(opitem.Date_Today__c.Month() > 10)
                        {
                            opitem.Product_Quarter__c = 'Q3';
                        }
                        else
                        {
                            opitem.Product_Quarter__c = '=';
                        }
                    }
                    else if(opitem.ServiceDate.Month() < 2)
                    {
                        if(opitem.Date_Today__c.Month() < 2)
                        {
                            opitem.Product_Quarter__c = 'Q3';
                        }
                        else
                        {
                            opitem.Product_Quarter__c = 'P3';
                        }
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';
                    } 
                }
                else if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year() + 1)
                {
                    if(opitem.ServiceDate.Month() < 2)
                    {
                        opitem.Product_Quarter__c = 'Q3';
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';
                    }
                }
                else if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year() - 1)
                {
                    if(opitem.ServiceDate.Month() > 10)
                    {
                        if(opitem.Date_Today__c.Month() < 2)
                        {
                            opitem.Product_Quarter__c = 'Q3';
                        }
                        else
                        {
                            opitem.Product_Quarter__c = 'P3';
                        }
                    }
                    else if(opitem.ServiceDate.Month() < 2)
                    {
                        if(opitem.Date_Today__c.Month() < 2)
                        {
                            opitem.Product_Quarter__c = 'P3';
                        }
                        else
                        {
                            opitem.Product_Quarter__c = '=';
                        }  
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';
                    }
                }
                else
                {
                    opitem.Product_Quarter__c = '=';
                }
            }
        }
        else if(opitem.Product_Quarter_Number__c == 4)
        {
            if(opitem.ServiceDate.Year()== opitem.Date_Today__c.Year())
            {
                if(opitem.ServiceDate.Month() < 5)
                    {
                        if(opitem.Date_Today__c.Month() < 5)
                        {
                            opitem.Product_Quarter__c = 'Q4';
                        }
                        else
                        {
                            opitem.Product_Quarter__c = '=';
                        }
                    }
                    else
                    {
                        opitem.Product_Quarter__c = '=';
                    }
            }
            else if(opitem.ServiceDate.Year() == opitem.Date_Today__c.Year() + 1)
            {
                if(opitem.Date_Today__c.Month() < 5)
                {
                    opitem.Product_Quarter__c = '=';
                }
                else
                {
                    opitem.Product_Quarter__c = 'Q4';
                }    
            }
            else if(opitem.ServiceDate.Year() - opitem.Date_Today__c.Year() > 1)
            {
                opitem.Product_Quarter__c = '=';
            } 
            else
            {
                opitem.Product_Quarter__c = '=';  
            }
                    
        }
    }  
}

I have tried chnaging the batch size (hence why it is currently 1) in case I was hitting any limits but that didnt seem to help. Any ideas?

Thanks,

Ben


 
I've written a sheduled batch apex class that I wasnt to run every day and send out an email to every user that has a custom Action object record that has passed its "Due Date". I have written the code below but the sendEmail method just throws an exception every time and despite catching it, the error doesnt really say anythinf useful. My code is below:
global class ActionOverdueNotification implements Database.Batchable<sObject>, Schedulable, Database.Stateful {

    //Variable Section
    global FINAL String strQuery;
    global List<String> errorMessages = new List<String>();
    
    global ActionOverdueNotification() { 
        this.strQuery = getBatchQuery();
    }
    
    //Returns the Query String to Batch constructor to fetch right records.
    private String getBatchQuery() {
        String strQuery = 'SELECT Id, Due_Date__c, Owner_Email__c FROM Action__c WHERE Due_Date__c < TODAY'; 
        return strQuery;
    }
    
    //Batch Start method
    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator(strQuery);
    }

    //Batch Execute method calls findCostForWoD method
    global void execute(Database.BatchableContext BC, List<sObject> scopeList) {
        System.debug(LoggingLevel.INFO, '== scopeList size ==' + scopeList.size());
        
        List<Action__c> actList = (List<Action__c>) scopeList;
        if(!actList.isEmpty()) { 
            List<Messaging.SingleEmailMessage> mailList = new List<Messaging.SingleEmailMessage>();
            EmailTemplate et=[Select id from EmailTemplate where name = 'Action Template' limit 1];
            for (Action__c act : actList)
            {               
                
                Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage(); 
                String[] toAddresses = new String[] {act.Owner_Email__c};
                Message.setTargetObjectId(act.id);
                Message.setSenderDisplayName('System Admin');
                Message.setTemplateId(et.id);
                Message.setToAddresses(toAddresses); 
                Message.SaveAsActivity = false;
                mailList.add(Message);
                
            }
            if(!mailList.isEmpty()) {
                try{
                    Messaging.sendEmail(mailList);
                }
                catch (Exception ex) {
                    errorMessages.add('Unable to send email: '+ ex.getStackTraceString());
                }
            }
        }
    }  

    //Batch Finish method for after execution of batch work
    global void finish(Database.BatchableContext BC) { 
        AsyncApexJob aaj = [Select Id, Status, NumberOfErrors, JobItemsProcessed, MethodName, TotalJobItems, CreatedBy.Email from AsyncApexJob where Id =:BC.getJobId()];
        
        // Send an email to the Apex job's submitter notifying of job completion.
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {aaj.CreatedBy.Email};
        mail.setToAddresses(toAddresses);
        mail.setSubject('JOB Salesforce Send Notification Batch: ' + aaj.Status);
        String bodyText='Total Job Items ' + aaj.TotalJobItems + ' Number of records processed ' + aaj.JobItemsProcessed + ' with '+ aaj.NumberOfErrors + ' failures.\n';
        bodyText += 'Number of Error Messages ' + errorMessages.size() + '\n';
        bodyText += 'Error Message' + String.join(errorMessages, '\n');
        mail.setPlainTextBody(bodyText);
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
    
    //Method which schedules the ProductDownloadBatch
    global void execute(SchedulableContext sc) {        
        ActionOverdueNotification snInstance = new ActionOverdueNotification();
        ID batchprocessid = Database.executeBatch(snInstance);
    }
}

The error message is as follows:

Total Job Items 1 Number of records processed 1 with 0 failures.
Number of Error Messages 1
Error MessageUnable to send email: Class.ActionOverdueNotification.execute: line 45, column 1

Any ideas what I'm doing wrong?
I have a controller extension for a custom create child object page to take me back to the correct page after editing and to allow me to have an erorr message wthout using a validation. The controller works exactly as it should but I am having issues writing the test class for it. Iget the error "system.queryexception: list has no rows for assignment to sobject" My code is as below:
Controller:
public with sharing class createController {
	public String oppId {get;set;}
	public Opportunity opp;
	public Opportunity_Forecast__c oppf {get;set;}
	private ApexPages.StandardController stdController; 
	
    public createController(ApexPages.StandardController stdController) {
    	this.stdController = stdController;
        oppf = (Opportunity_Forecast__c)stdController.getRecord();
        String oppid = (oppf.Opportunity__c);
        String ID = ApexPages.currentPage().getParameters().get('oppid');
        opp = [select Automatic_Forecasting__c from Opportunity where Id =:ID]; 
        if (ID != null){
        	oppf.Opportunity__c= ID;   
        }   
    }
	
	public PageReference cancelAndReturn() 
    {
    	PageReference cancel = new PageReference('/' + ApexPages.currentPage().getParameters().get('oppid'));
        cancel.setRedirect(true);
 		return cancel;	
    }
	
	public PageReference saveAndReturn()
    {
        try{
        	if(opp.Automatic_Forecasting__c == true){
        		ApexPages.Message myMsg = new ApexPages.Message(ApexPages.Severity.ERROR,'Error: Upside Automation must be disabled on the opportunity page before editing forecasts.');
        		ApexPages.addMessage(myMsg);
        		return null;
        	}
        	else{
        		upsert oppf;
        		PageReference save = new PageReference('/' + ApexPages.currentPage().getParameters().get('oppid'));
        		stdController.save();
        		save.setRedirect(true);
        		return save;
        	}
        }
        catch(DMLException ex){
        	ApexPages.addMessages(ex);
        	return null;
        }
    }   
}
Test Class:
@isTest 
public with sharing class CreateControllerTest {
    static testMethod void testCreateController(){
    	
    	Account testAcct = new Account(Name='Test Create Controller');
    	insert testAcct;
    	
    	Opportunity opp = new Opportunity(Name = 'Test Opportunity1',Account = testAcct,Project_Start_Date__c = Date.newInstance(2017, 12, 15),Delivery_End_Date__c = Date.newInstance(2018, 9, 15),Forecast_Amount__c = 50000, StageName = 'Continued Development of Project',Opportunity_Region__c = 'UK',CloseDate = Date.newInstance(2011, 12, 15), Is_Test__c =TRUE, Automatic_Forecasting__c = FALSE, Test_Date__c = Date.newInstance(2017, 12, 18));
    	insert opp;
    	
    	Opportunity_Forecast__c oppf = new Opportunity_Forecast__c(Opportunity__c = opp.id, Value__c = 2000, Forecast_Category__c = 'Upside', Forecast_Period__c = 'Next Quarter');
    	insert oppf;
    	
    	Test.StartTest();
    	
    	PageReference pageRef = Page.NewForecast;
		Test.setCurrentPage(pageRef);
		pageRef.getParameters().put('id',oppf.Id);
    	ApexPages.StandardController sc = new ApexPages.StandardController(oppf);
        CreateController testOppForecast = new CreateController(sc);
 
        testOppForecast.saveAndReturn();
        Test.StopTest();
    }
}
Any ideas what I'm doing wrong? I've done some reading on the error I'm getting but most cases are where the record hasn't been created. 
I have a custom object called Opportunity_Forecast__c that sits as a child object below Opportunity. I created a custom visualforce page for editing and creating new Opportunity_Forecast__c records. The reason for this was the navigation butotns on the standard edit page took me back to the wrong page. Editing is working as expected. I am however having issues with creating new forecasts, I get the error Error:
Id not specified in an update call
when saving. My code is as below;
VF Page:
<apex:page standardController="Opportunity_Forecast__c" extensions="createController">
<apex:form >
<apex:pageBlock title="Forecast" mode="Edit">
	  <apex:pageBlockButtons >
	  	<apex:commandButton action="{!saveAndReturn}" value="Save"/>
	  	<apex:commandButton action="{!cancelAndReturn}" value="Cancel"/>
	  </apex:pageBlockButtons>
	  <apex:pageMessages />
	  <apex:pageBlockSection title="Forecast Detail" columns="1">
      	<apex:inputField value="{!Opportunity_Forecast__c.Value__c}"/> 
      	<apex:inputField value="{!Opportunity_Forecast__c.Forecast_Category__c}"/> 
     	<apex:inputField value="{!Opportunity_Forecast__c.Forecast_Period__c}"/>
     	</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>
Controller Extension:
public with sharing class createController {
	Opportunity_Forecast__c oppf {get;set;}
	private ApexPages.StandardController stdController;
	
    public createController(ApexPages.StandardController stdController) {
    	this.stdController = stdController;
        Opportunity_Forecast__c oppf = (Opportunity_Forecast__c) stdController.getRecord();
        Map<String, String> m = ApexPages.currentPage().getParameters();
        oppf.Opportunity__c = (Id) m.get('oppId');
    }
	
	public PageReference cancelAndReturn()
    {
    	PageReference cancel = stdController.cancel();
        cancel.setRedirect(true);
 		return cancel;	
    }
	
	public PageReference saveAndReturn()
    {
        try{
        	insert oppf;
        	PageReference save = stdController.cancel();
        	stdController.save();
        	save.setRedirect(true);
        	return save;
        }
        catch(DMLException ex){
        	ApexPages.addMessages(ex);
        	return null;
        }
    }
    
}

Any ideas?
I am working on an automatic forecasting system that looks at the start, end date and value of a project and then automatically assigns the revenue as forecasts across the deployment months. Forecasts are created as custom objects below the opportunity object. Originally I would have my trigger delete all the forecasts and create new ones with each update. The issue is, with the trigger running after update, the old forecasts are triggering off validations as the forecasts update after the validations.

To get around this I created a before update trigger to delete the old forecasts before the validations run, my code is as follows;
trigger deleteExistingForecasts on Opportunity (before update) {
   
    Set<Id> ids = new set<Id>();
    
    for(Opportunity op: Trigger.new){	    
	    if(op.Automatic_Forecasting__c == true){
	    	ids.add(op.Id);	
	    }
    }
    delete [SELECT Id FROM Opportunity_Forecast__c WHERE Opportunity__c IN :ids];
}
My issue with this is that it just throws the error: Error:Apex trigger deleteExistingForecasts caused an unexpected exception, contact your administrator: deleteExistingForecasts: execution of BeforeUpdate caused by: System.DmlException: Delete failed. First exception on row 0 with id a099E000002GWg3QAG; first error: SELF_REFERENCE_FROM_TRIGGER, Object (id = 0069E000006JLS7) is currently in trigger deleteExistingForecasts, therefore it cannot recursively update itself: []: Trigger.deleteExistingForecasts: line 10, column 1

I'm a bit confused as to why though?