From c73c32267cce3826b95e0cfae530888eae64699c Mon Sep 17 00:00:00 2001 From: Fernando Rodriguez Date: Mon, 8 Apr 2013 12:42:11 -0300 Subject: [PATCH 1/4] Open Opportunities artifacts --- src/classes/OpenOpportunitiesBatch.cls | 40 + .../OpenOpportunitiesBatch.cls-meta.xml | 5 + .../OpenOpportunitiesNeedUpdateScheduler.cls | 16 + ...ortunitiesNeedUpdateScheduler.cls-meta.xml | 5 + src/classes/OpenOpportunitiesScheduler.cls | 16 + .../OpenOpportunitiesScheduler.cls-meta.xml | 5 + src/classes/OpenOpportunityEmailUtils.cls | 274 +++++ .../OpenOpportunityEmailUtils.cls-meta.xml | 5 + ...penOpportunityFieldSelectionController.cls | 142 +++ ...unityFieldSelectionController.cls-meta.xml | 5 + .../OpenOpportunityFieldsUIController.cls | 27 + ...OpportunityFieldsUIController.cls-meta.xml | 5 + src/classes/OpenOpportunityListData.cls | 18 + .../OpenOpportunityListData.cls-meta.xml | 5 + src/classes/OpenOpportunityMailer.cls | 113 ++ .../OpenOpportunityMailer.cls-meta.xml | 5 + .../OpenOpportunityNeedUpdateBatch.cls | 41 + ...penOpportunityNeedUpdateBatch.cls-meta.xml | 5 + .../OpenOpportunityReportController.cls | 202 ++++ ...enOpportunityReportController.cls-meta.xml | 5 + .../OpenOpportunityReportUIController.cls | 118 ++ ...OpportunityReportUIController.cls-meta.xml | 5 + src/classes/OpenOpportunityTest.cls | 135 +++ src/classes/OpenOpportunityTest.cls-meta.xml | 5 + src/classes/OpenOpportunityUtils.cls | 23 + src/classes/OpenOpportunityUtils.cls-meta.xml | 5 + .../OpenOpportunityReportTable.component | 23 + src/objects/Open_Opportunity_Fields__c.object | 36 + .../Open_Opportunity_Settings__c.object | 46 + src/objects/Opportunity.object | 1048 +++++++++++++++++ src/package.xml | 52 +- src/pages/OpenOpportunityFieldSelection.page | 53 + src/pages/OpenOpportunityReportLayout.page | 65 + src/tabs/Open_Opportunities.tab | 7 + src/tabs/Report_Settings.tab | 7 + .../OpenOpportunityUpdateTrigger.trigger | 9 + ...nOpportunityUpdateTrigger.trigger-meta.xml | 5 + .../OpportunityLocationTrigger.trigger | 9 + ...pportunityLocationTrigger.trigger-meta.xml | 5 + src/triggers/OpportunityProduct.trigger | 5 + .../OpportunityProduct.trigger-meta.xml | 5 + src/triggers/OpportunityToCase.trigger | 15 + .../OpportunityToCase.trigger-meta.xml | 5 + src/triggers/OpportunityTrigger.trigger | 4 + .../OpportunityTrigger.trigger-meta.xml | 5 + src/triggers/ProjectLocationTrigger.trigger | 11 + .../ProjectLocationTrigger.trigger-meta.xml | 5 + src/triggers/ProjectProduct.trigger | 9 + src/triggers/ProjectProduct.trigger-meta.xml | 5 + src/triggers/ProjectTrigger.trigger | 6 + src/triggers/ProjectTrigger.trigger-meta.xml | 5 + src/triggers/TaskToCaseNote.trigger | 32 + src/triggers/TaskToCaseNote.trigger-meta.xml | 5 + 53 files changed, 2711 insertions(+), 1 deletion(-) create mode 100644 src/classes/OpenOpportunitiesBatch.cls create mode 100644 src/classes/OpenOpportunitiesBatch.cls-meta.xml create mode 100644 src/classes/OpenOpportunitiesNeedUpdateScheduler.cls create mode 100644 src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml create mode 100644 src/classes/OpenOpportunitiesScheduler.cls create mode 100644 src/classes/OpenOpportunitiesScheduler.cls-meta.xml create mode 100644 src/classes/OpenOpportunityEmailUtils.cls create mode 100644 src/classes/OpenOpportunityEmailUtils.cls-meta.xml create mode 100644 src/classes/OpenOpportunityFieldSelectionController.cls create mode 100644 src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml create mode 100644 src/classes/OpenOpportunityFieldsUIController.cls create mode 100644 src/classes/OpenOpportunityFieldsUIController.cls-meta.xml create mode 100644 src/classes/OpenOpportunityListData.cls create mode 100644 src/classes/OpenOpportunityListData.cls-meta.xml create mode 100644 src/classes/OpenOpportunityMailer.cls create mode 100644 src/classes/OpenOpportunityMailer.cls-meta.xml create mode 100644 src/classes/OpenOpportunityNeedUpdateBatch.cls create mode 100644 src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml create mode 100644 src/classes/OpenOpportunityReportController.cls create mode 100644 src/classes/OpenOpportunityReportController.cls-meta.xml create mode 100644 src/classes/OpenOpportunityReportUIController.cls create mode 100644 src/classes/OpenOpportunityReportUIController.cls-meta.xml create mode 100644 src/classes/OpenOpportunityTest.cls create mode 100644 src/classes/OpenOpportunityTest.cls-meta.xml create mode 100644 src/classes/OpenOpportunityUtils.cls create mode 100644 src/classes/OpenOpportunityUtils.cls-meta.xml create mode 100644 src/components/OpenOpportunityReportTable.component create mode 100644 src/objects/Open_Opportunity_Fields__c.object create mode 100644 src/objects/Open_Opportunity_Settings__c.object create mode 100644 src/objects/Opportunity.object create mode 100644 src/pages/OpenOpportunityFieldSelection.page create mode 100644 src/pages/OpenOpportunityReportLayout.page create mode 100644 src/tabs/Open_Opportunities.tab create mode 100644 src/tabs/Report_Settings.tab create mode 100644 src/triggers/OpenOpportunityUpdateTrigger.trigger create mode 100644 src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml create mode 100644 src/triggers/OpportunityLocationTrigger.trigger create mode 100644 src/triggers/OpportunityLocationTrigger.trigger-meta.xml create mode 100644 src/triggers/OpportunityProduct.trigger create mode 100644 src/triggers/OpportunityProduct.trigger-meta.xml create mode 100644 src/triggers/OpportunityToCase.trigger create mode 100644 src/triggers/OpportunityToCase.trigger-meta.xml create mode 100644 src/triggers/OpportunityTrigger.trigger create mode 100644 src/triggers/OpportunityTrigger.trigger-meta.xml create mode 100644 src/triggers/ProjectLocationTrigger.trigger create mode 100644 src/triggers/ProjectLocationTrigger.trigger-meta.xml create mode 100644 src/triggers/ProjectProduct.trigger create mode 100644 src/triggers/ProjectProduct.trigger-meta.xml create mode 100644 src/triggers/ProjectTrigger.trigger create mode 100644 src/triggers/ProjectTrigger.trigger-meta.xml create mode 100644 src/triggers/TaskToCaseNote.trigger create mode 100644 src/triggers/TaskToCaseNote.trigger-meta.xml diff --git a/src/classes/OpenOpportunitiesBatch.cls b/src/classes/OpenOpportunitiesBatch.cls new file mode 100644 index 00000000..c1507241 --- /dev/null +++ b/src/classes/OpenOpportunitiesBatch.cls @@ -0,0 +1,40 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +global class OpenOpportunitiesBatch implements Database.Batchable { + + public OpenOpportunitiesBatch(){} + + global Iterable start(Database.BatchableContext bc) { + + Opportunity [] opportunities = new Opportunity [] {}; + + opportunities = [SELECT + Id, + OwnerId + FROM Opportunity + WHERE Isclosed = false]; + + Set usersId = new Set(); + + for(Opportunity opp :opportunities) { + usersId.add(opp.OwnerId); + } + + User[] users = [SELECT Id, Email, Name FROM User WHERE Id IN :usersId]; + return users; + } + + global void execute(Database.BatchableContext bc, User[] scope) { + + User user = scope[0]; + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(user.Id); + + OpenOpportunityMailer.sendOpenOpportunitiesBatchReport(user, new String[]{user.Email}, stagedOpportunities); + } + + global void finish(Database.BatchableContext bc){} +} \ No newline at end of file diff --git a/src/classes/OpenOpportunitiesBatch.cls-meta.xml b/src/classes/OpenOpportunitiesBatch.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunitiesBatch.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls new file mode 100644 index 00000000..073cf441 --- /dev/null +++ b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls @@ -0,0 +1,16 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/22/2012 + * + */ +global class OpenOpportunitiesNeedUpdateScheduler implements Schedulable { + + public OpenOpportunitiesNeedUpdateScheduler() {} + + global void execute(SchedulableContext sc) { + + Database.executeBatch(new OpenOpportunityNeedUpdateBatch(), 1); + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunitiesScheduler.cls b/src/classes/OpenOpportunitiesScheduler.cls new file mode 100644 index 00000000..28f3101e --- /dev/null +++ b/src/classes/OpenOpportunitiesScheduler.cls @@ -0,0 +1,16 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +global class OpenOpportunitiesScheduler implements Schedulable { + + public OpenOpportunitiesScheduler() {} + + global void execute(SchedulableContext sc) { + + Database.executeBatch(new OpenOpportunitiesBatch(), 1); + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunitiesScheduler.cls-meta.xml b/src/classes/OpenOpportunitiesScheduler.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunitiesScheduler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityEmailUtils.cls b/src/classes/OpenOpportunityEmailUtils.cls new file mode 100644 index 00000000..e8995939 --- /dev/null +++ b/src/classes/OpenOpportunityEmailUtils.cls @@ -0,0 +1,274 @@ +/************************************************** +Class Name: OpenOpportunityEmailUtils +Class Description: Utiliy class which creates the HTML content to be displayed on the email / VF page. +Author: Fernando Rodriguez (frodriguez@adooxen.com) +Modified By: Fernando +Update Date: 2013-03-04 +Additional Comments: This class has comments on the code in order to help future changes +**************************************************/ +public with sharing class OpenOpportunityEmailUtils { + + + private static String[] earlyStages = new String[] {'Stage 1 - Connect','Stage 2 - Talking','Stage 5 - Submitted'}; + + + /************************************************** + Comments: Static variables that represents the diferent containers for the email / VF page HTML components + **************************************************/ + private static String ENVELOPE = '
[TITLE][CONTAINER]
'; + private static String TITLE = '

Open Opportunities

'; + private static String SUB_TITLE = ''; + private static String CONTAINER = '
[SUB_CONTAINER]
'; + + private static final String FOGBUGZ_LINK = 'http://manage.dimagi.com/default.asp?'; + + /************************************************** + Method Name: buildEmailContent + Method Comments: Method call from the Weekly / Daily Schedule flow to build the HTML Content + **************************************************/ + public static String buildEmailContent(Map stagedOpportunities, Boolean hasComments, Map stageComments) { + + String result = ENVELOPE; + String content = ''; + + List sortedStages = new List(stagedOpportunities.keySet()); + sortedStages.sort(); + + for(String stageName :sortedStages) { + + String stageTable = '' + SUB_TITLE.replace('[SUB_TITLE]', stageName); + + /************************************************** + Comments: for each stage we call buildEmailStageTable + **************************************************/ + stageTable += ''; + + /************************************************** + Comments: for each stage we call getEmailStageComments (if there are no comments, the input text is generated anyway) + **************************************************/ + stageTable += ''; + + stageTable += '
' + buildEmailStageTable(stageName, stagedOpportunities.get(stageName)) + '
' + getEmailStageComments(stageComments.get(stageName)) + '
'; + stageTable = stageTable.replace('null', ''); + content += stageTable; + } + + result = result.replace('[TITLE]', TITLE); + return result.replace('[CONTAINER]', CONTAINER.replace('[SUB_CONTAINER]', content)); + } + + /************************************************** + Method Name: buildEmailStageTable + Method Comments: Method which returns the content of a HTML table for a Stage + **************************************************/ + public static String buildEmailStageTable(String stageName, Opportunity[] opportunities) { + + String result = '[THEADER][TBODY]
'; + result = result.replace('[THEADER]', getEmailStageTableHeader()); + + String tbody = ''; + + Integer daysNotUpdatedLimit = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c.intValue() + : 30; + + Integer daysNotUpdatedLimitEarlyStages = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c.intValue() + : 10; + + Set earlyStagesSet = new Set(earlyStages); + + for(Opportunity opp :opportunities) { + tbody += getEmailStageTableRow(opp, daysNotUpdatedLimit, daysNotUpdatedLimitEarlyStages, earlyStagesSet); + } + result = result.replace('[TBODY]', tbody).replace('null', ''); + return result; + } + + /************************************************** + Method Name: getEmailStageTableHeader + Method Comments: Returns the header of the stage tables, depending on which columns where selected + **************************************************/ + private static String getEmailStageTableHeader() { + + final String LEFT_STYLE = 'style="background:#f2f3f3;text-align:left"'; + final String RIGHT_STYLE = 'style="background:#f2f3f3;text-align:right"'; + + String result = ''; + + /************************************************** + Comments: Fetches the columns from the Custom Settings + **************************************************/ + Open_Opportunity_Fields__c[] selectedFields = OpenOpportunityReportController.getOpportunityFields(); + + if (!selectedFields.isEmpty()) { + + result += 'Opportunity Name'; + for(Open_Opportunity_Fields__c selectedField :selectedFields) { + + result += '' + selectedField.Label__c + ''; + } + } + else { + + result += 'Opportunity Name' + + 'Stage Duration' + + 'Fogbugz Ticket Number' + + 'Fogbugz Assigned To' + + 'Probability (%)' + + 'Amount' + + 'Account Name' + + 'Business Unit Owner' + + 'Days not Updated'; + } + result += ''; + + + return result.replace('[LEFT_STYLE]', LEFT_STYLE).replace('[RIGHT_STYLE]', RIGHT_STYLE); + } + + /************************************************** + Method Name: getEmailStageTableRow + Method Comments: method called from buildEmailStageTable. For each opportunity this method is called. Renders the status of the opportunity and its data. + **************************************************/ + private static String getEmailStageTableRow(Opportunity opp, Integer daysNotUpdatedLimit, Integer daysNotUpdatedLimitEarlyStages, Set earlyStagesSet) { + + final String LEFT_STYLE = 'style="border-width:0 0 1px 0;vertical-align:middle;padding:4px 2px 4px 5px;border-bottom:1px solid #e3deb8;"'; + final String STYLE = 'style="border-width:0 0 1px 0;vertical-align:middle;padding:4px 2px 4px 5px;border-bottom:1px solid #e3deb8;text-align:right"'; + String rowStart = ''; + + /************************************************** + Comments: Filtering process in order to define the style of the Opportunity Row (red, yellow, etc) + **************************************************/ + if (opp.AccountId == null) { + rowStart = ''; + } + else { + rowStart = ''; + } + + // added by Nick - checking if it is a long wait after submission + + if (opp.StageName.contains('Submitted') && opp.long_wait__c) { + rowStart = rowStart; + } + else { + // end added by Nick + + if (earlyStagesSet.contains(opp.StageName)) { + rowStart = opp.Total_Days_Not_Updated__c > daysNotUpdatedLimitEarlyStages + ? '' + : rowStart; + } + else { + rowStart = opp.Total_Days_Not_Updated__c > daysNotUpdatedLimit + ? '' + : rowStart; + } + } + + String href = URL.getSalesforceBaseUrl().toExternalForm() + '/' + String.valueOf(opp.Id); + + Open_Opportunity_Fields__c[] selectedFields = OpenOpportunityReportController.getOpportunityFields(); + String result = rowStart; + + if (!selectedFields.isEmpty()) { + + /************************************************** + Comments: This loop goes over the Opportunity fields (based on the selected columns) and + formats the different fields (datetime, float, link, etc) + **************************************************/ + result += '' + opp.Name + ''; + for(Open_Opportunity_Fields__c selectedField :selectedFields) { + + try { + + String fieldType = selectedField.Type__c; + + String fieldValue = ''; + if (selectedField.Name.equals('Owner.Name')) { + fieldValue = opp.Owner.Name; + } + else { + fieldValue = String.valueOf(opp.get(selectedField.Name)); + } + + if (fieldType.equalsIgnoreCase('Date')) { + fieldValue = opp.get(selectedField.Name) != null + ? Date.valueOf(opp.get(selectedField.Name)).format() + : ''; + } + else if (fieldType.equalsIgnoreCase('DateTime')) { + fieldValue = opp.get(selectedField.Name) != null + ? Datetime.valueOf(opp.get(selectedField.Name)).format('MM/dd/yyyy HH:mm a') + : ''; + } + else if (fieldType.equalsIgnoreCase('Currency')) { + + fieldValue = ''; + if (opp.get(selectedField.Name) != null) { + + List args = new String[]{'0','number','###,###,##0.00'}; + Decimal currencyValue = (Decimal) opp.get(selectedField.Name); + fieldValue = '$' + String.format(currencyValue.format(), args); + } + } + + if (selectedField.Name.equals('Fogbugz_Ticket_Number__c')) { + result += '' + fieldValue + ''; + } + else { + result += '' + fieldValue + ''; + } + } + catch (Exception e) {} + } + } + else { + /************************************************** + Comments: When the selectedFields list is empty, that means we need to display + the default columns, which are already defined below. + We don;t need to make a custom format treatment as above, as we already know which columns are selected. + **************************************************/ + String amountValue = ''; + if (opp.Amount != null) { + List args = new String[]{'0','number','###,###,##0.00'}; + amountValue = '$' + String.format(opp.Amount.format(), args); + } + + result += '' + opp.Name + '' + + '' + opp.Stage_Duration__c + '' + + '' + opp.Fogbugz_Ticket_Number__c + '' + + '' + opp.Fogbugz_Assigned_To__c + '' + + '' + opp.Fogbugz_Probability__c + '' + + '' + amountValue + '' + + '' + opp.Account.Name + '' + + '' + opp.Business_Unit_Owner__r.Name + '' + + '' + opp.Total_Days_Not_Updated__c + ''; + } + + result += ''; + + return result.replace('[STYLE]', STYLE).replace('[LEFT_STYLE]', LEFT_STYLE); + } + + /************************************************** + Method Name: getEmailStageComments + Method Comments: This method is called from buildEmailContent method, in order to add to the email comments written on the UI. + **************************************************/ + private static String getEmailStageComments(String comments) { + + String headerRow = ''; + + String dataComment = (comments != null && comments.trim().length() > 0) ? comments : ' '; + + String dataRow = '' + + dataComment + + ''; + + return '' + headerRow + dataRow + '
'; + } + + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityEmailUtils.cls-meta.xml b/src/classes/OpenOpportunityEmailUtils.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityEmailUtils.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityFieldSelectionController.cls b/src/classes/OpenOpportunityFieldSelectionController.cls new file mode 100644 index 00000000..4ef6b61b --- /dev/null +++ b/src/classes/OpenOpportunityFieldSelectionController.cls @@ -0,0 +1,142 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/16/2012 + * + */ +public with sharing class OpenOpportunityFieldSelectionController { + + public Selectoption[] availableFields {get; set;} + public String selectedValue {get; set;} + public Integer orderCount {get; set;} + + public OpenOpportunityFieldSelectionController() { + + init(); + } + + public void addColumn() { + + if (selectedValue != null && selectedValue != '') { + insertNewColumn(); + removeSelectedValue(); + } + else { + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'Please select a field')); + } + } + + public void clearCurrentSelection() { + + Map result = Open_Opportunity_Fields__c.getAll(); + delete result.values(); + + init(); + } + + public Open_Opportunity_Fields__c[] getCurrentSelection() { + + Open_Opportunity_Fields__c[] result = new Open_Opportunity_Fields__c[] {}; + + result = [SELECT + Id, + Name, + Label__c, + Order__c + FROM Open_Opportunity_Fields__c + Order By Order__c]; + + return result; + } + + private void init() { + + availableFields = new Selectoption[] {}; + orderCount = 0; + Set alreadySelectedColumns = getCurrentSelectionNames(); + Map fieldList = getOpportunitySchemaFields(); + + String[] fieldIterator = new String[] {}; + fieldIterator.addAll(fieldList.keySet()); + fieldIterator.add('Opportunity Owner'); + fieldIterator.sort(); + + for (String fieldName :fieldIterator) { + + if (!alreadySelectedColumns.contains(fieldName)) { + + if (fieldName.equals('Opportunity Owner')) { + availableFields.add(new Selectoption('Owner.Name', 'Opportunity Owner')); + } + else { + Schema.Describefieldresult fieldResult = fieldList.get(fieldName); + availableFields.add(new Selectoption(fieldResult.getName(), fieldResult.getLabel())); + } + } + } + } + + private void insertNewColumn() { + + Open_Opportunity_Fields__c newColumn = new Open_Opportunity_Fields__c(); + + if (selectedValue.equals('Owner.Name')) { + newColumn.Name = 'Owner.Name'; + newColumn.Label__c = 'Opportunity Owner'; + newColumn.Type__c = 'String'; + } + else { + Map opportunitySchemaFields = getOpportunitySchemaFields(); + Schema.Describefieldresult fieldDescribe = opportunitySchemaFields.get(selectedValue); + + newColumn.Name = fieldDescribe.getName(); + newColumn.Label__c = fieldDescribe.getLabel(); + newColumn.Type__c = fieldDescribe.getType().name(); + } + orderCount ++; + newColumn.Order__c = orderCount; + + try { + insert newColumn; + } + catch(Exception e) {} + } + + private void removeSelectedValue() { + + for (Integer i = 0; i < availableFields.size(); i++) { + + if (availableFields[i].getValue().equals(selectedValue)) { + + availableFields.remove(i); + break; + } + } + } + + private Set getCurrentSelectionNames() { + + Map result = Open_Opportunity_Fields__c.getAll(); + orderCount = result.size(); + return result != null ? result.keySet() : new Set (); + } + + private Map getOpportunitySchemaFields() { + + Map fieldsSchema = Schema.SObjectType.Opportunity.fields.getMap(); + Map result = new Map(); + + for (String fieldName :fieldsSchema.keySet()) { + + Schema.SObjectField fieldSchema = fieldsSchema.get(fieldName); + Schema.Describefieldresult fieldDescribe = fieldSchema.getDescribe(); + + if (fieldDescribe.getName() != 'Name') { + result.put(fieldDescribe.getName(), fieldDescribe); + } + } + + return result; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml b/src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityFieldsUIController.cls b/src/classes/OpenOpportunityFieldsUIController.cls new file mode 100644 index 00000000..1879135d --- /dev/null +++ b/src/classes/OpenOpportunityFieldsUIController.cls @@ -0,0 +1,27 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/16/2012 + * + */ +public with sharing class OpenOpportunityFieldsUIController { + + public OpenOpportunityListData stageContainer; + public String stageName {get; set;} + public String htmlTable {get; set;} + + + public OpenOpportunityFieldsUIController() {} + + + public void setStageContainer(OpenOpportunityListData value) { + + stageContainer = value; + htmlTable = OpenOpportunityEmailUtils.buildEmailStageTable(stageName, stageContainer.opportunities); + } + + public OpenOpportunityListData getStageContainer() { + + return stageContainer; + } +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityFieldsUIController.cls-meta.xml b/src/classes/OpenOpportunityFieldsUIController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityFieldsUIController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityListData.cls b/src/classes/OpenOpportunityListData.cls new file mode 100644 index 00000000..a7eb8fc9 --- /dev/null +++ b/src/classes/OpenOpportunityListData.cls @@ -0,0 +1,18 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/16/2012 + * + */ + public with sharing class OpenOpportunityListData { + + public String stageName {get; set;} + public Opportunity[] opportunities {get; set;} + + public OpenOpportunityListData(String stageName, Opportunity[] opportunities) { + + this.opportunities = opportunities; + this.stageName = stageName; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityListData.cls-meta.xml b/src/classes/OpenOpportunityListData.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityListData.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityMailer.cls b/src/classes/OpenOpportunityMailer.cls new file mode 100644 index 00000000..85447cc9 --- /dev/null +++ b/src/classes/OpenOpportunityMailer.cls @@ -0,0 +1,113 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +public class OpenOpportunityMailer { + + private static final String EMAIL_SUBJECT = ' | | Biz Dev Report Out'; + private static final String RED_EMAIL_SUBJECT = 'Your Overdue Opportunities'; + private static final String[] earlyStages = new String[] {'Stage 1 - Connect','Stage 2 - Talking', 'Stage 5 - Submitted'}; + + public static void sendOpenOpportunitiesBatchReport(User user, String[] recipients, Map stagedOpportunities) { + + Map stageComments = new Map(); + for (String stageComment :stagedOpportunities.keySet()) { + stageComments.put(stageComment, ''); + } + + String content = OpenOpportunityEmailUtils.buildEmailContent(stagedOpportunities, false, stageComments); + + sendEmail(content, recipients, EMAIL_SUBJECT, user.Name); + } + + public static void sendOpenOpportunitiesSingleReport(User[] users, String[] recipients, Map stageComments) { + + Id[] usersId = new Id[] {}; + String usersNameSubject = ' - '; + for (User user :users) { + usersId.add(user.Id); + usersNameSubject += user.Name + ' - '; + } + + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(usersId); + + String content = OpenOpportunityEmailUtils.buildEmailContent(stagedOpportunities, true, stageComments); + + sendEmail(content, recipients, EMAIL_SUBJECT, usersNameSubject); + } + + public static void sendRedOpenOpportunitiesBatchReport(User user, String[] recipients, Map stagedOpportunities) { + + Map redStagedOpportunities = new Map(); + + Integer daysNotUpdatedLimit = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c.intValue() + : 30; + + Integer daysNotUpdatedLimitEarlyStages = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c.intValue() + : 10; + + Set earlyStagesSet = new Set(earlyStages); + + // Remove fresh opportunities + for (String stageName :stagedOpportunities.keySet()) { + + Opportunity[] opportunities = new Opportunity[] {}; + + for(Opportunity opportunity :stagedOpportunities.get(stageName)) { + + if (earlyStagesSet.contains(opportunity.StageName)) { + + if (opportunity.Total_Days_Not_Updated__c > daysNotUpdatedLimitEarlyStages) { + opportunities.add(opportunity); + } + } + else { + + if (opportunity.Total_Days_Not_Updated__c > daysNotUpdatedLimit) { + opportunities.add(opportunity); + } + } + } + + if (!opportunities.isEmpty()) { + redStagedOpportunities.put(stageName, opportunities); + } + } + + // Only send Mail if there are opportunities + if (!redStagedOpportunities.isEmpty()) { + + Map stageComments = new Map(); + for (String stageComment :redStagedOpportunities.keySet()) { + stageComments.put(stageComment, ''); + } + + String content = OpenOpportunityEmailUtils.buildEmailContent(redStagedOpportunities, false, stageComments); + + sendEmail(content, recipients, RED_EMAIL_SUBJECT, user.Name); + } + } + + private static void sendEmail(String content, String[] recipients, String subjectTemplate, String userName) { + + String subject = subjectTemplate.replace('', userName).replace('', Date.today().format()); + + OrgWideEmailAddress wideAddress = OpenOpportunityReportController.getOrganizationWideAddressMail(); + + Messaging.Singleemailmessage mail = new Messaging.Singleemailmessage(); + + if (wideAddress != null) { + mail.setOrgWideEmailAddressId(wideAddress.Id); + } + + mail.setHtmlBody(content); + mail.setSubject(subject); + mail.setToAddresses(recipients); + Messaging.sendEmail(new Messaging.Email[] {mail}); + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityMailer.cls-meta.xml b/src/classes/OpenOpportunityMailer.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityMailer.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityNeedUpdateBatch.cls b/src/classes/OpenOpportunityNeedUpdateBatch.cls new file mode 100644 index 00000000..0fcdfa16 --- /dev/null +++ b/src/classes/OpenOpportunityNeedUpdateBatch.cls @@ -0,0 +1,41 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/22/2012 + * + */ +global class OpenOpportunityNeedUpdateBatch implements Database.Batchable { + + public OpenOpportunityNeedUpdateBatch(){} + + global Iterable start(Database.BatchableContext bc) { + + Opportunity [] opportunities = new Opportunity [] {}; + + opportunities = [SELECT + Id, + OwnerId + FROM Opportunity + WHERE Isclosed = false]; + + Set usersId = new Set(); + + for(Opportunity opp :opportunities) { + usersId.add(opp.OwnerId); + } + + User[] users = [SELECT Id, Email, Name FROM User WHERE Id IN :usersId]; + return users; + } + + global void execute(Database.BatchableContext bc, User[] scope) { + + User user = scope[0]; + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(user.Id); + + OpenOpportunityMailer.sendRedOpenOpportunitiesBatchReport(user, new String[]{user.Email}, stagedOpportunities); + } + + global void finish(Database.BatchableContext bc){} + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml b/src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityReportController.cls b/src/classes/OpenOpportunityReportController.cls new file mode 100644 index 00000000..d05654df --- /dev/null +++ b/src/classes/OpenOpportunityReportController.cls @@ -0,0 +1,202 @@ +/************************************************** +Class Name: OpenOpportunityReportController +Class Description: Opportunity Expert Controller +Author: Fernando Rodriguez (frodriguez@adooxen.com) +Modified By: Fernando +Update Date: 2013-03-04 +Additional Comments: This controller performs all Opportunity Related queries for all components on the OpenOpportunites Module. + It also fetches the org wide email as well as the opportunity column fields from the Custom Settings. +**************************************************/ +public class OpenOpportunityReportController { + + private static OpenOpportunityReportController instance = null; + private static Open_Opportunity_Fields__c[] selectedColumnFields = null; + + private OpenOpportunityReportController() {} + + public static OpenOpportunityReportController getInstance() { + + if (instance == null) { + + instance = new OpenOpportunityReportController(); + } + return instance; + } + + public Opportunity[] getOpenOpportunitiesByUser(Id[] usersId) { + + String[] stages = new String[] {'Stage 1 - Connect','Stage 2 - Talking','Stage 3 - Prospect','Stage 4 - Proposal Development','Stage 5 - Submitted','Stage 6 - In Negotiations'}; + + Opportunity[] result = new Opportunity[] {}; + + /************************************************** + Comments: Filled in with default values + **************************************************/ + String queryValues = 'Id,Name,StageName,CreatedDate,Amount,AccountId,Total_Days_Not_Updated__c,Stage_Name_Updated_Date__c,Stage_Duration__c,Business_Unit_Owner__c,' + + 'Business_Unit_Owner__r.Name,Account.Name,OwnerId,Owner.Name,Fogbugz_Link__c,Fogbugz_Probability__c,Fogbugz_Days_Not_Updated__c,' + + 'Fogbugz_Ticket_Number__c,Fogbugz_Assigned_To__c,Fogbugz_Last_Updated_Date__c,long_wait__c'; + // THIS STRING MUST NOT END WITH A COMMA. + + Open_Opportunity_Fields__c[] selectedFields = getOpportunityFields(); + + + /************************************************** + Comments: WHEN CUSTOM COLUMNS ARE SELECTED, I ADD THOSE COLUMNS TO THE QUERY STRING + **************************************************/ + if (!selectedFields.isEmpty()) { + queryValues = 'Name,'; + for(Open_Opportunity_Fields__c selectedField :selectedFields) { + + if (selectedField.Name != 'Name') { + queryValues += selectedField.Name + ','; + } + } + + // THERE ARE SOME VALUES THAT NEED TO BE ADDED TO THE QUERY EVEN IF THEY WERE NOT SELECTED. + queryValues+='Business_Unit_Owner__r.Name,Account.Name'; + + if (!queryValues.contains('AccountId')) { + queryValues += ',AccountId'; + } + if (!queryValues.contains('Owner.Name')) { + queryValues += ',Owner.Name'; + } + if (!queryValues.contains('Total_Days_Not_Updated__c')) { + queryValues += ',Total_Days_Not_Updated__c'; + } + if (!queryValues.contains('StageName')) { + queryValues += ',StageName'; + } + if(!queryValues.contains('long_wait__c')) { + queryValues += ',long_wait__c'; + } + + /************************************************** + Comments: IF WE WANT TO ADD AN EXTRA FIELD THAT MUST BE USED AS A CONDITION PUT IT HERE. + + Expected Format: + + if (!queryValues.contains('FIELD_NAME__c')) { + queryValues += ',FIELD_NAME__c'; + } + + **************************************************/ + } + + /************************************************** + Comments: Perform the query on Opportunities based on the fields selected. + **************************************************/ + String sql = 'SELECT ' + queryValues + ' FROM Opportunity WHERE IsClosed = false AND OwnerId IN :usersId AND StageName IN :stages'; + + result = Database.query(sql); + return result; + } + + + public Map getOpenOpportunitiesOrderByStage(Id userId) { + + return getOpenOpportunitiesOrderByStage(new Id[] {userId}); + } + + public Map getOpenOpportunitiesOrderByStage(Id[] usersId) { + + Map result = new Map(); + Opportunity[] opportunities = getOpenOpportunitiesByUser(usersId); + + for(Opportunity opportunity :opportunities) { + + String stageName = opportunity.StageName; + + if (result.containsKey(stageName)) { + + result.get(stageName).add(opportunity); + } + else { + + result.put(stageName, new Opportunity[] {opportunity}); + } + } + + return result; + } + + public void updateStageDate(Id[] usersId) { + + Opportunity[] result = new Opportunity[] {}; + + result = [SELECT + StageName, + CreatedDate, + Stage_Name_Updated_Date__c, + + (SELECT StageName, CreatedDate FROM OpportunityHistories) + + FROM Opportunity + WHERE IsClosed = false + AND Stage_Name_Updated_Date__c = null + AND OwnerId IN :usersId]; + + Opportunity[] opportunities = new Opportunity[] {}; + + for (Opportunity opp :result) { + + if (opp.Stage_Name_Updated_Date__c == null) { + + opp.Stage_Name_Updated_Date__c = Date.valueOf(opp.CreatedDate); + for (OpportunityHistory oh :opp.OpportunityHistories) { + + String ohStageName = oh.StageName; + Date createdDate = Date.valueOf(oh.CreatedDate); + + if (ohStageName != opp.StageName) { + + opp.Stage_Name_Updated_Date__c = createdDate; + break; + } + } + opportunities.add(opp); + } + } + + if (!opportunities.isEmpty()) { + update opportunities; + } + + } + + public static Open_Opportunity_Fields__c[] getOpportunityFields() { + + if (selectedColumnFields == null) { + + selectedColumnFields = new Open_Opportunity_Fields__c[] {}; + selectedColumnFields = [SELECT + Id, + Name, + Label__c, + Type__c, + Order__c + FROM Open_Opportunity_Fields__c + Order By Order__c]; + } + + return selectedColumnFields; + } + + public static OrgWideEmailAddress getOrganizationWideAddressMail() { + + OrgWideEmailAddress[] addresses = new OrgWideEmailAddress[] {}; + final String DIMAGI_WIDE_ADDRESS_NAME = 'Dimagi Salesforce'; + + addresses = [SELECT Id, + Address, + DisplayName + FROM OrgWideEmailAddress + WHERE DisplayName = :DIMAGI_WIDE_ADDRESS_NAME]; + + if (!addresses.isEmpty()) { + return addresses[0]; + } + return null; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityReportController.cls-meta.xml b/src/classes/OpenOpportunityReportController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityReportController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityReportUIController.cls b/src/classes/OpenOpportunityReportUIController.cls new file mode 100644 index 00000000..9b38eca8 --- /dev/null +++ b/src/classes/OpenOpportunityReportUIController.cls @@ -0,0 +1,118 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +public class OpenOpportunityReportUIController { + + public String[] selectedUsers {get;set;} + public OpenOpportunityListData[] stagedListData {get; set;} + public Map stagedComments {get;set;} + public String recipients {get;set;} + public Id userId {get;set;} + + private static final String DEFAULT_EMAIL = Open_Opportunity_Settings__c.getOrgDefaults() != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Default_Email_Recipient__c + : 'bizdev@dimagi.com'; + + public OpenOpportunityReportUIController() { + + userId = (Apexpages.currentPage().getParameters().containsKey('uid')) + ? Apexpages.currentPage().getParameters().get('uid') + : Userinfo.getUserId(); + + selectedUsers = new String[] {userId}; + stagedComments = new Map(); + stagedListData = new OpenOpportunityListData[] {}; + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(userId); + recipients = DEFAULT_EMAIL; + + if (!stagedOpportunities.isEmpty()) { + + String[] stageList = new String[] {}; + stageList.addAll(stagedOpportunities.keySet()); + stageList.sort(); + + for (String stageName :stageList) { + stagedComments.put(stageName, ''); + stagedListData.add(new OpenOpportunityListData(stageName, stagedOpportunities.get(stageName))); + } + } + } + + public void reload() { + + try { + + Id[] usersId = selectedUsers; + + if (usersId != null && !usersId.isEmpty()) { + + stagedComments = new Map(); + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(usersId); + stagedListData = new OpenOpportunityListData[] {}; + + OpenOpportunityReportController.getInstance().updateStageDate(usersId); + + if (!stagedOpportunities.isEmpty()) { + stagedListData = new OpenOpportunityListData[] {}; + + String[] stageList = new String[] {}; + stageList.addAll(stagedOpportunities.keySet()); + stageList.sort(); + + for (String stageName :stageList) { + stagedComments.put(stageName, ''); + stagedListData.add(new OpenOpportunityListData(stageName, stagedOpportunities.get(stageName))); + } + } + } + } + catch (Exception e) { + + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'An error ocurred. Please refresh the Report')); + } + } + + public void initAction() { + + Id userId = (Apexpages.currentPage().getParameters().containsKey('uid')) + ? Apexpages.currentPage().getParameters().get('uid') + : Userinfo.getUserId(); + + OpenOpportunityReportController.getInstance().updateStageDate(new Id[] {userId}); + } + + public void sendEmail() { + + try { + Id[] usersId = selectedUsers; + User[] users = [SELECT Id, Email, Name FROM User WHERE Id IN :usersId]; + + if (!users.isEmpty()) { + String[] mails = (recipients != null && recipients.trim().length() > 0) ? recipients.split(',') : new String[] {}; + OpenOpportunityMailer.sendOpenOpportunitiesSingleReport(users, mails, stagedComments); + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.CONFIRM, 'Mail Sent Success')); + } + else { + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'Cannot Send Mail. Please select one or more users')); + } + } + catch (Exception e) { + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'Cannot Send Mail. Please check message fields')); + } + } + + public Selectoption[] getUsers() { + + User[] users = [SELECT Id, Name FROM User ORDER BY Name]; + Selectoption[] result = new Selectoption[] {}; + + for (User user :users) { + result.add(new Selectoption(user.Id, user.Name)); + } + + return result; + } +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityReportUIController.cls-meta.xml b/src/classes/OpenOpportunityReportUIController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityReportUIController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityTest.cls b/src/classes/OpenOpportunityTest.cls new file mode 100644 index 00000000..b8a7b61d --- /dev/null +++ b/src/classes/OpenOpportunityTest.cls @@ -0,0 +1,135 @@ +/************************************************** +Class Name: OpenOpportunityTest +Class Description: Class for Open Opportunities Testing and Coverage +Author: Fernando Rodriguez (frodriguez@adooxen.com) +Modified By: Fernando Rodriguez +Update Date: 2013-03-04 +Additional Comments: +**************************************************/ +@isTest +public class OpenOpportunityTest { + + public static final String TEST_EMAIL = 'frodriguez@adooxen.com'; + + static testMethod void testEmailReportSuccess() { + + createOpportunity(); + Test.startTest(); + OpenOpportunityReportUIController controller = new OpenOpportunityReportUIController(); + controller.getUsers(); + controller.reload(); + for (String stageComment :controller.stagedComments.keySet()) { + controller.stagedComments.put(stageComment, 'Test Comment'); + } + controller.initAction(); + controller.sendEmail(); + Test.stopTest(); + } + + /* + static testMethod void testEmailReportSuccessWithFields() { + + addCustomColumns(); + createOpportunity(); + Test.startTest(); + OpenOpportunityReportUIController controller = new OpenOpportunityReportUIController(); + + for (String stageComment :controller.stagedComments.keySet()) { + controller.stagedComments.put(stageComment, 'Test Comment'); + } + controller.initAction(); + controller.sendEmail(); + Test.stopTest(); + } + */ + + static testMethod void testEmailReportFailure() { + + createOpportunity(); + Test.startTest(); + OpenOpportunityReportUIController controller = new OpenOpportunityReportUIController(); + controller.recipients += 'this is not an address'; + controller.sendEmail(); + Test.stopTest(); + } + + static testMethod void testBatchEmailReport() { + + createOpportunity(); + Test.startTest(); + Database.executeBatch(new OpenOpportunitiesBatch()); + Test.stopTest(); + } + + static testMethod void testBatchRedEmailReport() { + + createOpportunity(); + Test.startTest(); + Database.executeBatch(new OpenOpportunityNeedUpdateBatch()); + Test.stopTest(); + } + + static testMethod void testOpportunityStageDuration() { + + Test.startTest(); + Id opportunityId = createOpportunity(); + Opportunity opp = [SELECT StageName FROM Opportunity WHERE Id = :opportunityId]; + opp.StageName = 'Stage 2 - Talking'; + update opp; + Test.stopTest(); + } + + private static void addCustomColumns() { + + Open_Opportunity_Fields__c column = new Open_Opportunity_Fields__c(); + column.Name = 'CreatedDate'; + column.Label__c = 'Created Date'; + column.Type__c = 'DateTime'; + column.Order__c = 1; + insert column; + + column = new Open_Opportunity_Fields__c(); + column.Name = 'CloseDate'; + column.Label__c = 'Close Date'; + column.Type__c = 'Date'; + column.Order__c = 2; + insert column; + } + + + private static Id createOpportunity() { + + Country__c country = new Country__c(); + country.Name = 'Test'; + insert country; + + Account account = new Account(); + account.Name = 'Test Account'; + account.Office_Type__c = 'Country Office'; + account.Country__c = country.Id; + insert account; + + // Create 2 Opportunities for this user and account; + Id opportunityId = createOpportunity(account.Id); + createOpportunity(account.Id); + + return opportunityId; + } + + private static Id createOpportunity(Id accountId) { + + Opportunity opportunity = new Opportunity(); + opportunity.Name = 'Test Opportunity'; + opportunity.Amount = 5000; + opportunity.Fogbugz_Assigned_To__c = 'Test Assignee'; + opportunity.Fogbugz_Ticket_Number__c = '12345'; + opportunity.Fogbugz_Last_Updated_Date__c = Date.today(); + opportunity.StageName = 'Stage 1 - Connect'; + opportunity.CloseDate = Date.today(); + opportunity.AccountId = accountId; + insert opportunity; + + return opportunity.Id; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityTest.cls-meta.xml b/src/classes/OpenOpportunityTest.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityUtils.cls b/src/classes/OpenOpportunityUtils.cls new file mode 100644 index 00000000..fef68294 --- /dev/null +++ b/src/classes/OpenOpportunityUtils.cls @@ -0,0 +1,23 @@ +public with sharing class OpenOpportunityUtils { + + public static void updateStageName(Opportunity[] opportunities) { + + for (Opportunity opportunity :opportunities) { + + opportunity.Stage_Name_Updated_Date__c = Date.today(); + } + } + + public static void updateStageName(Opportunity[] opportunities, Map oldOpportunities) { + + for (Opportunity opportunity :opportunities) { + + Opportunity oldOpportunity = oldOpportunities.get(opportunity.Id); + + if (oldOpportunity.StageName != opportunity.StageName) { + opportunity.Stage_Name_Updated_Date__c = Date.today(); + } + } + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityUtils.cls-meta.xml b/src/classes/OpenOpportunityUtils.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityUtils.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/components/OpenOpportunityReportTable.component b/src/components/OpenOpportunityReportTable.component new file mode 100644 index 00000000..c7130069 --- /dev/null +++ b/src/components/OpenOpportunityReportTable.component @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/objects/Open_Opportunity_Fields__c.object b/src/objects/Open_Opportunity_Fields__c.object new file mode 100644 index 00000000..bbe97224 --- /dev/null +++ b/src/objects/Open_Opportunity_Fields__c.object @@ -0,0 +1,36 @@ + + + List + Public + false + false + + Label__c + false + + 100 + false + Text + false + + + Order__c + false + + 3 + false + 0 + Number + true + + + Type__c + false + + 50 + false + Text + false + + + diff --git a/src/objects/Open_Opportunity_Settings__c.object b/src/objects/Open_Opportunity_Settings__c.object new file mode 100644 index 00000000..c692b514 --- /dev/null +++ b/src/objects/Open_Opportunity_Settings__c.object @@ -0,0 +1,46 @@ + + + Hierarchy + Public + Open Opportunity Settings + false + false + + Days_Not_Updated_Limit_Early_Stages__c + 10 + If Days not Updated field is greater than this value, the row will be colored red. + false + If Days not Updated field is greater than this value, the row will be colored red. +Applies for Stages 1& 2 + + 3 + false + 0 + Number + false + + + Days_Not_Updated_Limit__c + 30 + If Days not Updated field is greater than this value, the row will be colored red. + false + If Days not Updated field is greater than this value, the row will be colored red. +Applies for Stages 3 + + + 3 + false + 0 + Number + false + + + Default_Email_Recipient__c + Default Email Recipient + false + + true + Email + false + + + diff --git a/src/objects/Opportunity.object b/src/objects/Opportunity.object new file mode 100644 index 00000000..476f7e95 --- /dev/null +++ b/src/objects/Opportunity.object @@ -0,0 +1,1048 @@ + + + true + + Account_Name__c + false + Account.Name + + false + Text + false + + + Area__c + false + + + + Health + true + + + Poverty and Economic Development + false + + + Natural Resource Management + false + + + Training & Education Services + false + + + Logistics + false + + + Other + false + + false + + false + MultiselectPicklist + 6 + + + Business_Unit_Owner_Name__c + false + Business_Unit_Owner__r.FirstName + ' ' + Business_Unit_Owner__r.LastName + BlankAsZero + + false + Text + false + + + Business_Unit_Owner__c + SetNull + false + Who owns the Biz Dev process for this business unit. + + User + Opportunities + false + false + Lookup + + + Country_Name__c + false + Country__r.Name + + false + Text + false + + + Country__c + Restrict + false + What country is this opportunity taking place. + + Country__c + Opportunities + Opportunities + false + false + Lookup + + + Deadline_for_Submitting_Proposal__c + false + Internal deadline for review ready. + + 32768 + false + LongTextArea + 3 + + + Fogbugz_Assigned_To__c + false + Who is the Fogbugz ticket assigned to currently. + + 50 + false + false + Text + false + + + Fogbugz_Client__c + false + + 50 + false + false + Text + false + + + Fogbugz_Days_Not_Updated__c + Today - Fogbugz Last Updated Date + false + TODAY() - DATEVALUE(Fogbugz_Last_Updated_Date__c) + BlankAsZero + + 18 + false + 0 + Number + false + + + Fogbugz_Last_Updated_Date__c + false + The last day the Fogbugz ticket was updated. + + false + false + DateTime + + + Fogbugz_Link__c + false + "http://manage.dimagi.com/default.asp?" & Fogbugz_Ticket_Number__c + + false + Text + false + + + Fogbugz_Most_Recent_Note__c + false + The most recent update to Fogbugz ticket. + + 32768 + false + LongTextArea + 5 + + + Fogbugz_Opened_By__c + false + + 50 + false + false + Text + false + + + Fogbugz_Owner_Mapping__c + false + CASE( Fogbugz_Assigned_To__c , +"Gillian Javetski", "Neal Lesh", +"Benjamin Lightburn", "Neal Lesh", +"Andrea Fletcher", "Kieran Sharpey-Schafer", +"Sheel Shah", "Devika Sarin", +"Mohini Bhavsar", "Devika Sarin", +"Krishna Swamy", "Devika Sarin", +"Vikram Kumar","Neal Lesh", +"Nick Nestle") + + false + Text + false + + + Fogbugz_Probability__c + false + + 18 + false + 0 + false + Percent + + + Fogbugz_Ticket_Number__c + true + The ticket number of the corresponding Fogbugz ticket. + + 50 + false + false + Text + false + + + Funder_Account__c + Restrict + false + The account that is supplying the money for the opportunity. i.e. USAID funds CARE who then pays us. USAID is the "Funder Account", CARE is the "Account". + + Account + Opportunities Funded + Opportunities_Funded + false + false + Lookup + + + Funding_Type__c + false + Please flag if NIH or gov't potentially + + 32768 + false + LongTextArea + 3 + + + Implementing_Business_Unit__c + SetNull + false + The Dimagi BU that will manage the execution of the project. + + Business_Unit__c + Opportunities (Implementing Business Unit) + Opportunities1 + false + false + Lookup + + + Industry__c + false + + + + Agriculture + false + + + Consulting + false + + + Education + false + + + Finance + false + + + Health + false + + + Mobile Money + false + + + Nutrition + false + + + Other + false + + + Telecom + false + + + Water & Sanitation + false + + false + + false + Picklist + + + LeadSource + + + Contact Us + false + + + Referred to Us + false + + + Other + false + + + Conference + false + + + Pulled from Website + false + + + Publication - First Author + false + + + Publication - Last Author + false + + + Web Sign Up + false + + + Workshop - Maputo + false + + + Workshop - Dakar + false + + + Workshop - Other + false + + false + + Picklist + + + Opp_Stage__c + false + CASE( StageName , +'Stage 1 - Connect', 'Stage 1 - Connect', +'Stage 2 - Talking', 'Stage 2 - Talking', +'Stage 3 - Prospect', 'Stage 3 - Prospect', +'Stage 4 - Proposal Development', 'Stage 4 - Proposal Development', +'Stage 5 - Submitted', 'Stage 5 - Submitted', +'Stage 6 - In Negotiations', 'Stage 6 - In Negotiations', +'Closed - Lost','Closed - Lost', +'Closed - Won', 'Closed - Won', +'Pending other action', 'Pending other action', +'Closed','Closed', +'Stage 1 - Connect') + BlankAsZero + + false + Text + false + + + Opportunity_Owning_Entity__c + Restrict + false + Which busniness unit owns this opportunity. + + Business_Unit__c + Opportunities + Opportunities + false + false + Lookup + + + Product_Text__c + Populated via a trigger + false + Stores the products on the opp record so you can filter on them. + + false + false + TextArea + + + Project_Dates__c + false + Start Date, when features need to be ready, etc. + + 32768 + false + LongTextArea + 4 + + + Proposal_Dropbox_Location__c + false + This should be in Dimagi - Proposals + + false + false + TextArea + + + Report_Out_Summary__c + false + Used in the Biz Dev Report Outs to add comments. + + false + false + TextArea + + + Salesforce_Opportunity_ID__c + false + Id + BlankAsZero + Salesforce's internal reference ID. Put this in the Fogbugz External ID field. + + false + Text + false + + + Short_Description__c + false + + false + false + TextArea + + + StageName + + + Stage 1 - Connect + false + false + Pipeline + 0 + false + + + Stage 2 - Talking + false + false + Talking with an org but there is no concrete opportunity yet. + Pipeline + 0 + false + + + Stage 3 - Prospect + false + false + There is a real discreet opportunity now. A Fogbugz ticket is created. + Pipeline + 0 + false + + + Stage 4 - Proposal Development + false + false + Developing a proposal. + Pipeline + 0 + false + + + Stage 5 - Submitted + false + false + You have submitted the proposal + Pipeline + 0 + false + + + Stage 6 - In Negotiations + false + false + Won the award and negotiating price. + Pipeline + 0 + false + + + Closed + false + false + Pipeline + 0 + false + + false + + Picklist + + + Stage_Duration__c + false + TODAY() - Stage_Name_Updated_Date__c + BlankAsZero + + 18 + false + 0 + Number + false + + + Stage_Name_Updated_Date__c + false + + false + false + Date + + + Sub_Area__c + false + + + + *** Health *** + false + + + Maternal, Newborn, & Child Health + false + + + Family Planning + false + + + HIV/AIDS + false + + + Malaria + false + + + Respiratory Diseases + false + + + Tuberculosis + false + + + Polio + false + + + Vaccinations + false + + + Diarrhea + false + + + Primary Care + false + + + Non-Communicable Diseases + false + + + Mental Health + false + + + Nutrition + false + + + *** Poverty and Economic Development *** + false + + + Gender Services + false + + + Water, Sanitation, & Hygiene + false + + + Financial Services to the Poor + false + + + Urban Development + false + + + *** Natural Resource Management *** + false + + + Agriculture + false + + + Food Security + false + + + Environment + false + + + *** Training & Education Services *** + false + + + Adult Training + false + + + Child Education + false + + + Early Childhood Development + false + + + *** Logistics *** + false + + + Human Resources + false + + + Commodity Tracking/Procurement + false + + + *** Other *** + false + + + Emergency Response + false + + + Orphans and Vulnerable Children + false + + + Telecommunications + false + + false + + false + MultiselectPicklist + 8 + + + Tech_Capabilities_Features__c + false + Known technical dependencies / architecture. + + 32768 + false + LongTextArea + 5 + + + Total_Days_Not_Updated__c + false + MIN(Fogbugz_Days_Not_Updated__c, TODAY() - DATEVALUE(LastModifiedDate)) + BlankAsZero + + 18 + false + 0 + Number + false + + + Type + + + Existing Business + false + + + New Business + false + + false + + Picklist + + + X10_Major_component_risks__c + false + + 32768 + false + LongTextArea + 3 + + + X11_Worked_with_org_before__c + false + Any special context to know about? + + 32768 + false + LongTextArea + 3 + + + X4_Budget_Size__c + false + Size, split: dev/field/server, are we willing to lose money on this? + + 32768 + false + LongTextArea + 3 + + + X5_Which_Entity__c + false + Inc, DSA, DSI + + 32768 + false + LongTextArea + 3 + + + X7_Long_term_partnership_or_one_off__c + false + Partnership potential or just is this a one-off project? + + 32768 + false + LongTextArea + 3 + + + X8_Other_Direct_Costs_ODC_covered_by__c + false + Are ODC covered by Dimagi or the partner. e.g. SMS gateway. + + 32768 + false + LongTextArea + 3 + + + X9_Room_for_innovation__c + false + Is there room for innovation or are we specifically neglecting parts of the RFP? + + 32768 + false + LongTextArea + 5 + + + long_wait__c + false + false + For submitted opportunities with a long waiting period to hear back. These won't be marked red on the Biz Dev reports. + + false + Checkbox + + + Active_Opps + OPPORTUNITY.NAME + ACCOUNT.NAME + Funder_Account__c + Area__c + Sub_Area__c + OPPORTUNITY.AMOUNT + OPPORTUNITY.STAGE_NAME + Fogbugz_Probability__c + CORE.USERS.ALIAS + Everything + + OPPORTUNITY.STAGE_NAME + notEqual + Closed + + + + + AllOpportunities + OPPORTUNITY.CREATED_DATE + OPPORTUNITY.NAME + ACCOUNT.NAME + CORE.USERS.FULL_NAME + Fogbugz_Assigned_To__c + Fogbugz_Ticket_Number__c + Fogbugz_Link__c + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Opportunity_Owning_Entity__c + Country__c + Country_Name__c + Everything + + + + ClosingNextMonth + Everything + + OPPORTUNITY.CLOSED + equals + 0 + + + OPPORTUNITY.CLOSE_DATE + equals + NEXT_MONTH + + + + + ClosingThisMonth + Everything + + OPPORTUNITY.CLOSED + equals + 0 + + + OPPORTUNITY.CLOSE_DATE + equals + THIS_MONTH + + + + + Moz_Opportunities + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Everything + + + + Moz_Opportunities1 + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Mine + + + + MyOpportunities + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.CREATED_DATE + OPPORTUNITY.STAGE_NAME + Fogbugz_Assigned_To__c + Fogbugz_Ticket_Number__c + Salesforce_Opportunity_ID__c + CORE.USERS.ALIAS + Fogbugz_Client__c + Fogbugz_Last_Updated_Date__c + Fogbugz_Link__c + Fogbugz_Most_Recent_Note__c + Fogbugz_Opened_By__c + Fogbugz_Probability__c + Mine + + OPPORTUNITY.STAGE_NAME + notEqual + Closed Won,Closed Lost + + + + + My_South_Africa_Opps + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.STAGE_NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + CORE.USERS.ALIAS + Country__c + Opportunity_Owning_Entity__c + Fogbugz_Link__c + Everything + + Country__c + equals + South Africa + + + + + NewThisWeek + Everything + + OPPORTUNITY.CREATED_DATE + equals + THIS_WEEK + + + + + Non_Integrated_Opportunities + OPPORTUNITY.NAME + Fogbugz_Assigned_To__c + ACCOUNT.NAME + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Fogbugz_Client__c + Fogbugz_Last_Updated_Date__c + Fogbugz_Link__c + Fogbugz_Opened_By__c + Fogbugz_Ticket_Number__c + Everything + + CORE.USERS.ALIAS + equals + fogbugz + + + + + Opportunities_without_Accounts + OPPORTUNITY.NAME + ACCOUNT.NAME + Salesforce_Opportunity_ID__c + CORE.USERS.ALIAS + OPPORTUNITY.STAGE_NAME + Fogbugz_Assigned_To__c + Fogbugz_Link__c + Everything + + Fogbugz_Assigned_To__c + equals + closed + + + + + Opportunities_without_an_Owner + OPPORTUNITY.NAME + ACCOUNT.NAME + Fogbugz_Assigned_To__c + OPPORTUNITY.AMOUNT + OPPORTUNITY.STAGE_NAME + Fogbugz_Link__c + Fogbugz_Ticket_Number__c + Everything + + Fogbugz_Assigned_To__c + equals + CLOSED + + + + + Won + Everything + + OPPORTUNITY.WON + equals + 1 + + + OPPORTUNITY.CLOSED + equals + 1 + + + + + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.CLOSE_DATE + Sync_with_FB_multi + OPPORTUNITY.NAME + ACCOUNT.NAME + ACCOUNT.SITE + OPPORTUNITY.NAME + ACCOUNT.NAME + ACCOUNT.SITE + OPPORTUNITY.NAME + ACCOUNT.NAME + ACCOUNT.SITE + OPPORTUNITY.STAGE_NAME + OPPORTUNITY.CLOSE_DATE + CORE.USERS.ALIAS + + + Change_Stage_Name + false + ISCHANGED( StageName ) && $User.ProfileId = '00eb0000000gmdW' + StageName + You may not make changes to this field in Salesforce. Please edit the Fogbugz ticket. + + + Sync_with_FB_multi + online + massActionButton + 600 + page + Sync all with FogBugz + sidebar + RunFBSync + false + true + + + Sync_with_FB_single + online + button + 600 + page + Sync with FogBugz + sidebar + RunFBSyncSingle + false + + diff --git a/src/package.xml b/src/package.xml index 3340efa1..17eb7117 100644 --- a/src/package.xml +++ b/src/package.xml @@ -4,5 +4,55 @@ * ApexClass - 25.0 + + * + ApexComponent + + + * + ApexPage + + + * + ApexTrigger + + + * + Account + AccountContactRole + Activity + Asset + Campaign + CampaignMember + Case + CaseContactRole + Contact + ContentVersion + Contract + ContractContactRole + Event + Idea + Lead + Opportunity + OpportunityCompetitor + OpportunityContactRole + OpportunityLineItem + PartnerRole + Product2 + Site + SocialPersona + Solution + Task + User + CustomObject + + + * + CustomTab + + + * + StaticResource + + 27.0 diff --git a/src/pages/OpenOpportunityFieldSelection.page b/src/pages/OpenOpportunityFieldSelection.page new file mode 100644 index 00000000..4afa38b1 --- /dev/null +++ b/src/pages/OpenOpportunityFieldSelection.page @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pages/OpenOpportunityReportLayout.page b/src/pages/OpenOpportunityReportLayout.page new file mode 100644 index 00000000..6f50aa5c --- /dev/null +++ b/src/pages/OpenOpportunityReportLayout.page @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/tabs/Open_Opportunities.tab b/src/tabs/Open_Opportunities.tab new file mode 100644 index 00000000..4f120dd2 --- /dev/null +++ b/src/tabs/Open_Opportunities.tab @@ -0,0 +1,7 @@ + + + + false + Custom56: Bottle + OpenOpportunityReportLayout + diff --git a/src/tabs/Report_Settings.tab b/src/tabs/Report_Settings.tab new file mode 100644 index 00000000..2113f012 --- /dev/null +++ b/src/tabs/Report_Settings.tab @@ -0,0 +1,7 @@ + + + + false + Custom26: Flag + OpenOpportunityFieldSelection + diff --git a/src/triggers/OpenOpportunityUpdateTrigger.trigger b/src/triggers/OpenOpportunityUpdateTrigger.trigger new file mode 100644 index 00000000..d5f49324 --- /dev/null +++ b/src/triggers/OpenOpportunityUpdateTrigger.trigger @@ -0,0 +1,9 @@ +trigger OpenOpportunityUpdateTrigger on Opportunity (before insert, before update) { + + if (trigger.isInsert) { + OpenOpportunityUtils.updateStageName(trigger.new); + } + else if (trigger.isUpdate) { + OpenOpportunityUtils.updateStageName(trigger.new, trigger.oldMap); + } +} \ No newline at end of file diff --git a/src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml b/src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml new file mode 100644 index 00000000..6e684be3 --- /dev/null +++ b/src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/triggers/OpportunityLocationTrigger.trigger b/src/triggers/OpportunityLocationTrigger.trigger new file mode 100644 index 00000000..1eb597af --- /dev/null +++ b/src/triggers/OpportunityLocationTrigger.trigger @@ -0,0 +1,9 @@ +trigger OpportunityLocationTrigger on Opportunity_Location__c (before delete, after insert) { + + if (Trigger.isInsert) { + OpportunityTriggerSync.onInsert(Trigger.new); + } + else if (Trigger.isDelete) { + OpportunityTriggerSync.onDelete(Trigger.old); + } +} \ No newline at end of file diff --git a/src/triggers/OpportunityLocationTrigger.trigger-meta.xml b/src/triggers/OpportunityLocationTrigger.trigger-meta.xml new file mode 100644 index 00000000..1257ef61 --- /dev/null +++ b/src/triggers/OpportunityLocationTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 27.0 + Active + diff --git a/src/triggers/OpportunityProduct.trigger b/src/triggers/OpportunityProduct.trigger new file mode 100644 index 00000000..22ef899f --- /dev/null +++ b/src/triggers/OpportunityProduct.trigger @@ -0,0 +1,5 @@ +trigger OpportunityProduct on OpportunityLineItem (after delete, after insert, after update) { + + + +} \ No newline at end of file diff --git a/src/triggers/OpportunityProduct.trigger-meta.xml b/src/triggers/OpportunityProduct.trigger-meta.xml new file mode 100644 index 00000000..1257ef61 --- /dev/null +++ b/src/triggers/OpportunityProduct.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 27.0 + Active + diff --git a/src/triggers/OpportunityToCase.trigger b/src/triggers/OpportunityToCase.trigger new file mode 100644 index 00000000..2a2054f8 --- /dev/null +++ b/src/triggers/OpportunityToCase.trigger @@ -0,0 +1,15 @@ +/** + * Creates a FogBugz case upon Opportunity creation + * + * @todo Handle bulk insertions + * + * @author Antonio Grassi + * @date 11/13/2012 + */ + +trigger OpportunityToCase on Opportunity (after insert) { + + if (Trigger.new[0].Fogbugz_Ticket_Number__c == null) { + OpportunityTriggers.createInFogbugz(Trigger.new[0].Id); + } +} \ No newline at end of file diff --git a/src/triggers/OpportunityToCase.trigger-meta.xml b/src/triggers/OpportunityToCase.trigger-meta.xml new file mode 100644 index 00000000..f9a06af0 --- /dev/null +++ b/src/triggers/OpportunityToCase.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 25.0 + Active + diff --git a/src/triggers/OpportunityTrigger.trigger b/src/triggers/OpportunityTrigger.trigger new file mode 100644 index 00000000..90fad743 --- /dev/null +++ b/src/triggers/OpportunityTrigger.trigger @@ -0,0 +1,4 @@ +trigger OpportunityTrigger on Opportunity (before insert, before update) { + + OpportunityTriggerSync.onOpportunityTrigger(Trigger.new); +} \ No newline at end of file diff --git a/src/triggers/OpportunityTrigger.trigger-meta.xml b/src/triggers/OpportunityTrigger.trigger-meta.xml new file mode 100644 index 00000000..1257ef61 --- /dev/null +++ b/src/triggers/OpportunityTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 27.0 + Active + diff --git a/src/triggers/ProjectLocationTrigger.trigger b/src/triggers/ProjectLocationTrigger.trigger new file mode 100644 index 00000000..16cfeedb --- /dev/null +++ b/src/triggers/ProjectLocationTrigger.trigger @@ -0,0 +1,11 @@ +trigger ProjectLocationTrigger on Project_Location__c (after insert, before delete) { + + + if (Trigger.isInsert) { + ProjectTriggerSync.onInsert(Trigger.new); + } + else if (Trigger.isDelete) { + ProjectTriggerSync.onDelete(Trigger.old); + } + +} \ No newline at end of file diff --git a/src/triggers/ProjectLocationTrigger.trigger-meta.xml b/src/triggers/ProjectLocationTrigger.trigger-meta.xml new file mode 100644 index 00000000..1257ef61 --- /dev/null +++ b/src/triggers/ProjectLocationTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 27.0 + Active + diff --git a/src/triggers/ProjectProduct.trigger b/src/triggers/ProjectProduct.trigger new file mode 100644 index 00000000..702d2775 --- /dev/null +++ b/src/triggers/ProjectProduct.trigger @@ -0,0 +1,9 @@ +trigger ProjectProduct on Project_Product__c (after insert, after update, after delete) { + + if (Trigger.isDelete) { + ProjectProductTrigger.onUpdate(Trigger.old); + } + else ProjectProductTrigger.onUpdate(Trigger.new); + + +} \ No newline at end of file diff --git a/src/triggers/ProjectProduct.trigger-meta.xml b/src/triggers/ProjectProduct.trigger-meta.xml new file mode 100644 index 00000000..1257ef61 --- /dev/null +++ b/src/triggers/ProjectProduct.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 27.0 + Active + diff --git a/src/triggers/ProjectTrigger.trigger b/src/triggers/ProjectTrigger.trigger new file mode 100644 index 00000000..4789206b --- /dev/null +++ b/src/triggers/ProjectTrigger.trigger @@ -0,0 +1,6 @@ +trigger ProjectTrigger on Project__c (before insert, before update) { + + ProjectTriggerSync.onProjectTrigger(Trigger.new); + ProjectTriggerArea.onUpdate(Trigger.new); + +} \ No newline at end of file diff --git a/src/triggers/ProjectTrigger.trigger-meta.xml b/src/triggers/ProjectTrigger.trigger-meta.xml new file mode 100644 index 00000000..1257ef61 --- /dev/null +++ b/src/triggers/ProjectTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 27.0 + Active + diff --git a/src/triggers/TaskToCaseNote.trigger b/src/triggers/TaskToCaseNote.trigger new file mode 100644 index 00000000..f928eafa --- /dev/null +++ b/src/triggers/TaskToCaseNote.trigger @@ -0,0 +1,32 @@ +/** + * Adds a note to the FogBugz case upon Task creation + * + * @todo Handle bulk insertions + * + * @author Antonio Grassi + * @date 11/16/2012 + */ +trigger TaskToCaseNote on Task (after insert) { + + Set tasksInSet = new Set {}; + + for (Task t:Trigger.new) { + tasksInSet.add(t.Id); + } + + Task[] tasks = [select Id, + WhatId + from Task + where Id in :tasksInSet + and Subject like 'Email: %' + and What.Type = 'Opportunity']; + + if (!tasks.isEmpty()) { + + Opportunity o = [select Fogbugz_Ticket_Number__c from Opportunity where Id = :tasks[0].WhatId]; + + if (o.Fogbugz_Ticket_Number__c != null) { + TaskTriggers.addNoteInFogBugz(tasks[0].Id); + } + } +} \ No newline at end of file diff --git a/src/triggers/TaskToCaseNote.trigger-meta.xml b/src/triggers/TaskToCaseNote.trigger-meta.xml new file mode 100644 index 00000000..f9a06af0 --- /dev/null +++ b/src/triggers/TaskToCaseNote.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 25.0 + Active + From f2c3a870712b72d38dcc3e438725d795cbf72f05 Mon Sep 17 00:00:00 2001 From: Fernando Rodriguez Date: Mon, 8 Apr 2013 12:47:49 -0300 Subject: [PATCH 2/4] adding static resources --- src/staticresources/SiteSamples.resource | Bin 0 -> 44926 bytes .../SiteSamples.resource-meta.xml | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 src/staticresources/SiteSamples.resource create mode 100644 src/staticresources/SiteSamples.resource-meta.xml diff --git a/src/staticresources/SiteSamples.resource b/src/staticresources/SiteSamples.resource new file mode 100644 index 0000000000000000000000000000000000000000..014adbd85cf86a384a9838e46bc02eef1ad28882 GIT binary patch literal 44926 zcmZ^oV~{R9u%O4bZQFduo-?*>+qP}nJY(CoZS#z=-`yWu_s^!QgGy3e=}M|oPb>TZ z1w#XZ`cHr;ta*X{S3&r%uWadJs_f!vW9rOc?Cfl-AS5@$gajC?Z&-G&YNb5h-XD%- z#*+RIQ8M2^lJVER%~1fir9A#Cd;k#EURe& z0`dFykYl4k*XeE`WPY@nDR=H$li1a|eT~R|H}v7Ijwy4Xw!SG!T&d~v^8fmwy`(1W^k}G2R1;-zll_klYoc8f%$I| zUP@9_>AyDWKlOzI`LBL-vPl90LZOrr6;kooyxECVE1tvwH`x2_x#b>fodINJa3_RN zPb+{w?|@~CQxGQ%sIBVlAOk1P(fp9WAolf^^XBz>{wL-3dG#|#9ep}W#aR+2@ez45 z`6TVDvn?-muJ<;x`66JyO~>bo8_q6s%z(A86RkdgS5!3o`+Hv1qetBtkcf-yDtlU8 zufxAm>l$6-BOL8D6DEPVFd72Y#!#dQ=No;7NFu|>AfIgXQ%CwfSu9%h?9R>V>Ny=> zKW5z5^l+|>cmrU8-7TX%17w1DkJa6=c|9FlCsN$4Cu7gJNc2SgxX8@6>-18u{sqy! zlL}G^{JmuOqWmJm-~oY5+-$CQQv*lFfODy+Y+*AW}8wn$&Y283U0DxXtP5#emv++-t75Q1^u8cy7Ms^O*SAV%pL)X^P z5_&zYq85iH=9&=L*NHwZf`}6;K4z4=mT2LsHs3NyHL%DhIOoW7PKhc_if80J#hgX; zUuT>HD;T*xRBeK!(VZ0-{GN0uqzNg@ooy2)s2RC>3bB2kbf|RktP^rfQQI+V;5ah5cCSJdyNS?gl}OhA(NaqBDP04gRJiVsMxhhC_$Lo2EC!@o^1=v zos}m!o`;j$3|bgN`L;OD&3tsCa2P5$^Nz0Vckrp=(!6s_7%)TmzE?x2Utj&Tc?|7k zywrh*CS%U#MfZ{v5I>~ezPsKF$5n7d39+y)vPZ6xLz1{%4I|TKTKBVXt`{aobW?wu z>2Jk5)urMrT+FJ#TZnJ9-3al+yONYaFk9)b3v@XXMwCJfd1NiRwh@^DqQce=$5_Eo z!B!Fch!RHhS6OF3#}q}K1WhSn;i~E_QcdO7)}u;_P96P(Ns@=?82@YWXoWnf zoFkbwG_QV=Co-|?gNZjx9(BOHw~4Q^+X}&=B7`B@Q5q*5OyAdRd{mAX(>mg-wtyhc zu-t4dif9G&Di<~+bomn)RS)xv7h&kDv)7appqjMr4?J<`ubHZfv7G>w@S-b0QlZ}F zhFc{s_mqDe4!M?FGaq}JmIjP9(DjQNUwNOc_lOXy=lg3B1W)KIrnpvwXu`sjUu4u< zXvK_?3{zmNthTVKU_M?Cl^+w2GWd@S(7rBjL0HDQX-l23>vGC^45Qpg;+wj-3&%Y` zd)jcD)5|UAb#5^Qb3+m+a-y;Cx-e1xpu3*dg$y=YeFOa344u}_EMwORcWg5z)W8ty zhi#EkpbxZ8vJy2UO045U2!ye$n5d9~MKpQkcez{`f6wiTG@V#Hz)nPqBBGkp(7BT- zxBYE>IfZTK6s?bl)6PYABL|3kapDaV&we6&4i$$(y)534%L)cJsOoDt z#n4Yg|Z}qsrd>L%NrwBuKR$popQK zgJOPs3B)=-XJAON50;a>=)mx<)CV5`?g5E6(-7df($HHK?gzyc+YBPZ(YAspuCOU7 zDAv|dll1O{3x1+kCTEgZX27TN_lre0Ttj$fKvBqHAY&O>A%o-Nu1v&3s0W&Ps{58 z^9oVV$vGALt?CmoX+HhVT$56`vDTtplPJPt11BN#-MZ{4j#iFA~tP3G)ld^o-_u6#)Ec-gh303yRF!-7=6 zgU##M*b7VM*^gOkGAKb}e#v8qb{AuzG3D53{Dj)RP(&2rbPj}yx=i-hb*-`|52MXE zspAk?JoUezGG!@A8(lJKffb5w#k{kOvnQhEV<#L1T}>7eGNoYJLgM)8f60KbRzwb` zbm1Gk?ESnysUApHHCH%AX0alKThTQ#97^NK%>!8D&EI9teZpgUWwH6C5-qGAITpLA z(31gLBxb9r6G7fK^h&`5_MLpE_Xb!d8zp&s#1K_Cf1o-hsisEz$ipU7PN1j2n#sEB z79frkm`qCf%tQ#pkyH%7dN1J6Rfl$$TUeJ@8A!-n|Jnp)PkF@Y#X=Hm&0Ac``VXGo zKkFmy$T<4Zs35dUyBv(HUe@p5p7oI4VpqGDp|7}18BrM5QADif$h9Dcjjc7jR* zLl7I@Djk@4yY(c4@rme}6>LjPAbiFJ?%7V+l5H66A>+u z47?Y_KNG&_v-!2|_RAQEVH{LGk3M{>D*l4BG05zu&oH_%F9?x+v!OaN1LRboHNG$C z!JP`Hm{XSBSL|2X70ZHTe{oILJ0-7GMS4&l$8OfbMy45WNw#Q5d{9=TRV zi)k*~te-MCJgsJatYxB0I8TfLK{@>A18H-pMHv30?gw~dt}vS6C9#AOeH?R6Q@>jo z7!&cU-LEvm9!g^+X8p=)P~gq1GM=GMRs>oD-~LlWB6i+$ZKR z=)X5ikuu)C_`Er#^cem91TJLO8(YM|PXgqW?BXIGR!nVva25^QBuwUJ7edgNdAvC` zt>6`1yaD8)Li7gry@af%$vs86Cr}Z*?Dhhxp4vzGEtPB88sNQm=}b`}X3kAeF+Udc z${r;reeV^P7SbiZcr0BBPS`C`k7e=-c5-rW3eX>kEab=Mb~#edMuYQ^D7uM6XE+Iz zC-*JWWus!xNGhXi7&UYnZbKkF<;i#{@b8 zPRf_R%DIr4uRO$vJZ6cbIG=5SsW!F(MC5r-DZHElfyFRdA){l%RL)wU=cXncB> z1XcfCjcg|PNzPzg7sWcMjWE7n!HOjuFu=h@InkW0!Jx+1UFeLLM~UMi#6J*zy0K(c zsU;ECpAygoMAPgSy7GNLKOcfn0M^HwdQG{`_|U!dAVjhsZU)ok`JC2bI}ajAY6g6j zt&c|B*lwH>!t*2d3Iz0Qn#p_4(ukO+oN&|bGtzy7&qqeHA8v9a?s0gQJuQw8J^VPS z^z`WO7I*w|zsS0HnykjO3G{_>AF!AwQcPfHGM@)Rkrz3ERoWr2`OJ6r?6A$3JBAK# z?3im)P$y9;<=*JQ=4_!U<6>yp&aOAXu>V#wyOF!!)q-)O4X(G6H_Sg9>9zXp3Iff> zyq}p=b7tIMtsLvJR)VC761h~oJzp>+^tt{sse$iXAmvVzo&Jj zUKk8XeMNy1LYF$3VWDY?_N3fVk+C`pP20vC3CC=&SmrYXn+4)u51+ypkdZ+b z`iCSO^uN%RnPcn2DpBI+s+f;-#AZi|+~0XTLa$kHsU6RQUTa_TqjPnsTIP%&J2C_% z+}<`b#B4ed)wwZC3#GFn%Wx{v5_zWg%~4}3ZOpjwc&r}JrlXvKN}fz0p_AbpP?~`J z0n`%Kjo9=<6Z%3EL2%4rHu}CGCwrW41AysF2(em_*kjP}~sP zt^Z=@)2Tyr5N;-`#6bmwFcpR!o#-=;N8D1z%tiX46b_;h{^BR6uH1>xd26QqQ3E|o z{cy0}ZAbhkY1C3|hz+-xqAxL);^nqd1y4NN|JVVN*RnKt;_eb|Ev6%0Hsj`yQ@~L< zdIo3iaVNl(LV zD}<>>!btqAQ}>~H4W5Z>f|%ygpwYpHVw&YY4er8>SaavIdm_>GQ)uF7^G?g6u3&hB zd!FmnBu}fk$;UTK#&B4>#`K|vqN)Oo20KMM3mezGj96=Y2i{y|^l4_3U6n)K(|CG7z$`%7T&Cb(0Kt$s&2~X=u)DGNPtvWUx?===l(E$aPcD z77Ynd#Qp6cuR}j`oXf(Ku4teSGMT}Jm@|<&<}AA~inm_p26&K-8u;zACs=s@(~T<= z+kusfW2>ERirZ`#H_bZ^NfUi(a-lpO30GV>7vhvbr9Npk4BdrI9tnOf_tso#C@H%7 zTlc4v;hZNPt5a^g#S=m7$f`O5{_jH)!1cC~p>Kra_l+CjALZ9wh_j6_QxSk0TMRe3 zMlK!lOGq~f5^;L4ht@+%s>T!xt8JHYj>YN-=Sn{>dB5Xd$Pi@rp+R=;dUgXd0l=}2 z*#J>k2Wr4fU>!Kw7d8>E2y8&kw*f;SK!T;zqln|+DYyD*n?U9f7d2dh_4`x^%cM(y z0+WNDwhMj%BtAI1cjWZ<`nXOt(;T_Q@t>(DK^5=lY2E+_FZ5X+Cqc=aT$-@bIMcKW zF&i2X8vNzHK~Y-MAAy7PeZ@0HTy_1DM~_V6#V!4UY&!n#>YV-@B$=d^wbXzgc)<96 zs`7F>QcT6sFsCW(h^J0bcdaS)EPr&lDibrVi;E}V{WBKPzF&dhn&`5)<9>P1l`@B@ zQi@pkysJg#1&<)~pC`gZ%0cAjxwDF+m?xew_N}tg`H%6Y%LL60rkqMlL+Aj7;9px5 zhDcUv=sX8{OHEMuW$|p!)j}vl*JsTJ&U|$$I7;jgo(Trzp6z@>QCEfg!V~}2CPI}H z#>2ATaIIRcUrlM}5I zHVFD3K9#Mh?qiAq1Z41^{{Q(D+JC$LFQ2lvb9QlZHFmMIxBDMVm8z|(jwh3VkA<}@ z;;^=VdwYu#)TQk}xV=N{;Q)UNC+FZEzJELG*Y#`gnN#Ogz2qu=+k=Q-Swt2a6cPU) zAyhCiC0qV^+W+mjFZi|TOL)KnA_}3QC9XQ?@VY%i7im9iDXqOYV?NLp0S)^5lZG2qZNASspc-RadNi-O1oD#v7YPEvJdh>!nMl;(2u&Dd%cg`Y_~MNwbs0_gAawgjnUx?xTb5 z`YzfGCt&{u*^Tk<6z!vGVhR3YfP#gG&%D#uQ@;>m zTF_1o)1L5M7otMwV0U_0t4avi7r`H%fJA!cZ` z!x`utwxTP?T0sv-)5;!32!7Wj{0&jMX|<4{=Ez(5Him15C2c)ZC^(sT_BovFyQ)Gi=+1Wf+& z`qiG->$SS~ITIR8D!&bHnc8pL&PVc*ko_{yfQw-v z<=r0fgaIS|KTz$|GVYTzpB_~IC?h@ox|W=$x}Ao?UJ7XyJ$RE3l1aYf+}RL&nNgx~ zg=ex=rUNnCqo;cZO!A`moC>`?QYyu3_1ED_Hq40BAW{9+3j^+`?PG&?8tw^=Jwnwq zv_?#LY>O1uPaq&_)%_dt3?miKwyEOZ^bPKTG=?FH?J}AjZDLaS3jJwYB(oj zZlvSKZ7-%R3wNp2_-~QOn~iie36~Ql>NAAYjkvv8ytzV%tJ>3bBxqWS2F{kM{Uv+H z9sF6L^Wuta1orHfe;bwjyK%|#e^d*49xPtowY{2(leR$o4rMDoJs z%@5C#Jp}eYyW^pkQn*O$Oe~n`Pl5ZegYZm$>Xg+s+jcFvHOM6Ct@Zj5v_8}H-$pw6 z44FUkQ<>OwVmGBm8-03qg$E-d$r0Wr-0GBTIF)K<555HJoUY}1kHV4G5Gr;LkzRH& zxECRTYg!I17Q_8jLVum&z7}b$Rrqs-08?dEaT|XxpDSWs{`+a#f6?`%{gjlPFECcj zJP?NdTQUE0)~9H^+zU>c=RZ|@ z!;(H@Y2=iZ%T+i_{%W7^gXw(6P2jJh%^I_2r-x>G{wgKHbn(b;*4@o;GQ~6v)j9aX zCFo;?^6ZtUtNma?nis-3&MzXm<6(9Fl}ezhKZgEc@BaNW{f30fSRvQ5ZucErKd#Xk`c#e;UT<=08v8`$G7S>Bcgb%%hrg$A2l;cIRiZvy*QpV_}PX0Trb?kN}`XFSZ&1st@$b z8*Ih;CaEB5AAa#MtHsjSp+qd6 z?}Gi$4{0CxhoN0VNJ)2W)9Zw)wbGWn@x?)^bH!#LYz>T0VOmKMOO1SQsx`&PQrvZ} z3MzOzJlIVNHIx3q)_;|ygPjxT(oKmHb3*rH@CBUIzO9LTv;|;{&-E#&#udGMTB7(I+8gi7LeuUa2P5Jvqfw_jGjOeFA4;JjstUIed!g!J!x-k9d&Ja$J zFWB=d^HQtQO&p=#TTi2Q%4?zBYs3Qx=rL;{qEeroEU#}sxcyDgMV)=etr~1hjr+r_F`R)8bl);aI zk3&6cwWGEtcOb(#+Dunp6G1BorGk~#xz!>hD%>4w>gRs1&%|09VF6>qxf7t)6XR+Yxk9yOR>}JRQJ~pDM>B9z^6AT9k;V=|zdk5n`qnr@^UK_$yv@c2i_f>YCH+<5#1^IKh#D7fIi;M_p!crl&j9 zuxM@&;h4y<<51N;ES47n0Qq(wzoLvkA9V?FeV8YI%s=!cRNYx^dENc7R5p0Xf)+eY zR=-kJKnjd6R*eaH6WOUugrWJS!%h(W!}+fg=%_?8&pOTTim zh!8rTJmZTZ)W0Z7YfR#Ah3Pa~Qr7{0I!xCY792q{l9$erD(L& z@Iz$w<$14P7wszC&{4frg-}3B1&n`y4Ii8jdN|4S8H2^F3=_5Cb#7SuOZ@g9ZUUV- z9Pnd(XskzQx(no#U-IhCv8Sb>$t6x)L(^kqc$G@R>&ry$ANvR!m$7uX?~!A$^qxnZ zllw}pwcw$s84M@NxyG}_?vi;948RMhXh}h|>OCH(poeVhhDXVnGBr{(KG19f#d2oj zed`SF-pIQshPJ)DZ`*21T|vz9ew9>_YArrbZ`>o!blq3PZyX<{bHSe^I`PO_Db(UA zJ_wg-wbnN63!k&GusT1Zdc@z#UNrPju9GN|JCnO}v5Qev-hR}|9n>Qz&kZWH2Yo2L z?bwH_l8%zX>i&?tq=kse{P&Vv%;fz~rN5ghG<-GW85_OtG7s%| zYRg^@H3`ZH8}$nH1+%;4kmmpc@7BY7^r&1$(jWcB*jGCIDOPIlr+J(DgYQS)e;n6D zN(fuM!*@yvafnHOyU9Cfb(waI_no*5*p0OYEbVkTSIt4pCanfz)EPl<={%<&cdGVX zW-7qYM&#PeMy-8|NB?XjqJ;?BaXE*eZH3ZjJt9Myh9ZijQ@m;Il>O}iZIOc`3+<~W zBMge;ZJ0z*)#f6^WEk&&b*Io!-dLhMGt)EQ6ljbcuXmg0SJENWV2VGBDr?c~4I|#G z;_`kay`})0yl&xl6+=5k!pS}kr=&3bIY$>Z6Ycjl`h1G{GyFAj%g*cL0q8`Gu=V1*x8Q$&q~3Fe?d^4KGNyGYhx z^PiG&gNr)T!|N;#wAGSGzQ+G_49M-xJZWEuk$KooXCr<}LlIaU zEfHP1p1$3S8Av$|ByX*B{Oi*w3VC;GgcmBiitUJ-lW#`g`R2Vsn$KZ{gHuSP3<|Rw zo{fU(Lr(#bfU$PX(?uLWQQ_5=e$hL0=fr&b>pYGha|8uO)e-|LslQLkuk#hkU%LjM z`r^!F^Oc7Je%@Sgxn=xBZ*baNW}vrD&QS}yf%Y>AVubro&w`Yw z4knueq1?IR>`sS}YZd6Z{kjEHO;iXL1b3Vi)XkDXUp-?FuH1dzwy$usvEuPWAi*PP z^7FzU{g|{*r=4I5FLuAN=?`zzTy3r=9d>-i`41Q^7iQJKZG~7|Z=GB~dMT9*RFyl` zD_sHG`B1y`p^+45t5p6BD6?N+y^pakc5j&YssmUiQXlDE2-p?8!WElijt_}$;w;3a zh9T$!x`@mlchAOSW}ZP7~sIalu7n{REY5=PoLx@ zfIrj7roP$f`ZRZ&i_s7u3|%JrS|O;>Vvyoh^d_sBth5NhZ}A>lkC6n=Is)1lZB2e= zH39SNG;n$RO$^r7+Xjc4!7-AW%Ud{^{iozh4GfV|>v)Zr$@l4KPXW4RsK0uzeX?x& z)IjR5mL5P-hjTR@FkMT4U$+q&o2iWkF#MparC796DStv68VbAG$*%cQ{vAr&lfAKv zj^ZS9OMe$aao*=x#V?)1@k-{07q{uie^Fy8&j*;635zuSY!nqI1~pjo81O2PIvcguompI!^f$phQuy1WyOo{NksqU z7>mKebqbILmzgq%%_93GwR4K8UbABj{nB}p=RbPS+g9+86&Uy~l$>yaLKS<7%9=}2$W=4cQ=2PWl^_*eKM2w1?z%H$0 zaTR>ZjX6?SNLt>^Sl~dMUj|cMS`G^SFtfN z1cF;&9*DFX+7507xo<6Z`5O7tAp^3T(TJh=`sOjXq%ZTV2f;c?T&N#nzV>f(SCi!I z`Sw_Yf5lZW>@H4Hc$0Fm(uD>OK2O_1r)HE)b-z?Y2H=_JYeI^n|a8m#zEwlJ$Br|gqP`$Rbp}?}Ti@aWfgOp8M zf=Ni1AHHN#PPvU`#veZf!m}R4_~LdQaTlHaU4^YGrHwPp$4lyJch}Ng25hWrsa+Fx z&-;-RyD=((IY|#y_${9+?8vTLY{3PV_69(pKDAE3(_JP;D{MoC8pHOQMySb31x^jV zHxV3((?*ITn3+8+f+xqYMSC`cT$BC1%cwj+S@Om)08;~w>VB$p3_(!;1m#z9Dd{}mBf>m#N7SXsPp;+H?kOnh>7cx{nwF-ayZ&9wdpv8Vw7cax zNbA~cgc&9sqrHwH-BOjVYobP~Uq<0qAF~CVL8Hj;1-C71rh+VMD3!`PHR&884SgVGmgM4hT}K&8_{Lp>Zw4e=qS z0tD?Z)(UTR)=!@CdQjGnjr9}Ztp**qr!_&Ui{k>-2x?^p|uEE-Wi#f9tb;tOc1I~1V;tFHpAE-^Lvu-;u$ZuUDRkoUB&X0kXqsGuaz-Y_hBu_ z9v6T;t5555<1!2R&7%$R7QrkDMPejrJTZP@v1>nVRq>Z@ZiIBagbnQuKT5jE_4P=$ zYgxvp)-=nH&<}d%E6ol!!|Bt*&yB;^NmyN16%r@3Pv9woO!MEEX#sj3+shhN;Qb5s4!SpWrwxx*K*hWXBr{YDti@@9u{03Ll?fIt zBziUq*;4c7cfwKdH|l+_zlFq708U|!Grzm?=PZ zRo%uG1y{VHDnF=#Rr5yos2Y3d^NsTu>RX2;4RxWj_ZtSW6$PX1@#$(m=^-hBX;JV5 z%6i9P=b`*j-xXGIk^D@Ud=V(;Xm+AXlQwt>Yo314!`A+Ct7<>Zg5OJd9_=5w60>=} zFO&G$Xw0Jvhds8p#SV+{_TH^2A{44fJ_Ao8supcMwI^hip7i-VZGVd;xyS^UnXWFC7xr|`i zShzA^&2i}DMP0u;j%C~Zdg^v&#vapn8JdA$nF_3>J5b@h@!x`?gn=MA(DV%wj44wN zgTmg+&6dVPyd=E9q@@i_vv*+t*xy(yA4}zotWD|0fwwo_#kPOz#qdZT(M*w25(u9o z0Be5W;BY^N=8+IY{-JR^@C=iXi<_h57Tjg&K{ok5OBmgqAJ8^Gai-X3z4~YGjuIbz zTEl4zatV+p8zZLeEq}nl&M+ao-!YlDPy{|pWr_!Kz_vmN7DHf{N=2}LYxM5(l{s;# zVPL+1b7{VR|0;(qU10;;5-|8WWanlKhP_}$?(P7*b%C2+|AJZJ)CU(oXhY}?ZQpjH z3Ff%y9D`B=PChakvTCg_E@ndpG zfN8N23?|IqA_N_pXnZ5AV*GS}T6pWWIKK0JE4_zmRXWwIiQs+v2xoamIRjoP>_M~d zz`0llke9!K%v`Qtfli!gJ}6D+xA_D5YC8fC;hbxu1HGK=1z+Q`*6IJ$+W$Kqms2&K zT{MhVo_RT~y+%}sR@T45vb+)#jMvLXXoqWaUgbCHB=C*P_Z|4!X3^-_Ohqm3V}m`3b?CkkE>_K^6vO;)CG2n2^1I+}gEb$nKBD3%rX zAK&9W4i)6P%`u@D631ihvP*H`WAaE4nCYboju3jG#vRUV zPfF@_?9NSd`#wVjVwjk4m^)5s_aSG*-cS5fd3r*gSg;S32|lfF{6iHzRSM7Q>*Z2W zmq13ZJ=`@()I{BfAOiO@NAMjSg?ZQB!E%^@huPNFbBdI=#2-2pow9`W@^quxNY_o@ zoy8(3zLI`YfsVNRpO@F2>J>XzbZK+&%_jR@7B5)w;pEc zqaz74c4Zi}OS7(@+gNvfTr}cZlbW(;;D~XLlsis!IJ`24!)RsoI>|borW1p{GV5#j zDq$d?NQcnWpwcJFA$stp-b%yNGP+&mMMj4#MxB$LOA3~gr8YPq2VBV39OML@9_S>^ zXURBPY#2B2w0RJDB$U5+uiKqAvu^o1ZUNXlEYxnU_hG1%I4ZXh$n)6*y?qsb5NnxK zv}O3l7*&fg6Z7Sd@^xtvFja{t$ni-QV!gbs+CY3uA3GbXWo9Q43Xh6yM9u%Ln#q-v zfLwDwTgf(IUJKAemJzk!#}P+-$M?@tkYQerR!0bGE=G-G6GSudt8lIa$$N;G+JWCD z;iT3gtM;Z>ul4=?#`MnDZFkKo>IPS!>}$g?u&<(j7OP?f5|*(M>@puz@K;$4?LD9F zrxZ(b;Exov|0GqxRpV?|0S1HvgIDdBtC1tvwD@BKl%$p-4;>_s&{0}?WdfeaSDeux z=Ayeca0LbAA-5~QmU}_{#u^{T3^^2gN#-;Icw3Aya#fx>D5<`eJru4Zr@C`hqaNp+;{)G}b1${_Tz4Wrj{7g+UA3mdt^~ z;jQ|2i7_}5DuMMkDDi>~vhycLg8(<|=;3R6@eac5oN~BpkO62S^vAk*I8m_&hb0QF z<%#?jQO~&SkgFo8RKIiH01Xf@(mHX#a>q{ErQT=+mqE? z*Zia1MkC|n2uF+Ny+4QW^@tgLzs>mOVXm=k_On(WOVr?sD%Bgp{IL^pZPnYap$l8f zb06W)tWk`7H`nx9-+j0}PCYf(Uz7stw?WZ#&qAlP^kzO91<}z`Y106t3^eu{<5Z+&5VG;9JS1MhP3hlm?F*M{weu!3dCHkLZ za{&M1v%QZclTg*}J;8o#1k;a$mG>oC$koLqJ7-@?M#K6oj`C9hFTat%b$YqfJTlOG6l#j86R9vGG|jasWHWfL=Af!JlcL_{H*ch*R3m(FD>Ak>F~%o zAGkZSXiD9!ZGPaRsj&za9xFQLP04+P1E=Hw#yvb!9zh={8sSlJ(LUCsRWJ~Q>QP0J zC+6GG0$#X*WuI=4mV}oAgzQkJxVtNNxc#{7)A9ZKL-@{!}knj3L;2iHEAXIzwr^uZ<3tw|Wa9i*6 z{S1hL=@LkL`IiS4n$4-ej0L9Z%bYob#gSagMRLBBFQNffZWy+7(jsVa*GUWUI_iC0 z)b@2d@V1kHWPkr4vfWuxEs_c>+3hVZnyEW{6nnZFn%Y6CueGYZ=mtT%>Zmtk)X3+z zL_ko&Ks~JWkb0sth&Hs6<5aH%NN1!`c-r@u;;PP2_-A-6N`*IIymElSfv*230Xwgc zg1GM^b3O{;cqx;rEcufa-xWh{i@%gX71gs66JAZE@^3U?c3P7P#~n-uAt4eBfC}eR zPhJliAZU5&Q2DyT;v*$DVmSGf>r68f{}f31 zb5;wn#YM5UDGP^`vCFF5r!K2{k>!@SjBR*Q)%{3a0wFDb(H7yGNdocHE7t%hP8l%2~yep|F(#Y~C{?l_%M}D8XR*9OIdKy^KPy;w=-zjXAIK-%Qc(AD|Avx{b3@sq!|MIsSgL;;Yph zl|Y^m1&x5v8L64hC5C=ube${0gKHCxldMQX5zdQG01|8;_kM)?)v&Vx>gu+JHB}z z@3>pe$!)Lr02d|F8+6u%*(E;n27ErSKq#~BnlUW3ZY);6Uq`|K9V}HVWz_SBADI+N zXw3DBgVz8R{5(5w0F(=`P=P{!LsTO%>t3^k-Z|Q9$ea~Sh@&Tf5S>z#`Ht$cv+u_+ zv0*q+^W+1(gKVHhk|w{8PP+$FqSDvLs3>9?E}AuqXT^GZjjOCGpxtj7My~CTS>ZP9 zkzz(DeACmCu)bDATg$J60V>+(?&lE(Yp(U&&1kK1dK!!^Q<_1E9yv7poMTf!3jX&D z<3c)1U;TA3-EL&#e;M}Wglb_`N$n*Zq?!oGijC6`=Jp`j_U=PJX>F@;)H}9fDq*(8 z@@n{k?v~adY|0weNURR@wbUxi_sc6V%q)Gt1O3PV`CG!3b$M&d{^B5nfqA7EIZsYG z-$GcBkt`3(+VZrKDoQ1_QY^jgF(7qob1MUC!pqHzXOBv9Qn0%z|0OuPWD;Tklwhaz7rUrWqE6E&P^CE1i9?y-ua={nnSBUB=*+!4-9}#)dd+< z)0~vkTH3eKL5`Qp#Hue|sp6lwFjgTXGCFMZG$AKA5h&qrll5LwsIgi@@6OirtiC`W z!&ht4iVU7vE^&Q>5VTHY_M{9%`PMy<+u@k7XCWP^_2K*}}V3m1EE_ihAeINbwF7Z}b}W@$2OeT@Y8wd*h4 z@|D9nGt+L2FicE(!b^c2KReA)Sfw16jz9$5FjGz4(K?V<4-{{NW#7dVo+~UdEX4Ab zWd%ftxR6=8?q_JokvC$izELjDes)28-Y%=+2?+AJopt%%c@Y{-qfYJaqtw51^s+gw z6Q~?2)H>iH3z5YGwRR}-3kY43i&Eu@?mTKvZoy}+H6%E7MAHjg(aIxW+dO76@J8E* zIGFdTLbr5w!CZuF8Eu752nbt=1T+zpl_6Ufh=Lxd+eURxH0e{)AsA=!7iz^vy(Ngw zkd$wPc{Q~j@jnWqAFx{*cb(@un+8UUO%l&DGQ-~XP~=FmMQPU%%zE?bqW69!)_q!i zME~w?#@notKVr^LB>&Pym&99&pVi=7Ti>$UF?%iB^WHA%IcEvHET<@^2)9 z=be6KMyv_y*~~*eeUd<#lv2?ym~W#@QXHH| z{w!=O*54nk-t@Zp^tXl7V!4&GjajyL#~jncf1@5v>09{Xf?!;Aq?8R z-}KvS|2#!$<^*GZQyv_3j|&X_&~^F3n1|r=9zREo@S%G4Ly?CKwG7;f!NO0@QWWMg z0d#Ds;|)qvXc9C%hq{(U3J?~6=eFj6x~pqFvaq1k`Fe{}If0+y7S*p{2GI4;M&Z~c z+GFR;J4DLceQ=Aqca$nCCYGM!MG}F$A3%}Fm*^dy3$Tc{@5bX`*eUR8qgw=AjVb7A zG{{p3Mr`Utmx9ookh7}~UpB=C4ul)Te$<$y33{C{==Z`6v4R;svFTBc$o+g*`ybzvlYWeBkLA!cYM7iAU%_^?|w7`fU;A=TGv3 zpDMjhiw!6K&4~Dm%iyX~5#o$F_tYX|J93hb2$EnZ*{bHBhcVZJH7U-!Kj64;mtaGx zoU*-rCo!uq%H2(5;7=?Q)w=ODAMLYY$6`2y%W?vwiE}+OqG07g7$H1>Jm*9aTB%V zOYhVm$VkQ7JzWlS&^;bG<_HcIrtvh22bd!yK`}aPPv8n_EfD_0Qt}qqYqw#TMEq)9 zG+JjD2~o2tx!e1abn^#7aW=RGK`q$s`Anm?XfnAQ8TNT4d~|R?s>JM;kseR$4}97V7$PiN7+5$29c$Udz2{cwxS2COg*9jz$tx?Vz*dpd=vzIuIC6CU z=`^(k#;_38@P`3ohO}bV=dAP#(OAqb@9{XeiGb@a!O+`JFW59-x}RKsLTeiL$V(w@v&@ujX>1i+B0Cz{fgECb=0=T7S*Fzs~ z39r(?ccZe6HmN`X*$%&T6I$ls)v}v+$Svk$Dr2A|Mjnd=?+iQ|3}s0bSBHmjWdiFwM=tXN{^F?!``Jb+czWRW)t)1;HV#I*f{ zG0=l(h^k@-&oY~ciwKkbR%`XR?EG^6<>ErA5+)1+IzO#-*_er5am@dD{`9fN<0bIJ z(5a^lhVIyU-0I&yZANA*y2iIgI|I_n*bO((=eexfsi$&dbt{5`8Ie$3g57@L@6-(S zzOTvNSK59lY}kU>yj{`!n_+ZX#ktY_>s=StGI zkqLf`%DMLA?rG_uHGaTzY3|WSE1EX<@V6CO$_U5r9q)k{J_`UHIqR~4GhqY=y|@o> z$ou2_pq2h(jyw{Zii+VSD_37ay5#hx8j3+(sfNtpeM-<;XT}Xa3ZDGC>LF|EW$;{R zzPQ@1_dUc<#W5eMSHk6at<^3N6@)l&IQPQw#}`MJ71D-%%v6LR3`@RX#GO|_k#k*A zGO6zqBkB~9ca#Ns`}ag_ZFCLl@ta<70ueDoa$#O zu%vQusy&(;hd?CD=kp21r)TxY*sLk0@sq!*n8j=25&`AEXxD_Tg}x*i9jrrQmqUed0*7~&IVPo2h$npx$+R54dFB{DP{sU&WFRbiUa8H4!?WbYx zP7)||Y|_A5zIZu*w90duMA!s6_C1Vw9^-4L2vx88WYhRRO5dy}Z3NakO+=+GV7snF zt>8CD@|h#}BBM@LFWHf}5;pb0deb$%1T>ay_HlOO=F*`(B|xe^M5OIl*(&PNg9D3RQEGRjtR~0RK3c)OR~^TVGw>b`s3tcJzwt(H;c%+(^dqY~?eJO5Re3 z%iz*bM53QqT-9v#1VJY50-4e`4%zYoQ1+@6tP_RCW$4}M|*@k4t`;x7UR6+Mx( zKx{QwiT@X_g;-FGZPpLcoHSka;IU;h)t?m^3b2PSv|S-oz!uKaj7=YUoMEIO5dR2D ziT!n5@X~ARr7UR7mS9^o155rg_t`3)Om-XZwbe6ZZ0ErmU=HCURVY@Jk1yNUn4C%P%SqO`m99 z(S?~JLcmXP?G$1e4SWHm0dbpPl7oi7Oal7Lab~#DH^#5{1ckpsa(Fj2&RHT(LZ5_W z=aud%mt{xj4tGmg>u#TjFyfHn!=&YpOQ>L7Xw}LE?qAwQeiY!udBAhy{w;tDPPw@| zeJ+8}HRb$^5!EBQMBHAAvg&)Ad%MVt;$JG+T+1_L%FdGU5CM-p`W{o4Ii+%sT->Jk z+S(i>Mt@Xst3-c)?w;93sc`};Q~vn!Z2JU*MBuoDU0OCb*-COqro zBBL?XoWo_F&wgQk*fV!yg-3$y+su@z7+RUp#w4&N?) z9OHgtntKBZ9a)@^iMZLOCkE`hVq-nF(JW)uq^&G-V=IVt*>(&cq9PLe>`|GsTi}G0 zAQM|Q6TO&Cb4+(|WN;P>4!-Ndcr|%_{?U9ZiFtgl@d8`k%ZTV08d*O|i?PEYvI7Hs zg$uwRFu5TU@clF;82%TnOlM`ImEyCKncmMV#@%57I4{b~HVZz>y9Sxq7unG?g3;kA zrN6TyRr%^zxjA_PtEx1OXkmQO>?|wi*mJb*VM&J8KP!L?_gF!(f1ym&bQ@J;k4s1{ zN14%)`6x>d>sy^|p#;?OU6WZVPU?M~j6G&+Nt(yX0*3-X8i_Fp==XsOQWB`A z7Z}EjgtxUZ_|Bn}fPYE!!p2%NYsT|08oq0bOq5Th>!j(! zmGF?pt+r$B5KxqC6XswhaX6mdLScD-kqLv(^*nB=W#m8u=Xm*Jb~bM%LF?=ZH(7cF zJmE2Gq>n>{#lWh(Y_njB-m#-x?4#xH+J`){)qeD)Y=rb&2g3@>Q7_wOec=3WFjKCPj^j8)X1!JdPTI$ z1mNNq^UxxZKB?*ccJVKUg};01_n0bLX8m1R+g2>70UCgWOf))?lS%?3lY*zlwL9rD zN!~{NGr@zGQhh&TXaX+;1#KIl#2hMLr;t`ff=toz73oWW48t2r5}#j=*aTlkBDg*O zo%lt(_&6R&>mj9?X>Ouu(WZhWXjzjFoyR}e_69ebC;>bxtyXPjtz3){PeHx>xW zIr)t04EzW4ue=9;4b{P>pYl?MB#@c$nsIwBjkN=QUznjxYN>o6(ASo#{=Sv=y9z@B}&0< z313mxA?DT|7<_2ni-^{9Jw(S*By;m?`Ry)b{DY~h>~h>(Z;?<>G_-;qaB5ypHP2%s zuIJ&IwJo0vQuFgX>uUIEeTi;NLU944&B+p3$J+c+(wVkzR4Kv9tn z8^$84-FV=}%(B$JZK`BWP^d}AQ((*>p8GjSZeA#~kqr4_tNYI{*Rg=U$HKk^*rm^R zMyEh2$0kTwQ{fMJTQdqEPAno6|MEKO6;<0J$ui&=;Je-Z`9xT(<*TQ>Tcrjde4-uRfAnS$ZXW4Nhujd!(U^D znGF}R<}j0b^J%S5v@fA1{+Yo`8XcE}hgiID1fJl}C<+745V96EjMx3FYe6K&NQ>jE zt{+!(YFpa=gN}g5$Pnx7EpfptV|+tvIedy?o<2ip!99%QzYIsM9AsLF=RG0Tx8G+T z+pYX#y7*_NC@p4{@QkoS*@e__RXj%QL*o&@>bQN;fx!6&R{(-;uJ}_5g#XQV( zSp8g0F>WK7^H053 zyTO{3&ZJe}Z0>n3i`mkF=etuqzgDI9DtY#A>z+!U^3dQukl7GO>jp{Gc#3J*f-Ut`lN>V6@1H83vfqQQLID$bU5u9 zbwM#_!EnL)-#+ku_!s$LX-t1_Fw;I+Pv)Jbh0FCUdlYq3Y<^%Yo)-KvIH1U%#X(B0`m!7J@|T=l zLZhz8z?0D}t)YE&$UF52Hr3_O#-DyG_G z>4T&c*=O0N)03aap%jjbRkn2^N3FH--Kl8-j~D$ekQlLu?5IR@?k_+GgpeK(t8=L> z?T*<+?J7&Sf%|X|38f6#k2w}%WLSKBvQN5U8D zhA&u-RuJ%WB^e&MaXQr{zz{0GH{7!IH&4~8{7GL%J9i94G6Ev+_I_8*my;zlWQ1h_ z^iI+{7{~N=6i}}*IKBMQMLlh6l3*bSB*tQS`41bTllpFK*&^Dm*_^keaAk?^%eT`8|Sx$`=<4{kKmd5 zr6Xr?BIm75sLqAG0yrd!M(NIHi(IM%FFoFX*+Aa04K34fbj@z>WqYkx-nB8=KDSir zy!3=Iehz705ybZvY=j)OyfHA7PzL=-kxNnaSb2)hrqg}%MsPZ6>z9m6T zzY)UBTR8w}Z@FHYY1H7FJ%rJRM__ezZLdP8^hywPEWQWImD+DRHYx$z5xtiFv;WyW zh!O<$x3%v!Ubl~H?_gdaJP)IgQUd3x!>68h_oP3HOLvF*r~^5Z))mOd{gYZ_HnUf= zK#5xNh>!;_)>8R$t5@VQ)86a)8-GON8K?x_{5Dii(;U0jrcfRncTa>&kI5v6^e9*} zj>fbi9#rRW<#Vq@KUpV^|5jB5P9@=YtPLz)nRKq4HQtMU{rXk?1a$Co<+T9;eyWO{ zbkHFB10>Xyr3(K{Q=JF=N1a1LQ5eusiwOV$N&NTzZwmcCME(C!=;jX2CT0e<4i*j! z7FOmha{$35SkV55yVI^4-X&X^^!1HUGAlls;>?T;9SeqzrQ+4n?9|e6g4Pr5e@Y$b z6llbx1-P&&QDd$vE1|ME^755OVvKVxKapqOTW4c&ZHP-F?DiPzAikua;2H+N)2F$) z1e?E5KCBD^Q{&_BUmvUp|8>~EnOImvD3ZAG-o{_R>WMhxRJ93p1aBbOQ`emn%26WiT)&SZgk)_w+n682SIAOBs-(Ql11Ira26{FecUXIL**H@n$+^*c=QEr$W%cE{N#+PO8 zC(X6gp+(ROrot_%2!U)tu@veTMiZZOkv01umZAUJRi8>QTV#c=B3!Kwi!()rW*{cc zNkJtILA{}EYfAZtS)gva8()L6rL#thIRi@=y0C#Gg>(V7=EPL8I8SPkumzHM0I06s zec?DxKHZ3=Uc{h?x)ZmGkT;8zjD_*h9@OV{*yrTOHGym!-N{N1`_=2Pt^!Syh`3Y7 z*rQpc&R%}?r0E!5o8BGA;8eEyU-W6O>#}V-;Dh9pyGk$P{Or7i_R#D>qF3XW@|-&Z6;tnBYs?b6}n?Wx0$`f9aH*$VZ`> z^M`X`im#af`Be?|isd7wD)bet$ytFL>C*rvAMRbP7j-IY7O(mcv`yIMgq#zyXeOMW zd61=%O9vF1-shpeo{p+JFdcf`kc7#fk>c4>hJIvfCq=`GP7XKd&t1K|(eYkC4dK3L zo^?&9!f^a>61O*EX*8)1HE@PzP%GntBW^u zqzL7;4u-3EF!)w7q;yFj0I{2%Hsj&uB zWRu11#d5;M=LMkqtGXBlhU4uD@h=Rf3}VL;sB8 z$c-VBDaJfr^0IKxfD?0eR0D60&0B&imaKi3-`A0mRy(g(R=3gfhg|*rAUcI*vwZI0s&~(8v3@S4*YCzE? zmla&Vtbv+LsCxW6>X2ZZC@-rbrO^QZVI3Q^@k_6B^TX|^vWo9E%WD0cR?eZ#yu(Hd z>0qm?CL+2QEldMtEq5=F%Rj}!AtK$K?Tlk{;wP_(B<8&fc!SM?+zg!>*bsw8f4H3d zikm_XuEACZ5SCe!hJv+^@~an7`agDd*x{Ise#qo>9YPQa!9hF`K$^+>7fn)i5SwA$ z`R@?Y!tlYkBSTIz+A|h0m0P1)#Jp2|r^%(md6LCre0a!GecJ@R7Ih|kRf{i-fB5>Z z;RYxX1?<^<&mf_dWmPeGxo_bibz(5?uQZ+;>d^8Z2X-d+?`h^z1I)#RZTQ$N!=by^UNkA@Ys;lR|7H-n^2 zo}|rWv~6+>`aqn9&>gs_nR*T8b^h4xv1OV$RQIw*+o%*=se^NpnU_)YvaY$y;5=r0 z8HRSKEShzzO6rhi+Vt0=!Ezz-zc~U#Z~Em~!Y@H~gaevM-Z3`tF7y~Rsd#7*xQvN0 zU+dzMv}SK1kr&7O*Be~?`lpJ94eM(I+lhUM4PYV#)lTYaT!Hj$O zAZvFt8l-GK0*P_UMdig~Oo)Qj*D1P;SoLVYD20-rd4#PN0_*Xn-e%qVrMJnH_k67BrdPysB* zWd;*D%6tkx5?juKC;MDSM+-VeDthjfv?K!XIkd^CxbIX>kihkmYxd499VO%E)wM@p z*Gy`Ce6tA+Sh&8tdaT`S>@CJ~8;1Ax;cI7zh<4xP3nk4qu*SbIj1alWtwUkmEBvfC zUzo-oCMa=iYhUHK%Md2Z^zrvDpfKOR8v~{p?@Kg24 zj(Z_WKlQ(ChVP{c7%r=b*T3%yG_qx=SAiC=(u@19AzQ%^QC?OpnxQ%UX{Tn)5Jq0i zEzm;x1njv=F9D$?s)hw6uO^^D_Ts9-5-4&Jkj2mN-4g=VQfloMwDE-F6E_>%*Mia#x12Ire;)tNkO%1Qm@`+hX*Jz-zH26Kl$^&$BhaU=)`9A4(`s1o@)`I;&7+D0y)Fu&QYHfAR-7oI-IcUvWPlD*dO+qqd7O#YT>*T-tO4{|4 z_Y=c`Tm5+S2&JClHlFGLgUpvz!B}V(?Z?O&%3m5Vno)Xj8tKMmD-$%de~TJSY)_SG z-VteIh5ByQ#i7@u!lGoelq!+?zSs~!gEV>}5(*(0Q+84W?x00s2l-gNCl@l>wUp+G z9$X@uS~w|7K$`w#)SBx{Q3T@j zH3^!E%jp(>;Qz9-Y(RuXTol%#q71?cBVFA8Q&{*8XxYvtR;qTg*i;g(;{>fTh7!sp$oMP1#OIvEASexm{V zM3j)w_8m$5E?p@-X?p&waHF~0`+uX?yGyYJ-F!WK z0h-1&y?@7?eou>RnkRajWd-hfgcIkU(x!2>s?R_>$=0FKQ)%@Kf1#13l<9j04o;j~ z1-<$uJ%T_sP3-0w1hb=ORYf7e>YG zhVW9os8^*9CWq26cPsNXzmEKuhL&Z)e?WT|yyepTPR4P;+S8tNebFnp)$J}O@S^x! zeo4prySeYxX72B|3=Ya5yT|#Ay#V=u?&Yzt7!hfbhgFvCVwVBUa#p(u$FQ7w(0fVh0ZKd&m5_H7WZJ?oU18L!WC6W|JOEWe_3(0+`;B@60kEfH;p7tHFrY-GORz(xDhNc_zsn`6*|wE1{0$+}?frzl)3R%~YFW z{qA^qAhsyeOJMujcdC~$}3|qMY4zBS@KPAz}sz`e|Fl*&jtezmr)X&$eD8e->yV#(`RDRie#pL; zGzg!Lt^IYsER~(bpGuc_ov6D)(Z;b-*SdPFle&Ub*i4@U02W$+1J{K(mZc#iA>i$6 ze8Rs&z3+)G&f#fM0X#vc1hjq)RhM$5lX8*1>DxIC0B-4(G_nLquYTdZEhoMTld5eG zvD(aLWc z@V2Vqb7^vSNvuM@{3O44ty}ON2`-WM1;%KRg!VW3Nc#i}bk8H$_8xB;18~Ln)lr#R z_OnDP>BPYb=ds;2{HqkYLqp4Mv%w6y=vtW&cAKxn^oT>A|ny{y)tssnxWD3l_0vcOwjIX0{!vG!Ut%5VP*R|Lh+&GZ1yZ zeAFA71{OOXyPFtz9s2`F4*~S-eC6=aCMGH}B9+7|e+3awS2r4}4eA|D@GSFC|L6?z zejtSxpN5|fAq%8wN<{)?r25)}eRSg&SvTL2e2Utk5n}}duQJ}=vo+VV?-3PdZ`~F7l*5T&jE=7{-K_uEPZzOedT#vR-u(!HwXDOU@gX%wn1QlQx5qeTP-U? z=I=ymi@gB?y0jnMaI@Aic#3fe~Zw%ZjG;@lz)A=sj8 zF7J@!maQz4J`r~Ff^PBo$og*D4lYm=M9iELs;iO^IxJVe0LW2mfI>7eao2H`LZZcK zo9g+y<{_P*Z78Z%psch_P(b;sj?&}WxcqEb<%dy(=-^uRZwk?@mV$T#m8Ht4avw7O zYXI#ymJwB;osgxI{gW_xS_4eOZ?Rj;oEsmB%tjRD(R} z($W*9xgSInQcW%F!L3J9f@njv*{QX_WWi5%{SBYhb1QH`qo!99<77qJD4%cWJ*8GXoQlHumdj zTgUlBJ|FuCx+S#d&KEX}>CF+|-#4OO>7410*&f8GfP!WzoWo^hu(>EAHT|91II%S5 zCU?tUZ}a6&^PwvVfMi4oMC_MqY8VRGhrSk0VtjiggP;;kE$TD`o*n+&V9e^alGV## zwtL4#&k`=42=OHZIroNePXf^+DF($pD$OI6GRuJgHHV;|~ut{jXU|R*rgX|wbaj3$>yNhwB0H8(V(&UHq z&RQJxT`R|}uHPZT8o>jL|5QcD$%HhYCtqy~jw++6R7WenL#TLOPqXYET+1G_!*KCq zP;1&RgEj|#JAzqpMbjjZEr8Uaia3OD^y35{^`AOy@e+%HU;-@ve33CeF|XZA`;&1e ze5GgEy@xM*3(oci<>4(61To9G7;$2N_nb~mu7zI#OG!CX<)Z5tg1Igit?I{qUq@kS zq!~~8#=C-$h+(>FQ>ItpgwsNg<%~s>8r|fl^#PxXK;h&Y%!28dkP5T4`a7+s(*VVcJELr5T6U1de}CeYcEPGA$;v;^h?rYr)8f(HrOnK2IP? zHyFk$HdK}aFfuU28hM z`QbU#Ng+?6;u?5RWkRMfc21gLA{DRR(0%ive&qJ?(O7b1&rKTIrOU;iI z0Jk~;*_oot6D_M(=vP?3pLgx|ae>TF;);k=z9(={%k^qU`Kj_bPj4P!n2tyBO#`aw z7UM~B`1a<IK0zsGPk8@30vqx54yk>OyEsY?%p>|3_;h zgn-Nl#d9Ztq~bhS9aeM!S--B4r;ye*ISy=ki%7>w4%&uQhSk8IChcjr9iY>w?SdAq zKDF0ZWnVyVh{~~#PZupe+(xiYE@3u-GRNET&6o2yYBpJ4q%fXhYtbx8V2 zmxi|`0BY{5dQn29^1Oc0G#=ZTj=41vl=z4ps6&}xwcy#H>@5OYt~u3P9|#xVF3!H}DJ(5fCpcRT;o}UGlNkq_7qZEI<1DtQ_+iEs<{+op$Y&i(37gzKnvJzt_4JqfRGxRm%gIe|3uXewCB0v3}iz zUINR2$0)-Fq}c!OnD7VH=tEyoGhz2`ORHT`pA7YQjRGvKzpbb8Y23>>#u!CioM=6o zREgWpBM{X0OWAh{S-#dgS+%#V!To1DND2S4VX18lKz_t!expFG6QE4T(Pk(*<{UI| zub>$uQ==r9q|p9WH1yNznpgGcLX?4PWq8`kXZ|WRpIK3}PxJ~L#u6Q+%27y~21cLdD2ZLyZT+NwK8^g> zoYo{(D(N6V0(Gkfu=732WS6+%=5V%Ial-T54VJ+<>3qdHT*+O(N)>-QK0NPtlba#B zq^-;P;I;igK;W)+xwxCo1 zYnd4f+6{g6C@4t5TZ)Z^t+(&zftY{@)&kC@2AK=q>K7eAs^xnVh&MnvJrG8{33WzT zHHgs?816Jir;y|wTw+Q>LISQLj1%CVa;t>62_pm4qH3sYahE>jv8<@#l8ju@-u-dP z9*LJEa=F9iwI%S`q5N08%^;Ik))MHpgdpI2MDJH_Z)lgYNT|{^W+gunwWGv#SfUC!J%)Hh2I=NvQnE~mW)`DpDl)L zPi&9dy7EJKrWbm9zAM1Try9S;|D4zJ&Dk5l*}?-EgNVSp@8mUWV`=OAQa$p#iiXw7 zOnO+C{$t{9T}W-1M#&H6D0qX#f?IwKxB?_S$qQG21$sAlIo`W@NN(9(RH|B*cL5Bv z(W4Y|1-XUqK)N651=DlYJw}<1XgaPLa_sEvzC}gsU(HYu=z-F(XDw!?a$X7qX|xq! zR#M-N0v8Vd8%hi3MIL?^T5~qD)7z1{*b30|kbL`h%QPcZyS#AavR~upvpAe5f(n2N zh&uXsIxBEtB-0dzY<8eHfJ28N!@x3@Gw0c0(pnQxzpfgJKvPO>_0ea^l6&&0KH3^9 zczQL)8NjP$Jwob#dMZb;qiJix8lrGa>Fe$F|7f(~Mov#e4~mugTrv{ zAyTcmX`Y+m*FNc0bY(dHJQOc|csdJGTHy4_$;m08XXN3`iA0X`$>pm_8*lYPYM#=s zOXnPvh3j!^YS5~KJgC(B9JCsH1t}V|Bh5n(lGo3G)ms14bW{KGHqJzVXdRP*+lYgU zm3_kpBc&DVp0XI;q) z=R066jgyhYfv-Uf2L$2D0ptMlw_pa+zk~_{=V^BLuU}dqBFfsvL()~JX|ADkaHoZk zWvY;*WMsAMx|9rsSo)8l)GNzteU)UQ@W(jvU^i2BB)R5k*&+zX1 zND-VB*mS>0Z7)+I@1ASIYbaQe)%u2z@rNa?`f{Bc!;KeUSV+3o?(YL|(_eBC{m{Eq z_`7D|t@V3&1Z{1s?0ac!9&M(GoJ|rC_OgzNuGx#a$E3|B$|K%`;QT4$xc}4GP?UVC zvwDY}T*?|wWv;elKkLEO_WfjFIVUfv5|m#Mw{7oL0}S62%OpuqXn zHt832HyqW_1bO9`_2FHUAN^__mY+y?jiYT6`WEd5s-H`)C)2=rEGf6C&gZuJ;A-6lO9V?$v&f z#fv~|!_D!^r7dhs8)sP?_21f=0P;y|iNBGyR{D90(V5t4S(0M-`X28uHvncEKSf3~ z=BjiuQ9gXscV9|lf?y~(4gEh;%Xc$t)i?c;Dirg$yxOlf9I$D^b~cl>#*{Wv5!%-hbVRTOB?lDFRWpQn-g{@HV!Jv9i`?)*<)lolf7~4m1^4Xvzt@2TV9M(# zJfS@`heHwW8}+Ep+Ljh}(uq|BpvjrQ?rom_CKo3Do1v87Oie2yH8;dvJD?Q(YjVdq z;F2SV-A=|hZF7!Rlk@@BOmQ?2*n$GE{SGCG8^+6T5qY}|4;*o*cnCNd&L8M=!CJ9G zg-yZT6>v}!`i(ZNhG-y#yUm25FW>XFv!kbFdSpwZ9woP`BMhDKCcR;V-xc=0X&*s_ z7ePgNZ&ddGXxPH=z5$DKCY${3vRE;xWytH_UXT$aLj6D>Of4~@v;%KV&i5gDXx6SW z{Th}h#Ja_IFJ@&JW(4_FFxs&MJm_H+FQ*FBS zu|EYyVTG971eSAPe+Z3dWZ#VC9<0E^_An$yL^9~50~BHzdCr8{0`z8@F*s9#;tEy{ zbyVZgaEI|V-02V+#6}E}FL5|q4w*>XSwX|+bU+b1#sG^a5EKG_uWzPk1CH*P&=ZhL z-vNvQ79q|`tz8%1C~Qr!SOjmFPAF@n;o_c zyj8(L9p><&-+eek2X)8XZ1C_-)3$g1=({CgWX%)e4Y}$6F9zF=l0J%weDrA8Ld|;e z!9;n!con$<@dO(1Qclz!$-Aj}Fp;@6Uk_mS7BK9RRdwFAdG^F1UiSV76WBa^>0XgM z6k3)l*schU(aiBBQd{Gv?HDFoI}aP0dfyx#@nJPrRsgxSBcuWOSJcO9@_Ut@Yh)x* zU}XD6?U$o7itx2z2ZCW(M+s(j7a7RdQS}t(;a~mOm2{i6q$*G=I`7W1f03u+{C%bN zA?n_8e2v5P-_HnI3T4dEf)9xe1=>eBE75^mIMu&+_NY|aE+L}mkubl>IJSIcE}a)O zn8Qq#RcB78-=nDWscnNIR4^>n?FdIM+DFbSeB43>aR8kGaPG^_R*{qCaTl0IQV->+ z+bJ!;P;>12O=xL7&&!MpiwK&g3M5t4EuW#I8d%3Gi6NOatREn^=>YAe^9Uo4uE9C0 zs$W-GY1kz&jt`Lyom+W$F)!K}#n9<>1_jz8$UTa-74@XhbUn@_hquxMC`-C4QA;8I z8CRHlk4n9&5OTVP7f@E~S8(6S?;9=`25C0-ZwsyEYmpL5NM^nHn`NA|2F~gJ#W?_9 zXZi|g==-Oac<6T&JBtnfD7$&y#yX+Nv9vaj3B=N7Ej4U_747=&gHz7&m-HIorkBXidKhOfU0L2?6sdO3F0z?n0*v&m)rW)noYX*Ydj7AstXk~B#sQ+BlTg&5>-e?dC!NX` zR;8Tbo6&xER)PVDN3;UOVzgOtO3i}o&ydW2`rEjnb*T6z*I)$TMg%Zr#xHgaF9pKT zRm!2MMOjJuv8_rFQd$9}VQGYW(*_3EvH* z#HM*6*nAuy0(RwUnfnV8u~-6GWs-By-IM@N*2BHm5T&?jSrHrKdYZi+|6Xbp?P}uI z*;`&t!+8$K;uoQn!RVadAb4EPbiM^gA_|qb{i*z%vSCR2K?JqdqHv2)MP~9PB-mNn z?NFe+%dU* z!rC5z)S+7obHp-b^p)1tNTztJo)2xlcod51#s3NDJ|akx{6!;+TSgtucoJuoNt34c z{hmA2UaDAHff$soRREW+8(F$MB&Z5K0}v%s3HY!&^1SK0wkOmp7-BvwM}p8CY+8{e zMW*zHR^2K$55EixeEy~SgPQOY39|4UBGE%-v9P+-YugP`DBXhy{MxleR1KPa#QyiE zeuFtk@Uo_PzMApzh&B4CO|g*13Tnt_uJ@m0D7Clv{dl(JC^w5%gufZGqGO zCQBc)|2(}7>C@J`9`hR$RTduTEpIPJ`&Ba29QNh8ddv2yc!dS@J?-hpWhy`{Z-1(e zCTK@PI^!EkCbt^lCBHQAUqC)c9@T(D*xb$2OB+MWlJQqZjpPK&c_T}GJU(!d3iK=D zB)_RaBER4P!q*1_HcoEMx6u{P9}j#&sQ)_Gy8zcYAdwBAp`dp#o0*@^UCF=t< zgU7Yw)u*u1JAq6%COo=0GTrNr$Lko=!XYs(4GLk5>tue@RXieq z`3v2m>^u-W@Xfo&gR7vb)Mm^H|g&BVh6egGi-(nZbT@$_#eGkoRq6tT^ONrDeOJYCKF5C zYibiM;yv{ew`VO@o#`i%HC&X1bb=DPw?|C^1;n-|I~KQZ*I{w~f(y_2$E--EUlOwP z)30DzMIh#=@5m*g8plc%eHvyHUmY*S@o!cU0R`pT#Gg8P$C+sqix9DTFHNKGKU;0ZB! z{t9|?cT#j7Sy^G@Kx)Vx@*jk>WDTh})kY`sA6fBXY_z;kg|^g^R2l^~5B7bW{EbFC z^GV2JxpeXw9~Z*jDH4$_9L;KOW_nwkP=ez%NS%OYUCDZdjqfR{mxDD#0Y*{mi`l&A z{1?V`Vpv5Ez^uWqgY#2xc|>Jn*qPJ+^mQIkO>|q|PpF}XDxJ`~ARR39j#LFiM2ZL~ zy{Ujo?_EGTArukmARr(F2oQQldY9e_5L)=+z4!f|=eg_q-aYHgBw4f1%$k)s=QsZ| zd!I}d=h%IwnJm_2Rx8W81bI5%GJ@OXk#w=odS!vz4qOk9?-I_lanQ$s9RqM#O#G1D znAwCOwzBaMyZ3?16S2W?U4PR>a#}K$V^`XWClUR7c)i40&3EA|M1V0!!Dekf_>6yjT9nN@LA}wh`GR>JRV%SH;Mbv2Sa;=rtYX_uYZ+Dme z)67g|Nbz?Uy7+T!rd?O9ki{7q(8Q8v;x7wYs$_&U2d@&$b?x3e;m8W_{FlZe#QPBD zPaO*vl?N(y{Wp&>>Vt^JdsHfMBR#r~IjU9$J4!FpZ$bSAUKDUg*e0?*s~*1@hy^@rQ-cP4#5p5Y>I?XO;2 z*B0-6X|^fdbG9p-#zAYi+4?0)+Ov~B7a+`8{+Zzv3F7;dDoCaBp3Cuh4TA|$KMf||Af5}cF-M&WH7T@Ze-60(}Sk&&BvSEC_haS!mM zl-z_R^{|uu5zz)Yh6Co{3KD(eQ`T? z>~@*?`{4VIBH_e@-EGr#ReJJCRo7=+59xFI>gl0*A094!FfwvZcAvQTOoRjP5p2G? zno%Uxmd<`NJ-;MPcF~S!3@30I!_G`_lfMEp4E1ypX;Th+XdJ{!Um4j7uVgyXCUoT1^~6eLe%}kHaNzKelYiE8Pkc-Z z=G$|!8XewuB=l3b(=Shojotk1!@B_1$7!Z5c;fWWti9Zlw|?@rm-ihSc0Lvwh}0KB zzY2{w7BrU-@JWJEFqZrBXm2#-=lD;xOYI9&0#i7B!C@{is;Z;A2E|ekcQ8Q0rlLmH zy>h?tTrYLW%+ozMH|m5%wl(kc2fBLRt}+UdD-E5G(99R2I8yR&Z#!m!nHpR&uzy{b zO};(8)$pT!3}sa5Ka>pn1+RIVNdDmZouj>vNy6zYKJj|$^On!QxDp?hIj-jUlzJbT zIvZZU*2rLJo{5nI0+{_OFZNDihFoP6$jRQx=#VBWQiiVA71NH0%Y8C9Yf1D7w5T$- zLdqDH_GdMB?r#qJG}nxCVB=}SO~RAz6$CIp$5}njr=xfKT(MJ*UPH^%BI&i-Gk*AH zwYNWHiXj}+pOD~xFRJ#`eN1qbuL#w0=k^cej$3*k(J%58h-;W}a|_83&pS5a{WdeC zp}?!290CIISd&!EoOhY}Gn~rOG-F`jb(m3e*cs{R;L-Rbw6sUQ>nP3t4s-_8(#yRF zMdhINpW3!T?K{6%{c0lm#P=yY8h*N=t7Ek$x?(eX9pEAnphj6JM6L5e6)oa1R%XP$2L7Vjz$0zu8m-B1oPl9u*r3ya- zJ$1(9-6ED6dZuMnKR#_babPOm)ejGI%Fc61k`mpv3x=nqMeZ^7Xcfnx^PIY zb-;P+>$Vwo=PP~hGr>nYd8N8T6OkQXbe-lE&{03>2_7#Pm<`Xp;aVe8+E8Qn^nWsG z3}RkkjA8g7>pODi>5BF`F5~`uA*4!)m0#uKP565eLAb$BdVLUqb?2;iDyG+q5O=X- z|BjM7<#{uSvI-Z27x2jaWkO(|-Q*LH9KV1N3>J|-?2L4;i+4}qlk2k11a#{Vdz|UJM<|>S-9r)OO31zL$tM3TR01jxU$bk$D zBX&O#8bH5+E#I`<+rQgVma;^{z{E`$>vEzcQ{J3hJ>R5pWDfkO;*#F*iMBZeo&`Le|`149d!R*j@j~0oz7l7_cAyrKKrHM#8`x(0e_2| z?fs&e{ocVWk^V2PS>80wUJu;^-beOHb4~`Idf(^Qi~W%{l}bc`zdvNzY#eIlkdz-p zB1CRvC^XvQAp1_sm=fgHe{?5eI7gpApd)#<0q;4b%!QRz1r))c_QmdZBAHpTpM{ro zIHy*l#scr=Y|ee$@7i6IA1K(Tsw6}ly9TTM9P?207bvTKI2myahnj@Y=ia6Lqr-I7 ze1$A1Pd&q?*@azAe%963g??}-Xm%H$bH%up;b+Xq`sl>W^|oX#p|oHF^yc^6?@DVH zK-%@yrTxZpx0ZQnBY{U=W+y%u#iTCQUx|ts=Refb%e48;_9uVk6>I|QP*WuKInEp7 zRSZl;2pOAr-pQ!i;UR6Zsh!@ZwN&MJ>oQ4^)1t^>pzE7fMae>4dxNPN&lckiZTN~H zRFP}c+amoezbofk%NHi~mMe&zaDKD0EOVpkSA<>5+RG;ybYGM+ za64KxZpk&jWMlvQ`uQ(s6aKv;Q*ip+_UBLQ1v$OKPtE4qdBAG|yVpnELRos(Bi+YA z??!9#%5ecBxjzfpzAG!5<83yS&t`Qv>;sQfE;9vj7oa0Zw~brXT`mmmnU54wT_&XRP z7(4otY)Rkb2bMUZH-0DGhE$R5JO+GXkf}+Ha5;lFzi-LQJ%qtInw7c9+_*#icdUpA zelmIYt`4w;XJ`98M_1>8myJZ;rQ);`w~5Jj-87oRvwAKS=_Fe@^d;~sJAawHzw5qV zdtbKn2X!QYMKmLaj9VlMiarfPfy(qYS*;e1r5Cap4Zxpovki`nYYHX2FKGZX+W)|` zZMn5IxQyIIKlCS3s?<9C?b>a<=VIL^5XXrzmwIra`I&?LF%i*YP04-nyB%NB_^#-g zc{3Pn?z0xfvz}4>LXZZfiPu$M2(dA-;N2_j)4dg6MQz>e=rncKSOqJXc)f{$NF36_ zmmJq}qlE3^ZvA{mS4);ZaxmO-#Np4P>JwvaKU)2nr@-E8aL~_aHJ&F*l!tc2Wile@ z{Bns7Pdb@$_jr%S`BR}7FKi_s_wGVjlspqz`aeGV=`?e;?#z28_r9NJ=KZIN@qxQX+slsT z6R-OZ``p{Mu7k<7EDdeBcHWU#a632)Z-dXewkNm6`qR1=l$!~5crCsdFI=$r2q78N zVZIL|-xvnTheRh5I*CYCH4yh_PZ=Jhzk1S4QznSL`5`Bz&QOfz^t8HKBJm*xZaAVbdgR1@0T4gfRs!r22mniHb?Pmh34`xabO zo*Lp2%@YyZt-Y{Un>AYGAEM|M%4qriC9}jM{o&Upa;Niy!8#=y38MPOF;)k@WWn^X z8@4}TD2D=rU)00^fS1_v{|`f<{7)E)tIHc(ciR`%@BRd#09o;tE+h1H)yaq%iLpD7 zX=xT3TB4_4Nk_ z2NxC=z+iArPL8FeWn5gGtE+2ydHLY%f$Qht@{ZpMo^3fl7Wa=YT86jc%4QZeP5^49 zq7F?TVH1|oefs`gV8bfNvpS{HNbFH@y){)7AHAL4P1$zd766A368N>pmeVuY+}*!P z@=cS8G24}oXwdV>zy0{7r6pR7vCL#C*H!9A?J~`ja?Q{i_=mBdx(DO(#r+l<3 zPnPJawGU0C_93w~|JnYNHc*zjpjjg=8QVMwiP0e+@%&b$rj;~bU%Kx@A-_4}p37gF zFZ8vjxaI>Zmkk|EH<2eC! zL}6lhD72eIPqBl+!G<`Mp^u6NM)^Y`y~rw*r1N)4UfZL?29tb_^hFiv$aAH9YAIQR zp3spRzuYUb&;DOmq@p(hY5P!8iR*l}l%s_%V}MYT=L72^KxJzFeMQp3CRk?WLH4_D z94qdU3ueW){N&GcU%}$iXx23qv;z@N8qH|>F?Ad?F4x_5Eo@v(Oc+`)94!^UYKj4T zl~>prygRRW^byYxF%-F7_#ogcNh~B*>Z<&+v&@d zL?o9;%-DEZqgV>v?BX?!wFPb~Wv0Bok+bW~NaSNPW=^5rLa}#b%40m7M4&AHZ!{5d zP!a(eV;USm{~&r<2mj|5^-}4BW;C5hPM zI;kW6?(V3ES>gV<3F5>kbxu^TRko$aNxtMLn`_67fYQ(kFw+xF&{a|w;Q@1-Z_F07u} zGZNI1wpu8^DF|DhIpG4;PPdEd2EAd`bhvL&caeQ1g& zS>1=}pz6A1je5F>cj>spjb&L*jiC;ECQPnWul0(W6oSc{uOqF^8xuOqGMK+_PcK81 zNq*fK9Pp@{s(re9y-Yl8Uo*(o?+aqM9xulzUjtR9qy@?bq$RNT_dlkISEGUMa^e91 ztl08b3xii1C-Y~||1mP`2L~rR5l;|v{Q=0fS zH8VTAnJ6C_75%$6At^a|dUg)k-TOHvY-nhxwzl?XORKj5`@q0JLSkaLCTCMqQ*m+e z^78WBX!X$WNON;@XJ?noeb$mt*)-s)&Ce69BP5LrlzJgG&Bqj4Z~ot-oC!^iC=#2KOFp2&&1*KZ|7MMZ6G?S1_Non6SXvhwXL#qQp|y(-m>m^; zwA9zvcXV{Lv$KP3uYdpkJu))V+0hP%!+-snJUBeu-QDd(_H1lyY;JB=R#t9pZLO}Z zj*gBZk;saQ3KZ((;NSp(K&-8A93P{4`mpoBV2{(+-{0Te-FO5v{B3b* zY4!J7MP=pY*48f%DLA|p-r71?`(*%8FpuyI50BW{-5qKyY7DjQ8yMP}{x&%^jdTD?6ts=EK0phi~4#+b+_XpI_M8-kuq4T0}T?_w?+nv=+3&+t8Z>@ zwKkPZPEBo&Ko@);#Ky(#Zw$d=1C8}{uCK4L-6#7w9uNSy0SNuMXa3w13ZNUn4H2_O z1++B?M9rwOLO2-XdB?hMD2Q)_L!01%zJX0wblXR}zEnkJv6wV*3|I`&zJ(j444p(K zJ;}`=CAW27lNcUQ&^~me%ZK46LO_geFMUO;@*=XTl__y-nn<2!la(`$mn>$TXCzC54 zM+nDl|6El{)o_2fuql`_m@h#0#D1pa29*}9~P{yf`KzU+4_N@0xWySIt-FS z064%jKAA#xNWm4z@@0`EJb=*HH4hvsni$F$T~TEMIAUHC3ya@b2I3$yjY4jbXpDrj zTFnk?5I$QSTIP?^htxF%VMiK5e2<|h^#is zH2^=Z2BvWfVqT%Xw^fkP#qlN|8fdQuIMlj#HyRMm(m>5M%PmmUN#5W}>R_owl~#|S z@X?ROzk+fAqln8H4}-MP3LtufB_)D7ghK{IhPRm|YIc~Y^&3uTTpz}|2LTYLD)8bE z<8rh?qFM7{I54=fkavAM8x%@pexSsAGX^LFedh(orM42jxSc19nBl9B`mwf5WK2Nwarw?C{cZiY3*7r~)Qe%8b|XgFD< zE0GM8ke!|VGmBw$7%?c2L2g(LHwjoSh?fiCRfsaXYNe}#cM#(oD64}KGn7KCD69Y~ zEL+epYd{f!G5|nX;Sap3$yGIie`Y$ZAO`~4iKW{p^lQ03i^?px zt{vfs;V3+MIAC6skj1d_hWNW*pMnTD%{>48l z4PtZaQ=kw^AmaD4wDBxb;4-ZYV1Or(N>eHF?fDbPECv7tNdQDI<&3mE`G`RAN;1U| zAc;&M*lIFQ`w1|R0<=rTKp)JB76oyV8ZwqZf*JS3!frDMifrnbP9YY>2nqnSF3=eG z5R~9Po)V*iz6zyf5C~Y&hNEhaNbGa15)}fH%E+j28x95X_&mguj#uK{st8?I#n*}k z!~WjWxlseA<+~-9E4UPs)vN6YA7TtD9pKh?wwte`y~Yg0`C5K z)Vs|HVALhT!Wp*w1!Ac|9sd9^n*V^9m$Q|Zr@f22gOBZtfAJV4(cV9K?4SPnT%*TKwwI%|n&`QnU)mhA$S-Aq-?>gsNZ z>RF1(SR?>Fkhc}DfI$^w$>gwzHsLWImX|hcq&A_foi#NLd5Pt&fa^LM%;90L8B0e; z2Ns#ItaXt^aD>dhMzo*TI#gFxVG0452Hkx@@2cvp_%$c0bzD_HkQ+lQr@lqOuXt1Xb-bovEkT5q+x#2@3*S2 zMBTl}B|K@gUiFv4bU`o|$7mb#vj*zH@~}Hm>wxC{GIkzUrjMP~C+nxizT#da>d{sNC8c@zt~k zXALAr#hQod>X?iD`QBd45X(-A%Tk3tI^ZK__E}#tD`xra=E5-Mu=}j@-Pyp0@#k#2 zc@BH|Z?E$3b}AwkYokx9tx$EZQ7bFQySuBQvRC`-jp6iLD=VuZ_E){$n{gh$CMGar z61&K*vmDFq$w|z~#6gwj$$(|MIyq`=bSvf&I!b3H?fG6lV6RZ`dMo<`?pUfuewuHK zs;oFneTY6jUd@AH&W@KF8~%45Q=m*URzQL@g0iS4Ti)99)7| zJ}A-r0)gA+4#oFNL$%w9jh{H*OKRh3A5m~$!UMSSri`r@S!=_YqKSn&DYw+NDz&NrT5*5$JnnMgVz z2ycGi**_|2^a=sl3`hT!8WZVmm^Gb_CAa6ToK&mzIg5(@s1M?h~)dN%}RlEiSU zv|y5ZrzoR6AdLANDQ<|^TYlUS0av~|a9q||2#z9A+9%}?mnUb>{c-6g5yXP^Rk)mY zlK_$Afe5N;}ovC{# zNs95n(r;#Ydz?<(mWT>^F#TeFAy9!Hp5Qg%b{LF9!v6wJr?*Okm*qug1<0g|umUgz zt3e2Efh*Yw2!pE<0_iCMazkLiBQ0&viI-xSu7sCdG|9h9x}GH z^{t}w#w<*@7HOu=eE&M14QCfc9B@BBzUm=M^2mxd_yRtnNGLc|h08_JLmAHOJ)0ML zJ8byONMD)O>ye?h*byLdq9y*!gM@14=w4)T{4!;C6<1B+u?4Ob^mrA!_?NMbJR9 zdyAbxWDPPd*C$6Wf|v-TO!L~ni499pm57egO>)Kja?Ayb|Ep%t%^(V100O51r|`sS z`s){HXoH51n5@57Dj?^qK9~Etz7umwNa0$;g$EECcUgvJ5t|(Z()U>stQ#ffEcQ{@ zgRDSV3QE|=7uF?y3duq%3TLq36B#Hz*UODa@(q{;)j1Ie0`hI9FZt!7`n9_!J{-X<@2^fyS*W#?jWK%d~sgeDXY} z@fTMJvN4eA$H_ZDPEZk%!W99j))EbwiV`h^MCF-xz~^VAN)(J5VXsMzz?YT)d705k ziuMyacLo^a9jG?Zf&ZvWUOdEBGIMWM#!INGTIRbltxD0v&aXxASEPr~7Nbf7SP9mNoS zb2uq(y=vH2as+@6U{mr)+uwp8(Qn?*!qhpmPS<;U8GZhhg3fK$bhqgH}2U=Ud zQj60lwoPQQQQ%s9Lc-UlK{z9!ivJRf&w2tP(J#Umu#c~ABdr0&vIlCYz5;TXF6Ps1 z1ZyoEgz}FqQ)P843?nO|?6_jIZRkVPWCbeSM@nAl*+V1l&eO8m*3roYhwwRSI$02p&a8T8 zs7-x0nBVBUOJEtQ_|}RhuRhq4P}{T>$D77NW9Ko@Am}NW>rs5n4q4S=z|&PHJP~Ls zfs|{&fX``l)|?QTg=ip9VgmPnxJshnQiu-ARFwbVDweF+e{dDWf4J(6mAkWpv)#Xt z>JDh&>>m=>X_NKI4{*vl~{bx!h>LiWZ08)zBkxyV3XdGwz&?L|p!E zJgzdtqQ+^D<7Nn*kC*pe!kwD$-@Ehe_X;07+S%_GXrtP-P&GQu-=eYI`)Qlw>1=6h zTN_s6LA4v8f2A}OCS1%HV#Sri%m?W4#PN!#i>2br?T#M>L1Kb@XB|El%Vn3pt4_yr z&f2|C2ck|9FONze9pv3bqxSj2x1&a<_4hI-g9#_dz%_^4sAjFpt)|oA&sT@Tr=uCC zU4F-%0c%!#=)RCOXYupp%8TWK+1XiCp6+S0<8+4FUdkO4CBc1|^2P63tWtH-6LvaP zux2ZW4XryH_CS5NKCH1uPv`EWaGv(n9M!&D_m=)rLGOcuH)5Q~slP416qwzYNg(`)tmV&Y3&~cZRzu$Wg7nhlt z8LTW985$N65O|Duj};WBbJgqW>(L7}1zvYsD-&yT-VY8Aopryx+Woz~vxDl3K106Q z&lWvQRBcL&KB#lRT%oRy=Fq=VYs!nyXVZ5AWOtIdw!$F$1s``}1QuLXw%ynV3$ zN$ls>r2g`cSY_^fK4&wQ?zqiwD}e#k?lV;AcQzD!+H7^cREMfH=>C$9D%3kDH9eb% zJN)r_C&32QlD`>n>lhh&)*pD*?uMRB+6-4c?u$7agSW@}o{guY3iRReiYN8fXH%IM z>&?eK;b*^~jd}5?YTX|drC2d=1aV zgO&f(!hW_$g_xEWsRB4!5?OAw{%FtVTE&$S_*v9yEnVT0YG0~&3e8Y(SscB66DxL_ zFRma`h9e>VXFX$v^tx8*T91i!(nj$02)qMDi}k}*l65sSRBB76s_jP;@u~SJIdDu2 zBLcx3;Y23#3S)9SF~JKh{Y%(JyYTW-%m^MU=(^{nTAu;S`WqpexDumzqTj@Q+_`E39yZb&(FV&j*@h zAg`q&sUhpGiRp|~JPr#3?=LA=Gh;n(*eD_tGc^-InGB|9dvUCbk*QBli<+-5(Z|~# zUz9zGW9=rbN)-rJfM|gTGO2;gUwj|W5w$aw;2D3c)mB8Ewps|gxat-93QMVwTc z_XE}1LE?)`2$1PZSscO=&+dQ}Qvug7AwG$rFiO`&0t1$hJAoZc_`k)oWTXN7A( z7bHxms*5Q~j2I6L9?^`$Rc&n-5aq4Vy(?5L1S7}(3B*Zxy^X7FgbFD;=-DRS;e>1| zxq-3wFn;<&qI{I?tfG&|+DYzA;wnq(UF_5-$g4qdm>xFewN2UQJX6A^fa3~uvZ&qx z#AmO%>QZN~mY&VO8@w@!6LB1RnUT(~#SftTmBbETj`jfet}yqt*8^mc)**Mu#!?FH z3}Ke)Kw~K6VgpvKFp4J>|Db{A?QAYw%C1dOT%|D;7D3&)!wH}jOVWkd>tIvff_TpiNY7QWYqJRB?%ydbhH}8DrdaaSCtWi!kRB0uP*iT z9JEtdT%mroXfna!@>`0Z-!66Ft0x59I)I zP|^263GSe?I2{jy=u2n`I8DKtNM*9C_+VlaWE*X>7DdIYTvBc;R@&Q^_Xz`n84(bm z2KpdO;cBV!ri~iGp#*#K;1Px5ZGr%;F$JOIa2U-OzK*BCOObU@7@c5i3SS$y?Nix! zpicluCHyzoRws|~7qU|-B|+FWxR;+)A7nreza6EICsS1r{@~&;&Yv_Na>-ZrfB-~@ zP5K9V6o8A(k4o}P28hq2*{x-El$e_NLSpfR)NaR;GFsn+^8`kwoO+a0q*EYZ^0mkE zMJo$r=rfU_TclyU72y~F$oR@2z`__n4JR!W4b$wiyr{yl2LwFuhT}8i0dXv3@%eT9 zak*SuX`_&ZQgz+^&OYK9<_b6rt_qQ41%W2!{3-$?ArRd97w7+14S&ywi9P<`Oac2^ z!JiubnZ5hJ&*1N`GW^M#`In*1{_X!?Sv3E#al}FxBLGc-2A)i9|7*~2>H*8_bTJxuKz8^{k?`isQLGQ#2?iB%i8k)UBlnN XQ(qgO;Li?3*hL%%0FW&G^X>ltkFXA| literal 0 HcmV?d00001 diff --git a/src/staticresources/SiteSamples.resource-meta.xml b/src/staticresources/SiteSamples.resource-meta.xml new file mode 100644 index 00000000..1ed30e1a --- /dev/null +++ b/src/staticresources/SiteSamples.resource-meta.xml @@ -0,0 +1,6 @@ + + + Public + application/zip + Static resource for sites sample pages + From 83ee2d207b0911b068b9e980b8af39010c36d042 Mon Sep 17 00:00:00 2001 From: Fernando Rodriguez Date: Mon, 8 Apr 2013 12:51:04 -0300 Subject: [PATCH 3/4] Open Opportunity Artifacts --- src/classes/OpenOpportunitiesBatch.cls | 40 + .../OpenOpportunitiesBatch.cls-meta.xml | 5 + .../OpenOpportunitiesNeedUpdateScheduler.cls | 16 + ...ortunitiesNeedUpdateScheduler.cls-meta.xml | 5 + src/classes/OpenOpportunitiesScheduler.cls | 16 + .../OpenOpportunitiesScheduler.cls-meta.xml | 5 + src/classes/OpenOpportunityEmailUtils.cls | 274 +++++ .../OpenOpportunityEmailUtils.cls-meta.xml | 5 + ...penOpportunityFieldSelectionController.cls | 142 +++ ...unityFieldSelectionController.cls-meta.xml | 5 + .../OpenOpportunityFieldsUIController.cls | 27 + ...OpportunityFieldsUIController.cls-meta.xml | 5 + src/classes/OpenOpportunityListData.cls | 18 + .../OpenOpportunityListData.cls-meta.xml | 5 + src/classes/OpenOpportunityMailer.cls | 113 ++ .../OpenOpportunityMailer.cls-meta.xml | 5 + .../OpenOpportunityNeedUpdateBatch.cls | 41 + ...penOpportunityNeedUpdateBatch.cls-meta.xml | 5 + .../OpenOpportunityReportController.cls | 202 ++++ ...enOpportunityReportController.cls-meta.xml | 5 + .../OpenOpportunityReportUIController.cls | 118 ++ ...OpportunityReportUIController.cls-meta.xml | 5 + src/classes/OpenOpportunityTest.cls | 135 +++ src/classes/OpenOpportunityTest.cls-meta.xml | 5 + src/classes/OpenOpportunityUtils.cls | 23 + src/classes/OpenOpportunityUtils.cls-meta.xml | 5 + .../OpenOpportunityReportTable.component | 23 + src/objects/Open_Opportunity_Fields__c.object | 36 + .../Open_Opportunity_Settings__c.object | 46 + src/objects/Opportunity.object | 1048 +++++++++++++++++ src/package.xml | 52 +- src/pages/OpenOpportunityFieldSelection.page | 53 + src/pages/OpenOpportunityReportLayout.page | 65 + src/tabs/Report_Settings.tab | 7 + .../OpenOpportunityUpdateTrigger.trigger | 9 + ...nOpportunityUpdateTrigger.trigger-meta.xml | 5 + 36 files changed, 2573 insertions(+), 1 deletion(-) create mode 100644 src/classes/OpenOpportunitiesBatch.cls create mode 100644 src/classes/OpenOpportunitiesBatch.cls-meta.xml create mode 100644 src/classes/OpenOpportunitiesNeedUpdateScheduler.cls create mode 100644 src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml create mode 100644 src/classes/OpenOpportunitiesScheduler.cls create mode 100644 src/classes/OpenOpportunitiesScheduler.cls-meta.xml create mode 100644 src/classes/OpenOpportunityEmailUtils.cls create mode 100644 src/classes/OpenOpportunityEmailUtils.cls-meta.xml create mode 100644 src/classes/OpenOpportunityFieldSelectionController.cls create mode 100644 src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml create mode 100644 src/classes/OpenOpportunityFieldsUIController.cls create mode 100644 src/classes/OpenOpportunityFieldsUIController.cls-meta.xml create mode 100644 src/classes/OpenOpportunityListData.cls create mode 100644 src/classes/OpenOpportunityListData.cls-meta.xml create mode 100644 src/classes/OpenOpportunityMailer.cls create mode 100644 src/classes/OpenOpportunityMailer.cls-meta.xml create mode 100644 src/classes/OpenOpportunityNeedUpdateBatch.cls create mode 100644 src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml create mode 100644 src/classes/OpenOpportunityReportController.cls create mode 100644 src/classes/OpenOpportunityReportController.cls-meta.xml create mode 100644 src/classes/OpenOpportunityReportUIController.cls create mode 100644 src/classes/OpenOpportunityReportUIController.cls-meta.xml create mode 100644 src/classes/OpenOpportunityTest.cls create mode 100644 src/classes/OpenOpportunityTest.cls-meta.xml create mode 100644 src/classes/OpenOpportunityUtils.cls create mode 100644 src/classes/OpenOpportunityUtils.cls-meta.xml create mode 100644 src/components/OpenOpportunityReportTable.component create mode 100644 src/objects/Open_Opportunity_Fields__c.object create mode 100644 src/objects/Open_Opportunity_Settings__c.object create mode 100644 src/objects/Opportunity.object create mode 100644 src/pages/OpenOpportunityFieldSelection.page create mode 100644 src/pages/OpenOpportunityReportLayout.page create mode 100644 src/tabs/Report_Settings.tab create mode 100644 src/triggers/OpenOpportunityUpdateTrigger.trigger create mode 100644 src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml diff --git a/src/classes/OpenOpportunitiesBatch.cls b/src/classes/OpenOpportunitiesBatch.cls new file mode 100644 index 00000000..c1507241 --- /dev/null +++ b/src/classes/OpenOpportunitiesBatch.cls @@ -0,0 +1,40 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +global class OpenOpportunitiesBatch implements Database.Batchable { + + public OpenOpportunitiesBatch(){} + + global Iterable start(Database.BatchableContext bc) { + + Opportunity [] opportunities = new Opportunity [] {}; + + opportunities = [SELECT + Id, + OwnerId + FROM Opportunity + WHERE Isclosed = false]; + + Set usersId = new Set(); + + for(Opportunity opp :opportunities) { + usersId.add(opp.OwnerId); + } + + User[] users = [SELECT Id, Email, Name FROM User WHERE Id IN :usersId]; + return users; + } + + global void execute(Database.BatchableContext bc, User[] scope) { + + User user = scope[0]; + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(user.Id); + + OpenOpportunityMailer.sendOpenOpportunitiesBatchReport(user, new String[]{user.Email}, stagedOpportunities); + } + + global void finish(Database.BatchableContext bc){} +} \ No newline at end of file diff --git a/src/classes/OpenOpportunitiesBatch.cls-meta.xml b/src/classes/OpenOpportunitiesBatch.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunitiesBatch.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls new file mode 100644 index 00000000..073cf441 --- /dev/null +++ b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls @@ -0,0 +1,16 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/22/2012 + * + */ +global class OpenOpportunitiesNeedUpdateScheduler implements Schedulable { + + public OpenOpportunitiesNeedUpdateScheduler() {} + + global void execute(SchedulableContext sc) { + + Database.executeBatch(new OpenOpportunityNeedUpdateBatch(), 1); + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunitiesScheduler.cls b/src/classes/OpenOpportunitiesScheduler.cls new file mode 100644 index 00000000..28f3101e --- /dev/null +++ b/src/classes/OpenOpportunitiesScheduler.cls @@ -0,0 +1,16 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +global class OpenOpportunitiesScheduler implements Schedulable { + + public OpenOpportunitiesScheduler() {} + + global void execute(SchedulableContext sc) { + + Database.executeBatch(new OpenOpportunitiesBatch(), 1); + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunitiesScheduler.cls-meta.xml b/src/classes/OpenOpportunitiesScheduler.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunitiesScheduler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityEmailUtils.cls b/src/classes/OpenOpportunityEmailUtils.cls new file mode 100644 index 00000000..e8995939 --- /dev/null +++ b/src/classes/OpenOpportunityEmailUtils.cls @@ -0,0 +1,274 @@ +/************************************************** +Class Name: OpenOpportunityEmailUtils +Class Description: Utiliy class which creates the HTML content to be displayed on the email / VF page. +Author: Fernando Rodriguez (frodriguez@adooxen.com) +Modified By: Fernando +Update Date: 2013-03-04 +Additional Comments: This class has comments on the code in order to help future changes +**************************************************/ +public with sharing class OpenOpportunityEmailUtils { + + + private static String[] earlyStages = new String[] {'Stage 1 - Connect','Stage 2 - Talking','Stage 5 - Submitted'}; + + + /************************************************** + Comments: Static variables that represents the diferent containers for the email / VF page HTML components + **************************************************/ + private static String ENVELOPE = '
[TITLE][CONTAINER]
'; + private static String TITLE = '

Open Opportunities

'; + private static String SUB_TITLE = ''; + private static String CONTAINER = '
[SUB_CONTAINER]
'; + + private static final String FOGBUGZ_LINK = 'http://manage.dimagi.com/default.asp?'; + + /************************************************** + Method Name: buildEmailContent + Method Comments: Method call from the Weekly / Daily Schedule flow to build the HTML Content + **************************************************/ + public static String buildEmailContent(Map stagedOpportunities, Boolean hasComments, Map stageComments) { + + String result = ENVELOPE; + String content = ''; + + List sortedStages = new List(stagedOpportunities.keySet()); + sortedStages.sort(); + + for(String stageName :sortedStages) { + + String stageTable = '' + SUB_TITLE.replace('[SUB_TITLE]', stageName); + + /************************************************** + Comments: for each stage we call buildEmailStageTable + **************************************************/ + stageTable += ''; + + /************************************************** + Comments: for each stage we call getEmailStageComments (if there are no comments, the input text is generated anyway) + **************************************************/ + stageTable += ''; + + stageTable += '
' + buildEmailStageTable(stageName, stagedOpportunities.get(stageName)) + '
' + getEmailStageComments(stageComments.get(stageName)) + '
'; + stageTable = stageTable.replace('null', ''); + content += stageTable; + } + + result = result.replace('[TITLE]', TITLE); + return result.replace('[CONTAINER]', CONTAINER.replace('[SUB_CONTAINER]', content)); + } + + /************************************************** + Method Name: buildEmailStageTable + Method Comments: Method which returns the content of a HTML table for a Stage + **************************************************/ + public static String buildEmailStageTable(String stageName, Opportunity[] opportunities) { + + String result = '[THEADER][TBODY]
'; + result = result.replace('[THEADER]', getEmailStageTableHeader()); + + String tbody = ''; + + Integer daysNotUpdatedLimit = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c.intValue() + : 30; + + Integer daysNotUpdatedLimitEarlyStages = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c.intValue() + : 10; + + Set earlyStagesSet = new Set(earlyStages); + + for(Opportunity opp :opportunities) { + tbody += getEmailStageTableRow(opp, daysNotUpdatedLimit, daysNotUpdatedLimitEarlyStages, earlyStagesSet); + } + result = result.replace('[TBODY]', tbody).replace('null', ''); + return result; + } + + /************************************************** + Method Name: getEmailStageTableHeader + Method Comments: Returns the header of the stage tables, depending on which columns where selected + **************************************************/ + private static String getEmailStageTableHeader() { + + final String LEFT_STYLE = 'style="background:#f2f3f3;text-align:left"'; + final String RIGHT_STYLE = 'style="background:#f2f3f3;text-align:right"'; + + String result = ''; + + /************************************************** + Comments: Fetches the columns from the Custom Settings + **************************************************/ + Open_Opportunity_Fields__c[] selectedFields = OpenOpportunityReportController.getOpportunityFields(); + + if (!selectedFields.isEmpty()) { + + result += 'Opportunity Name'; + for(Open_Opportunity_Fields__c selectedField :selectedFields) { + + result += '' + selectedField.Label__c + ''; + } + } + else { + + result += 'Opportunity Name' + + 'Stage Duration' + + 'Fogbugz Ticket Number' + + 'Fogbugz Assigned To' + + 'Probability (%)' + + 'Amount' + + 'Account Name' + + 'Business Unit Owner' + + 'Days not Updated'; + } + result += ''; + + + return result.replace('[LEFT_STYLE]', LEFT_STYLE).replace('[RIGHT_STYLE]', RIGHT_STYLE); + } + + /************************************************** + Method Name: getEmailStageTableRow + Method Comments: method called from buildEmailStageTable. For each opportunity this method is called. Renders the status of the opportunity and its data. + **************************************************/ + private static String getEmailStageTableRow(Opportunity opp, Integer daysNotUpdatedLimit, Integer daysNotUpdatedLimitEarlyStages, Set earlyStagesSet) { + + final String LEFT_STYLE = 'style="border-width:0 0 1px 0;vertical-align:middle;padding:4px 2px 4px 5px;border-bottom:1px solid #e3deb8;"'; + final String STYLE = 'style="border-width:0 0 1px 0;vertical-align:middle;padding:4px 2px 4px 5px;border-bottom:1px solid #e3deb8;text-align:right"'; + String rowStart = ''; + + /************************************************** + Comments: Filtering process in order to define the style of the Opportunity Row (red, yellow, etc) + **************************************************/ + if (opp.AccountId == null) { + rowStart = ''; + } + else { + rowStart = ''; + } + + // added by Nick - checking if it is a long wait after submission + + if (opp.StageName.contains('Submitted') && opp.long_wait__c) { + rowStart = rowStart; + } + else { + // end added by Nick + + if (earlyStagesSet.contains(opp.StageName)) { + rowStart = opp.Total_Days_Not_Updated__c > daysNotUpdatedLimitEarlyStages + ? '' + : rowStart; + } + else { + rowStart = opp.Total_Days_Not_Updated__c > daysNotUpdatedLimit + ? '' + : rowStart; + } + } + + String href = URL.getSalesforceBaseUrl().toExternalForm() + '/' + String.valueOf(opp.Id); + + Open_Opportunity_Fields__c[] selectedFields = OpenOpportunityReportController.getOpportunityFields(); + String result = rowStart; + + if (!selectedFields.isEmpty()) { + + /************************************************** + Comments: This loop goes over the Opportunity fields (based on the selected columns) and + formats the different fields (datetime, float, link, etc) + **************************************************/ + result += '
' + opp.Name + ''; + for(Open_Opportunity_Fields__c selectedField :selectedFields) { + + try { + + String fieldType = selectedField.Type__c; + + String fieldValue = ''; + if (selectedField.Name.equals('Owner.Name')) { + fieldValue = opp.Owner.Name; + } + else { + fieldValue = String.valueOf(opp.get(selectedField.Name)); + } + + if (fieldType.equalsIgnoreCase('Date')) { + fieldValue = opp.get(selectedField.Name) != null + ? Date.valueOf(opp.get(selectedField.Name)).format() + : ''; + } + else if (fieldType.equalsIgnoreCase('DateTime')) { + fieldValue = opp.get(selectedField.Name) != null + ? Datetime.valueOf(opp.get(selectedField.Name)).format('MM/dd/yyyy HH:mm a') + : ''; + } + else if (fieldType.equalsIgnoreCase('Currency')) { + + fieldValue = ''; + if (opp.get(selectedField.Name) != null) { + + List args = new String[]{'0','number','###,###,##0.00'}; + Decimal currencyValue = (Decimal) opp.get(selectedField.Name); + fieldValue = '$' + String.format(currencyValue.format(), args); + } + } + + if (selectedField.Name.equals('Fogbugz_Ticket_Number__c')) { + result += '' + fieldValue + ''; + } + else { + result += '' + fieldValue + ''; + } + } + catch (Exception e) {} + } + } + else { + /************************************************** + Comments: When the selectedFields list is empty, that means we need to display + the default columns, which are already defined below. + We don;t need to make a custom format treatment as above, as we already know which columns are selected. + **************************************************/ + String amountValue = ''; + if (opp.Amount != null) { + List args = new String[]{'0','number','###,###,##0.00'}; + amountValue = '$' + String.format(opp.Amount.format(), args); + } + + result += '' + opp.Name + '' + + '' + opp.Stage_Duration__c + '' + + '' + opp.Fogbugz_Ticket_Number__c + '' + + '' + opp.Fogbugz_Assigned_To__c + '' + + '' + opp.Fogbugz_Probability__c + '' + + '' + amountValue + '' + + '' + opp.Account.Name + '' + + '' + opp.Business_Unit_Owner__r.Name + '' + + '' + opp.Total_Days_Not_Updated__c + ''; + } + + result += ''; + + return result.replace('[STYLE]', STYLE).replace('[LEFT_STYLE]', LEFT_STYLE); + } + + /************************************************** + Method Name: getEmailStageComments + Method Comments: This method is called from buildEmailContent method, in order to add to the email comments written on the UI. + **************************************************/ + private static String getEmailStageComments(String comments) { + + String headerRow = ''; + + String dataComment = (comments != null && comments.trim().length() > 0) ? comments : ' '; + + String dataRow = '' + + dataComment + + ''; + + return '' + headerRow + dataRow + '
'; + } + + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityEmailUtils.cls-meta.xml b/src/classes/OpenOpportunityEmailUtils.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityEmailUtils.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityFieldSelectionController.cls b/src/classes/OpenOpportunityFieldSelectionController.cls new file mode 100644 index 00000000..4ef6b61b --- /dev/null +++ b/src/classes/OpenOpportunityFieldSelectionController.cls @@ -0,0 +1,142 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/16/2012 + * + */ +public with sharing class OpenOpportunityFieldSelectionController { + + public Selectoption[] availableFields {get; set;} + public String selectedValue {get; set;} + public Integer orderCount {get; set;} + + public OpenOpportunityFieldSelectionController() { + + init(); + } + + public void addColumn() { + + if (selectedValue != null && selectedValue != '') { + insertNewColumn(); + removeSelectedValue(); + } + else { + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'Please select a field')); + } + } + + public void clearCurrentSelection() { + + Map result = Open_Opportunity_Fields__c.getAll(); + delete result.values(); + + init(); + } + + public Open_Opportunity_Fields__c[] getCurrentSelection() { + + Open_Opportunity_Fields__c[] result = new Open_Opportunity_Fields__c[] {}; + + result = [SELECT + Id, + Name, + Label__c, + Order__c + FROM Open_Opportunity_Fields__c + Order By Order__c]; + + return result; + } + + private void init() { + + availableFields = new Selectoption[] {}; + orderCount = 0; + Set alreadySelectedColumns = getCurrentSelectionNames(); + Map fieldList = getOpportunitySchemaFields(); + + String[] fieldIterator = new String[] {}; + fieldIterator.addAll(fieldList.keySet()); + fieldIterator.add('Opportunity Owner'); + fieldIterator.sort(); + + for (String fieldName :fieldIterator) { + + if (!alreadySelectedColumns.contains(fieldName)) { + + if (fieldName.equals('Opportunity Owner')) { + availableFields.add(new Selectoption('Owner.Name', 'Opportunity Owner')); + } + else { + Schema.Describefieldresult fieldResult = fieldList.get(fieldName); + availableFields.add(new Selectoption(fieldResult.getName(), fieldResult.getLabel())); + } + } + } + } + + private void insertNewColumn() { + + Open_Opportunity_Fields__c newColumn = new Open_Opportunity_Fields__c(); + + if (selectedValue.equals('Owner.Name')) { + newColumn.Name = 'Owner.Name'; + newColumn.Label__c = 'Opportunity Owner'; + newColumn.Type__c = 'String'; + } + else { + Map opportunitySchemaFields = getOpportunitySchemaFields(); + Schema.Describefieldresult fieldDescribe = opportunitySchemaFields.get(selectedValue); + + newColumn.Name = fieldDescribe.getName(); + newColumn.Label__c = fieldDescribe.getLabel(); + newColumn.Type__c = fieldDescribe.getType().name(); + } + orderCount ++; + newColumn.Order__c = orderCount; + + try { + insert newColumn; + } + catch(Exception e) {} + } + + private void removeSelectedValue() { + + for (Integer i = 0; i < availableFields.size(); i++) { + + if (availableFields[i].getValue().equals(selectedValue)) { + + availableFields.remove(i); + break; + } + } + } + + private Set getCurrentSelectionNames() { + + Map result = Open_Opportunity_Fields__c.getAll(); + orderCount = result.size(); + return result != null ? result.keySet() : new Set (); + } + + private Map getOpportunitySchemaFields() { + + Map fieldsSchema = Schema.SObjectType.Opportunity.fields.getMap(); + Map result = new Map(); + + for (String fieldName :fieldsSchema.keySet()) { + + Schema.SObjectField fieldSchema = fieldsSchema.get(fieldName); + Schema.Describefieldresult fieldDescribe = fieldSchema.getDescribe(); + + if (fieldDescribe.getName() != 'Name') { + result.put(fieldDescribe.getName(), fieldDescribe); + } + } + + return result; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml b/src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityFieldsUIController.cls b/src/classes/OpenOpportunityFieldsUIController.cls new file mode 100644 index 00000000..1879135d --- /dev/null +++ b/src/classes/OpenOpportunityFieldsUIController.cls @@ -0,0 +1,27 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/16/2012 + * + */ +public with sharing class OpenOpportunityFieldsUIController { + + public OpenOpportunityListData stageContainer; + public String stageName {get; set;} + public String htmlTable {get; set;} + + + public OpenOpportunityFieldsUIController() {} + + + public void setStageContainer(OpenOpportunityListData value) { + + stageContainer = value; + htmlTable = OpenOpportunityEmailUtils.buildEmailStageTable(stageName, stageContainer.opportunities); + } + + public OpenOpportunityListData getStageContainer() { + + return stageContainer; + } +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityFieldsUIController.cls-meta.xml b/src/classes/OpenOpportunityFieldsUIController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityFieldsUIController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityListData.cls b/src/classes/OpenOpportunityListData.cls new file mode 100644 index 00000000..a7eb8fc9 --- /dev/null +++ b/src/classes/OpenOpportunityListData.cls @@ -0,0 +1,18 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/16/2012 + * + */ + public with sharing class OpenOpportunityListData { + + public String stageName {get; set;} + public Opportunity[] opportunities {get; set;} + + public OpenOpportunityListData(String stageName, Opportunity[] opportunities) { + + this.opportunities = opportunities; + this.stageName = stageName; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityListData.cls-meta.xml b/src/classes/OpenOpportunityListData.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityListData.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityMailer.cls b/src/classes/OpenOpportunityMailer.cls new file mode 100644 index 00000000..85447cc9 --- /dev/null +++ b/src/classes/OpenOpportunityMailer.cls @@ -0,0 +1,113 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +public class OpenOpportunityMailer { + + private static final String EMAIL_SUBJECT = ' | | Biz Dev Report Out'; + private static final String RED_EMAIL_SUBJECT = 'Your Overdue Opportunities'; + private static final String[] earlyStages = new String[] {'Stage 1 - Connect','Stage 2 - Talking', 'Stage 5 - Submitted'}; + + public static void sendOpenOpportunitiesBatchReport(User user, String[] recipients, Map stagedOpportunities) { + + Map stageComments = new Map(); + for (String stageComment :stagedOpportunities.keySet()) { + stageComments.put(stageComment, ''); + } + + String content = OpenOpportunityEmailUtils.buildEmailContent(stagedOpportunities, false, stageComments); + + sendEmail(content, recipients, EMAIL_SUBJECT, user.Name); + } + + public static void sendOpenOpportunitiesSingleReport(User[] users, String[] recipients, Map stageComments) { + + Id[] usersId = new Id[] {}; + String usersNameSubject = ' - '; + for (User user :users) { + usersId.add(user.Id); + usersNameSubject += user.Name + ' - '; + } + + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(usersId); + + String content = OpenOpportunityEmailUtils.buildEmailContent(stagedOpportunities, true, stageComments); + + sendEmail(content, recipients, EMAIL_SUBJECT, usersNameSubject); + } + + public static void sendRedOpenOpportunitiesBatchReport(User user, String[] recipients, Map stagedOpportunities) { + + Map redStagedOpportunities = new Map(); + + Integer daysNotUpdatedLimit = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c.intValue() + : 30; + + Integer daysNotUpdatedLimitEarlyStages = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c.intValue() + : 10; + + Set earlyStagesSet = new Set(earlyStages); + + // Remove fresh opportunities + for (String stageName :stagedOpportunities.keySet()) { + + Opportunity[] opportunities = new Opportunity[] {}; + + for(Opportunity opportunity :stagedOpportunities.get(stageName)) { + + if (earlyStagesSet.contains(opportunity.StageName)) { + + if (opportunity.Total_Days_Not_Updated__c > daysNotUpdatedLimitEarlyStages) { + opportunities.add(opportunity); + } + } + else { + + if (opportunity.Total_Days_Not_Updated__c > daysNotUpdatedLimit) { + opportunities.add(opportunity); + } + } + } + + if (!opportunities.isEmpty()) { + redStagedOpportunities.put(stageName, opportunities); + } + } + + // Only send Mail if there are opportunities + if (!redStagedOpportunities.isEmpty()) { + + Map stageComments = new Map(); + for (String stageComment :redStagedOpportunities.keySet()) { + stageComments.put(stageComment, ''); + } + + String content = OpenOpportunityEmailUtils.buildEmailContent(redStagedOpportunities, false, stageComments); + + sendEmail(content, recipients, RED_EMAIL_SUBJECT, user.Name); + } + } + + private static void sendEmail(String content, String[] recipients, String subjectTemplate, String userName) { + + String subject = subjectTemplate.replace('', userName).replace('', Date.today().format()); + + OrgWideEmailAddress wideAddress = OpenOpportunityReportController.getOrganizationWideAddressMail(); + + Messaging.Singleemailmessage mail = new Messaging.Singleemailmessage(); + + if (wideAddress != null) { + mail.setOrgWideEmailAddressId(wideAddress.Id); + } + + mail.setHtmlBody(content); + mail.setSubject(subject); + mail.setToAddresses(recipients); + Messaging.sendEmail(new Messaging.Email[] {mail}); + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityMailer.cls-meta.xml b/src/classes/OpenOpportunityMailer.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityMailer.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityNeedUpdateBatch.cls b/src/classes/OpenOpportunityNeedUpdateBatch.cls new file mode 100644 index 00000000..0fcdfa16 --- /dev/null +++ b/src/classes/OpenOpportunityNeedUpdateBatch.cls @@ -0,0 +1,41 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/22/2012 + * + */ +global class OpenOpportunityNeedUpdateBatch implements Database.Batchable { + + public OpenOpportunityNeedUpdateBatch(){} + + global Iterable start(Database.BatchableContext bc) { + + Opportunity [] opportunities = new Opportunity [] {}; + + opportunities = [SELECT + Id, + OwnerId + FROM Opportunity + WHERE Isclosed = false]; + + Set usersId = new Set(); + + for(Opportunity opp :opportunities) { + usersId.add(opp.OwnerId); + } + + User[] users = [SELECT Id, Email, Name FROM User WHERE Id IN :usersId]; + return users; + } + + global void execute(Database.BatchableContext bc, User[] scope) { + + User user = scope[0]; + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(user.Id); + + OpenOpportunityMailer.sendRedOpenOpportunitiesBatchReport(user, new String[]{user.Email}, stagedOpportunities); + } + + global void finish(Database.BatchableContext bc){} + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml b/src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityReportController.cls b/src/classes/OpenOpportunityReportController.cls new file mode 100644 index 00000000..d05654df --- /dev/null +++ b/src/classes/OpenOpportunityReportController.cls @@ -0,0 +1,202 @@ +/************************************************** +Class Name: OpenOpportunityReportController +Class Description: Opportunity Expert Controller +Author: Fernando Rodriguez (frodriguez@adooxen.com) +Modified By: Fernando +Update Date: 2013-03-04 +Additional Comments: This controller performs all Opportunity Related queries for all components on the OpenOpportunites Module. + It also fetches the org wide email as well as the opportunity column fields from the Custom Settings. +**************************************************/ +public class OpenOpportunityReportController { + + private static OpenOpportunityReportController instance = null; + private static Open_Opportunity_Fields__c[] selectedColumnFields = null; + + private OpenOpportunityReportController() {} + + public static OpenOpportunityReportController getInstance() { + + if (instance == null) { + + instance = new OpenOpportunityReportController(); + } + return instance; + } + + public Opportunity[] getOpenOpportunitiesByUser(Id[] usersId) { + + String[] stages = new String[] {'Stage 1 - Connect','Stage 2 - Talking','Stage 3 - Prospect','Stage 4 - Proposal Development','Stage 5 - Submitted','Stage 6 - In Negotiations'}; + + Opportunity[] result = new Opportunity[] {}; + + /************************************************** + Comments: Filled in with default values + **************************************************/ + String queryValues = 'Id,Name,StageName,CreatedDate,Amount,AccountId,Total_Days_Not_Updated__c,Stage_Name_Updated_Date__c,Stage_Duration__c,Business_Unit_Owner__c,' + + 'Business_Unit_Owner__r.Name,Account.Name,OwnerId,Owner.Name,Fogbugz_Link__c,Fogbugz_Probability__c,Fogbugz_Days_Not_Updated__c,' + + 'Fogbugz_Ticket_Number__c,Fogbugz_Assigned_To__c,Fogbugz_Last_Updated_Date__c,long_wait__c'; + // THIS STRING MUST NOT END WITH A COMMA. + + Open_Opportunity_Fields__c[] selectedFields = getOpportunityFields(); + + + /************************************************** + Comments: WHEN CUSTOM COLUMNS ARE SELECTED, I ADD THOSE COLUMNS TO THE QUERY STRING + **************************************************/ + if (!selectedFields.isEmpty()) { + queryValues = 'Name,'; + for(Open_Opportunity_Fields__c selectedField :selectedFields) { + + if (selectedField.Name != 'Name') { + queryValues += selectedField.Name + ','; + } + } + + // THERE ARE SOME VALUES THAT NEED TO BE ADDED TO THE QUERY EVEN IF THEY WERE NOT SELECTED. + queryValues+='Business_Unit_Owner__r.Name,Account.Name'; + + if (!queryValues.contains('AccountId')) { + queryValues += ',AccountId'; + } + if (!queryValues.contains('Owner.Name')) { + queryValues += ',Owner.Name'; + } + if (!queryValues.contains('Total_Days_Not_Updated__c')) { + queryValues += ',Total_Days_Not_Updated__c'; + } + if (!queryValues.contains('StageName')) { + queryValues += ',StageName'; + } + if(!queryValues.contains('long_wait__c')) { + queryValues += ',long_wait__c'; + } + + /************************************************** + Comments: IF WE WANT TO ADD AN EXTRA FIELD THAT MUST BE USED AS A CONDITION PUT IT HERE. + + Expected Format: + + if (!queryValues.contains('FIELD_NAME__c')) { + queryValues += ',FIELD_NAME__c'; + } + + **************************************************/ + } + + /************************************************** + Comments: Perform the query on Opportunities based on the fields selected. + **************************************************/ + String sql = 'SELECT ' + queryValues + ' FROM Opportunity WHERE IsClosed = false AND OwnerId IN :usersId AND StageName IN :stages'; + + result = Database.query(sql); + return result; + } + + + public Map getOpenOpportunitiesOrderByStage(Id userId) { + + return getOpenOpportunitiesOrderByStage(new Id[] {userId}); + } + + public Map getOpenOpportunitiesOrderByStage(Id[] usersId) { + + Map result = new Map(); + Opportunity[] opportunities = getOpenOpportunitiesByUser(usersId); + + for(Opportunity opportunity :opportunities) { + + String stageName = opportunity.StageName; + + if (result.containsKey(stageName)) { + + result.get(stageName).add(opportunity); + } + else { + + result.put(stageName, new Opportunity[] {opportunity}); + } + } + + return result; + } + + public void updateStageDate(Id[] usersId) { + + Opportunity[] result = new Opportunity[] {}; + + result = [SELECT + StageName, + CreatedDate, + Stage_Name_Updated_Date__c, + + (SELECT StageName, CreatedDate FROM OpportunityHistories) + + FROM Opportunity + WHERE IsClosed = false + AND Stage_Name_Updated_Date__c = null + AND OwnerId IN :usersId]; + + Opportunity[] opportunities = new Opportunity[] {}; + + for (Opportunity opp :result) { + + if (opp.Stage_Name_Updated_Date__c == null) { + + opp.Stage_Name_Updated_Date__c = Date.valueOf(opp.CreatedDate); + for (OpportunityHistory oh :opp.OpportunityHistories) { + + String ohStageName = oh.StageName; + Date createdDate = Date.valueOf(oh.CreatedDate); + + if (ohStageName != opp.StageName) { + + opp.Stage_Name_Updated_Date__c = createdDate; + break; + } + } + opportunities.add(opp); + } + } + + if (!opportunities.isEmpty()) { + update opportunities; + } + + } + + public static Open_Opportunity_Fields__c[] getOpportunityFields() { + + if (selectedColumnFields == null) { + + selectedColumnFields = new Open_Opportunity_Fields__c[] {}; + selectedColumnFields = [SELECT + Id, + Name, + Label__c, + Type__c, + Order__c + FROM Open_Opportunity_Fields__c + Order By Order__c]; + } + + return selectedColumnFields; + } + + public static OrgWideEmailAddress getOrganizationWideAddressMail() { + + OrgWideEmailAddress[] addresses = new OrgWideEmailAddress[] {}; + final String DIMAGI_WIDE_ADDRESS_NAME = 'Dimagi Salesforce'; + + addresses = [SELECT Id, + Address, + DisplayName + FROM OrgWideEmailAddress + WHERE DisplayName = :DIMAGI_WIDE_ADDRESS_NAME]; + + if (!addresses.isEmpty()) { + return addresses[0]; + } + return null; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityReportController.cls-meta.xml b/src/classes/OpenOpportunityReportController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityReportController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityReportUIController.cls b/src/classes/OpenOpportunityReportUIController.cls new file mode 100644 index 00000000..9b38eca8 --- /dev/null +++ b/src/classes/OpenOpportunityReportUIController.cls @@ -0,0 +1,118 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +public class OpenOpportunityReportUIController { + + public String[] selectedUsers {get;set;} + public OpenOpportunityListData[] stagedListData {get; set;} + public Map stagedComments {get;set;} + public String recipients {get;set;} + public Id userId {get;set;} + + private static final String DEFAULT_EMAIL = Open_Opportunity_Settings__c.getOrgDefaults() != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Default_Email_Recipient__c + : 'bizdev@dimagi.com'; + + public OpenOpportunityReportUIController() { + + userId = (Apexpages.currentPage().getParameters().containsKey('uid')) + ? Apexpages.currentPage().getParameters().get('uid') + : Userinfo.getUserId(); + + selectedUsers = new String[] {userId}; + stagedComments = new Map(); + stagedListData = new OpenOpportunityListData[] {}; + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(userId); + recipients = DEFAULT_EMAIL; + + if (!stagedOpportunities.isEmpty()) { + + String[] stageList = new String[] {}; + stageList.addAll(stagedOpportunities.keySet()); + stageList.sort(); + + for (String stageName :stageList) { + stagedComments.put(stageName, ''); + stagedListData.add(new OpenOpportunityListData(stageName, stagedOpportunities.get(stageName))); + } + } + } + + public void reload() { + + try { + + Id[] usersId = selectedUsers; + + if (usersId != null && !usersId.isEmpty()) { + + stagedComments = new Map(); + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(usersId); + stagedListData = new OpenOpportunityListData[] {}; + + OpenOpportunityReportController.getInstance().updateStageDate(usersId); + + if (!stagedOpportunities.isEmpty()) { + stagedListData = new OpenOpportunityListData[] {}; + + String[] stageList = new String[] {}; + stageList.addAll(stagedOpportunities.keySet()); + stageList.sort(); + + for (String stageName :stageList) { + stagedComments.put(stageName, ''); + stagedListData.add(new OpenOpportunityListData(stageName, stagedOpportunities.get(stageName))); + } + } + } + } + catch (Exception e) { + + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'An error ocurred. Please refresh the Report')); + } + } + + public void initAction() { + + Id userId = (Apexpages.currentPage().getParameters().containsKey('uid')) + ? Apexpages.currentPage().getParameters().get('uid') + : Userinfo.getUserId(); + + OpenOpportunityReportController.getInstance().updateStageDate(new Id[] {userId}); + } + + public void sendEmail() { + + try { + Id[] usersId = selectedUsers; + User[] users = [SELECT Id, Email, Name FROM User WHERE Id IN :usersId]; + + if (!users.isEmpty()) { + String[] mails = (recipients != null && recipients.trim().length() > 0) ? recipients.split(',') : new String[] {}; + OpenOpportunityMailer.sendOpenOpportunitiesSingleReport(users, mails, stagedComments); + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.CONFIRM, 'Mail Sent Success')); + } + else { + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'Cannot Send Mail. Please select one or more users')); + } + } + catch (Exception e) { + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'Cannot Send Mail. Please check message fields')); + } + } + + public Selectoption[] getUsers() { + + User[] users = [SELECT Id, Name FROM User ORDER BY Name]; + Selectoption[] result = new Selectoption[] {}; + + for (User user :users) { + result.add(new Selectoption(user.Id, user.Name)); + } + + return result; + } +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityReportUIController.cls-meta.xml b/src/classes/OpenOpportunityReportUIController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityReportUIController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityTest.cls b/src/classes/OpenOpportunityTest.cls new file mode 100644 index 00000000..b8a7b61d --- /dev/null +++ b/src/classes/OpenOpportunityTest.cls @@ -0,0 +1,135 @@ +/************************************************** +Class Name: OpenOpportunityTest +Class Description: Class for Open Opportunities Testing and Coverage +Author: Fernando Rodriguez (frodriguez@adooxen.com) +Modified By: Fernando Rodriguez +Update Date: 2013-03-04 +Additional Comments: +**************************************************/ +@isTest +public class OpenOpportunityTest { + + public static final String TEST_EMAIL = 'frodriguez@adooxen.com'; + + static testMethod void testEmailReportSuccess() { + + createOpportunity(); + Test.startTest(); + OpenOpportunityReportUIController controller = new OpenOpportunityReportUIController(); + controller.getUsers(); + controller.reload(); + for (String stageComment :controller.stagedComments.keySet()) { + controller.stagedComments.put(stageComment, 'Test Comment'); + } + controller.initAction(); + controller.sendEmail(); + Test.stopTest(); + } + + /* + static testMethod void testEmailReportSuccessWithFields() { + + addCustomColumns(); + createOpportunity(); + Test.startTest(); + OpenOpportunityReportUIController controller = new OpenOpportunityReportUIController(); + + for (String stageComment :controller.stagedComments.keySet()) { + controller.stagedComments.put(stageComment, 'Test Comment'); + } + controller.initAction(); + controller.sendEmail(); + Test.stopTest(); + } + */ + + static testMethod void testEmailReportFailure() { + + createOpportunity(); + Test.startTest(); + OpenOpportunityReportUIController controller = new OpenOpportunityReportUIController(); + controller.recipients += 'this is not an address'; + controller.sendEmail(); + Test.stopTest(); + } + + static testMethod void testBatchEmailReport() { + + createOpportunity(); + Test.startTest(); + Database.executeBatch(new OpenOpportunitiesBatch()); + Test.stopTest(); + } + + static testMethod void testBatchRedEmailReport() { + + createOpportunity(); + Test.startTest(); + Database.executeBatch(new OpenOpportunityNeedUpdateBatch()); + Test.stopTest(); + } + + static testMethod void testOpportunityStageDuration() { + + Test.startTest(); + Id opportunityId = createOpportunity(); + Opportunity opp = [SELECT StageName FROM Opportunity WHERE Id = :opportunityId]; + opp.StageName = 'Stage 2 - Talking'; + update opp; + Test.stopTest(); + } + + private static void addCustomColumns() { + + Open_Opportunity_Fields__c column = new Open_Opportunity_Fields__c(); + column.Name = 'CreatedDate'; + column.Label__c = 'Created Date'; + column.Type__c = 'DateTime'; + column.Order__c = 1; + insert column; + + column = new Open_Opportunity_Fields__c(); + column.Name = 'CloseDate'; + column.Label__c = 'Close Date'; + column.Type__c = 'Date'; + column.Order__c = 2; + insert column; + } + + + private static Id createOpportunity() { + + Country__c country = new Country__c(); + country.Name = 'Test'; + insert country; + + Account account = new Account(); + account.Name = 'Test Account'; + account.Office_Type__c = 'Country Office'; + account.Country__c = country.Id; + insert account; + + // Create 2 Opportunities for this user and account; + Id opportunityId = createOpportunity(account.Id); + createOpportunity(account.Id); + + return opportunityId; + } + + private static Id createOpportunity(Id accountId) { + + Opportunity opportunity = new Opportunity(); + opportunity.Name = 'Test Opportunity'; + opportunity.Amount = 5000; + opportunity.Fogbugz_Assigned_To__c = 'Test Assignee'; + opportunity.Fogbugz_Ticket_Number__c = '12345'; + opportunity.Fogbugz_Last_Updated_Date__c = Date.today(); + opportunity.StageName = 'Stage 1 - Connect'; + opportunity.CloseDate = Date.today(); + opportunity.AccountId = accountId; + insert opportunity; + + return opportunity.Id; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityTest.cls-meta.xml b/src/classes/OpenOpportunityTest.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityUtils.cls b/src/classes/OpenOpportunityUtils.cls new file mode 100644 index 00000000..fef68294 --- /dev/null +++ b/src/classes/OpenOpportunityUtils.cls @@ -0,0 +1,23 @@ +public with sharing class OpenOpportunityUtils { + + public static void updateStageName(Opportunity[] opportunities) { + + for (Opportunity opportunity :opportunities) { + + opportunity.Stage_Name_Updated_Date__c = Date.today(); + } + } + + public static void updateStageName(Opportunity[] opportunities, Map oldOpportunities) { + + for (Opportunity opportunity :opportunities) { + + Opportunity oldOpportunity = oldOpportunities.get(opportunity.Id); + + if (oldOpportunity.StageName != opportunity.StageName) { + opportunity.Stage_Name_Updated_Date__c = Date.today(); + } + } + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityUtils.cls-meta.xml b/src/classes/OpenOpportunityUtils.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityUtils.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/components/OpenOpportunityReportTable.component b/src/components/OpenOpportunityReportTable.component new file mode 100644 index 00000000..c7130069 --- /dev/null +++ b/src/components/OpenOpportunityReportTable.component @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/objects/Open_Opportunity_Fields__c.object b/src/objects/Open_Opportunity_Fields__c.object new file mode 100644 index 00000000..bbe97224 --- /dev/null +++ b/src/objects/Open_Opportunity_Fields__c.object @@ -0,0 +1,36 @@ + + + List + Public + false + false + + Label__c + false + + 100 + false + Text + false + + + Order__c + false + + 3 + false + 0 + Number + true + + + Type__c + false + + 50 + false + Text + false + + + diff --git a/src/objects/Open_Opportunity_Settings__c.object b/src/objects/Open_Opportunity_Settings__c.object new file mode 100644 index 00000000..c692b514 --- /dev/null +++ b/src/objects/Open_Opportunity_Settings__c.object @@ -0,0 +1,46 @@ + + + Hierarchy + Public + Open Opportunity Settings + false + false + + Days_Not_Updated_Limit_Early_Stages__c + 10 + If Days not Updated field is greater than this value, the row will be colored red. + false + If Days not Updated field is greater than this value, the row will be colored red. +Applies for Stages 1& 2 + + 3 + false + 0 + Number + false + + + Days_Not_Updated_Limit__c + 30 + If Days not Updated field is greater than this value, the row will be colored red. + false + If Days not Updated field is greater than this value, the row will be colored red. +Applies for Stages 3 + + + 3 + false + 0 + Number + false + + + Default_Email_Recipient__c + Default Email Recipient + false + + true + Email + false + + + diff --git a/src/objects/Opportunity.object b/src/objects/Opportunity.object new file mode 100644 index 00000000..476f7e95 --- /dev/null +++ b/src/objects/Opportunity.object @@ -0,0 +1,1048 @@ + + + true + + Account_Name__c + false + Account.Name + + false + Text + false + + + Area__c + false + + + + Health + true + + + Poverty and Economic Development + false + + + Natural Resource Management + false + + + Training & Education Services + false + + + Logistics + false + + + Other + false + + false + + false + MultiselectPicklist + 6 + + + Business_Unit_Owner_Name__c + false + Business_Unit_Owner__r.FirstName + ' ' + Business_Unit_Owner__r.LastName + BlankAsZero + + false + Text + false + + + Business_Unit_Owner__c + SetNull + false + Who owns the Biz Dev process for this business unit. + + User + Opportunities + false + false + Lookup + + + Country_Name__c + false + Country__r.Name + + false + Text + false + + + Country__c + Restrict + false + What country is this opportunity taking place. + + Country__c + Opportunities + Opportunities + false + false + Lookup + + + Deadline_for_Submitting_Proposal__c + false + Internal deadline for review ready. + + 32768 + false + LongTextArea + 3 + + + Fogbugz_Assigned_To__c + false + Who is the Fogbugz ticket assigned to currently. + + 50 + false + false + Text + false + + + Fogbugz_Client__c + false + + 50 + false + false + Text + false + + + Fogbugz_Days_Not_Updated__c + Today - Fogbugz Last Updated Date + false + TODAY() - DATEVALUE(Fogbugz_Last_Updated_Date__c) + BlankAsZero + + 18 + false + 0 + Number + false + + + Fogbugz_Last_Updated_Date__c + false + The last day the Fogbugz ticket was updated. + + false + false + DateTime + + + Fogbugz_Link__c + false + "http://manage.dimagi.com/default.asp?" & Fogbugz_Ticket_Number__c + + false + Text + false + + + Fogbugz_Most_Recent_Note__c + false + The most recent update to Fogbugz ticket. + + 32768 + false + LongTextArea + 5 + + + Fogbugz_Opened_By__c + false + + 50 + false + false + Text + false + + + Fogbugz_Owner_Mapping__c + false + CASE( Fogbugz_Assigned_To__c , +"Gillian Javetski", "Neal Lesh", +"Benjamin Lightburn", "Neal Lesh", +"Andrea Fletcher", "Kieran Sharpey-Schafer", +"Sheel Shah", "Devika Sarin", +"Mohini Bhavsar", "Devika Sarin", +"Krishna Swamy", "Devika Sarin", +"Vikram Kumar","Neal Lesh", +"Nick Nestle") + + false + Text + false + + + Fogbugz_Probability__c + false + + 18 + false + 0 + false + Percent + + + Fogbugz_Ticket_Number__c + true + The ticket number of the corresponding Fogbugz ticket. + + 50 + false + false + Text + false + + + Funder_Account__c + Restrict + false + The account that is supplying the money for the opportunity. i.e. USAID funds CARE who then pays us. USAID is the "Funder Account", CARE is the "Account". + + Account + Opportunities Funded + Opportunities_Funded + false + false + Lookup + + + Funding_Type__c + false + Please flag if NIH or gov't potentially + + 32768 + false + LongTextArea + 3 + + + Implementing_Business_Unit__c + SetNull + false + The Dimagi BU that will manage the execution of the project. + + Business_Unit__c + Opportunities (Implementing Business Unit) + Opportunities1 + false + false + Lookup + + + Industry__c + false + + + + Agriculture + false + + + Consulting + false + + + Education + false + + + Finance + false + + + Health + false + + + Mobile Money + false + + + Nutrition + false + + + Other + false + + + Telecom + false + + + Water & Sanitation + false + + false + + false + Picklist + + + LeadSource + + + Contact Us + false + + + Referred to Us + false + + + Other + false + + + Conference + false + + + Pulled from Website + false + + + Publication - First Author + false + + + Publication - Last Author + false + + + Web Sign Up + false + + + Workshop - Maputo + false + + + Workshop - Dakar + false + + + Workshop - Other + false + + false + + Picklist + + + Opp_Stage__c + false + CASE( StageName , +'Stage 1 - Connect', 'Stage 1 - Connect', +'Stage 2 - Talking', 'Stage 2 - Talking', +'Stage 3 - Prospect', 'Stage 3 - Prospect', +'Stage 4 - Proposal Development', 'Stage 4 - Proposal Development', +'Stage 5 - Submitted', 'Stage 5 - Submitted', +'Stage 6 - In Negotiations', 'Stage 6 - In Negotiations', +'Closed - Lost','Closed - Lost', +'Closed - Won', 'Closed - Won', +'Pending other action', 'Pending other action', +'Closed','Closed', +'Stage 1 - Connect') + BlankAsZero + + false + Text + false + + + Opportunity_Owning_Entity__c + Restrict + false + Which busniness unit owns this opportunity. + + Business_Unit__c + Opportunities + Opportunities + false + false + Lookup + + + Product_Text__c + Populated via a trigger + false + Stores the products on the opp record so you can filter on them. + + false + false + TextArea + + + Project_Dates__c + false + Start Date, when features need to be ready, etc. + + 32768 + false + LongTextArea + 4 + + + Proposal_Dropbox_Location__c + false + This should be in Dimagi - Proposals + + false + false + TextArea + + + Report_Out_Summary__c + false + Used in the Biz Dev Report Outs to add comments. + + false + false + TextArea + + + Salesforce_Opportunity_ID__c + false + Id + BlankAsZero + Salesforce's internal reference ID. Put this in the Fogbugz External ID field. + + false + Text + false + + + Short_Description__c + false + + false + false + TextArea + + + StageName + + + Stage 1 - Connect + false + false + Pipeline + 0 + false + + + Stage 2 - Talking + false + false + Talking with an org but there is no concrete opportunity yet. + Pipeline + 0 + false + + + Stage 3 - Prospect + false + false + There is a real discreet opportunity now. A Fogbugz ticket is created. + Pipeline + 0 + false + + + Stage 4 - Proposal Development + false + false + Developing a proposal. + Pipeline + 0 + false + + + Stage 5 - Submitted + false + false + You have submitted the proposal + Pipeline + 0 + false + + + Stage 6 - In Negotiations + false + false + Won the award and negotiating price. + Pipeline + 0 + false + + + Closed + false + false + Pipeline + 0 + false + + false + + Picklist + + + Stage_Duration__c + false + TODAY() - Stage_Name_Updated_Date__c + BlankAsZero + + 18 + false + 0 + Number + false + + + Stage_Name_Updated_Date__c + false + + false + false + Date + + + Sub_Area__c + false + + + + *** Health *** + false + + + Maternal, Newborn, & Child Health + false + + + Family Planning + false + + + HIV/AIDS + false + + + Malaria + false + + + Respiratory Diseases + false + + + Tuberculosis + false + + + Polio + false + + + Vaccinations + false + + + Diarrhea + false + + + Primary Care + false + + + Non-Communicable Diseases + false + + + Mental Health + false + + + Nutrition + false + + + *** Poverty and Economic Development *** + false + + + Gender Services + false + + + Water, Sanitation, & Hygiene + false + + + Financial Services to the Poor + false + + + Urban Development + false + + + *** Natural Resource Management *** + false + + + Agriculture + false + + + Food Security + false + + + Environment + false + + + *** Training & Education Services *** + false + + + Adult Training + false + + + Child Education + false + + + Early Childhood Development + false + + + *** Logistics *** + false + + + Human Resources + false + + + Commodity Tracking/Procurement + false + + + *** Other *** + false + + + Emergency Response + false + + + Orphans and Vulnerable Children + false + + + Telecommunications + false + + false + + false + MultiselectPicklist + 8 + + + Tech_Capabilities_Features__c + false + Known technical dependencies / architecture. + + 32768 + false + LongTextArea + 5 + + + Total_Days_Not_Updated__c + false + MIN(Fogbugz_Days_Not_Updated__c, TODAY() - DATEVALUE(LastModifiedDate)) + BlankAsZero + + 18 + false + 0 + Number + false + + + Type + + + Existing Business + false + + + New Business + false + + false + + Picklist + + + X10_Major_component_risks__c + false + + 32768 + false + LongTextArea + 3 + + + X11_Worked_with_org_before__c + false + Any special context to know about? + + 32768 + false + LongTextArea + 3 + + + X4_Budget_Size__c + false + Size, split: dev/field/server, are we willing to lose money on this? + + 32768 + false + LongTextArea + 3 + + + X5_Which_Entity__c + false + Inc, DSA, DSI + + 32768 + false + LongTextArea + 3 + + + X7_Long_term_partnership_or_one_off__c + false + Partnership potential or just is this a one-off project? + + 32768 + false + LongTextArea + 3 + + + X8_Other_Direct_Costs_ODC_covered_by__c + false + Are ODC covered by Dimagi or the partner. e.g. SMS gateway. + + 32768 + false + LongTextArea + 3 + + + X9_Room_for_innovation__c + false + Is there room for innovation or are we specifically neglecting parts of the RFP? + + 32768 + false + LongTextArea + 5 + + + long_wait__c + false + false + For submitted opportunities with a long waiting period to hear back. These won't be marked red on the Biz Dev reports. + + false + Checkbox + + + Active_Opps + OPPORTUNITY.NAME + ACCOUNT.NAME + Funder_Account__c + Area__c + Sub_Area__c + OPPORTUNITY.AMOUNT + OPPORTUNITY.STAGE_NAME + Fogbugz_Probability__c + CORE.USERS.ALIAS + Everything + + OPPORTUNITY.STAGE_NAME + notEqual + Closed + + + + + AllOpportunities + OPPORTUNITY.CREATED_DATE + OPPORTUNITY.NAME + ACCOUNT.NAME + CORE.USERS.FULL_NAME + Fogbugz_Assigned_To__c + Fogbugz_Ticket_Number__c + Fogbugz_Link__c + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Opportunity_Owning_Entity__c + Country__c + Country_Name__c + Everything + + + + ClosingNextMonth + Everything + + OPPORTUNITY.CLOSED + equals + 0 + + + OPPORTUNITY.CLOSE_DATE + equals + NEXT_MONTH + + + + + ClosingThisMonth + Everything + + OPPORTUNITY.CLOSED + equals + 0 + + + OPPORTUNITY.CLOSE_DATE + equals + THIS_MONTH + + + + + Moz_Opportunities + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Everything + + + + Moz_Opportunities1 + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Mine + + + + MyOpportunities + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.CREATED_DATE + OPPORTUNITY.STAGE_NAME + Fogbugz_Assigned_To__c + Fogbugz_Ticket_Number__c + Salesforce_Opportunity_ID__c + CORE.USERS.ALIAS + Fogbugz_Client__c + Fogbugz_Last_Updated_Date__c + Fogbugz_Link__c + Fogbugz_Most_Recent_Note__c + Fogbugz_Opened_By__c + Fogbugz_Probability__c + Mine + + OPPORTUNITY.STAGE_NAME + notEqual + Closed Won,Closed Lost + + + + + My_South_Africa_Opps + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.STAGE_NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + CORE.USERS.ALIAS + Country__c + Opportunity_Owning_Entity__c + Fogbugz_Link__c + Everything + + Country__c + equals + South Africa + + + + + NewThisWeek + Everything + + OPPORTUNITY.CREATED_DATE + equals + THIS_WEEK + + + + + Non_Integrated_Opportunities + OPPORTUNITY.NAME + Fogbugz_Assigned_To__c + ACCOUNT.NAME + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Fogbugz_Client__c + Fogbugz_Last_Updated_Date__c + Fogbugz_Link__c + Fogbugz_Opened_By__c + Fogbugz_Ticket_Number__c + Everything + + CORE.USERS.ALIAS + equals + fogbugz + + + + + Opportunities_without_Accounts + OPPORTUNITY.NAME + ACCOUNT.NAME + Salesforce_Opportunity_ID__c + CORE.USERS.ALIAS + OPPORTUNITY.STAGE_NAME + Fogbugz_Assigned_To__c + Fogbugz_Link__c + Everything + + Fogbugz_Assigned_To__c + equals + closed + + + + + Opportunities_without_an_Owner + OPPORTUNITY.NAME + ACCOUNT.NAME + Fogbugz_Assigned_To__c + OPPORTUNITY.AMOUNT + OPPORTUNITY.STAGE_NAME + Fogbugz_Link__c + Fogbugz_Ticket_Number__c + Everything + + Fogbugz_Assigned_To__c + equals + CLOSED + + + + + Won + Everything + + OPPORTUNITY.WON + equals + 1 + + + OPPORTUNITY.CLOSED + equals + 1 + + + + + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.CLOSE_DATE + Sync_with_FB_multi + OPPORTUNITY.NAME + ACCOUNT.NAME + ACCOUNT.SITE + OPPORTUNITY.NAME + ACCOUNT.NAME + ACCOUNT.SITE + OPPORTUNITY.NAME + ACCOUNT.NAME + ACCOUNT.SITE + OPPORTUNITY.STAGE_NAME + OPPORTUNITY.CLOSE_DATE + CORE.USERS.ALIAS + + + Change_Stage_Name + false + ISCHANGED( StageName ) && $User.ProfileId = '00eb0000000gmdW' + StageName + You may not make changes to this field in Salesforce. Please edit the Fogbugz ticket. + + + Sync_with_FB_multi + online + massActionButton + 600 + page + Sync all with FogBugz + sidebar + RunFBSync + false + true + + + Sync_with_FB_single + online + button + 600 + page + Sync with FogBugz + sidebar + RunFBSyncSingle + false + + diff --git a/src/package.xml b/src/package.xml index 3340efa1..17eb7117 100644 --- a/src/package.xml +++ b/src/package.xml @@ -4,5 +4,55 @@ * ApexClass - 25.0 + + * + ApexComponent + + + * + ApexPage + + + * + ApexTrigger + + + * + Account + AccountContactRole + Activity + Asset + Campaign + CampaignMember + Case + CaseContactRole + Contact + ContentVersion + Contract + ContractContactRole + Event + Idea + Lead + Opportunity + OpportunityCompetitor + OpportunityContactRole + OpportunityLineItem + PartnerRole + Product2 + Site + SocialPersona + Solution + Task + User + CustomObject + + + * + CustomTab + + + * + StaticResource + + 27.0 diff --git a/src/pages/OpenOpportunityFieldSelection.page b/src/pages/OpenOpportunityFieldSelection.page new file mode 100644 index 00000000..4afa38b1 --- /dev/null +++ b/src/pages/OpenOpportunityFieldSelection.page @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pages/OpenOpportunityReportLayout.page b/src/pages/OpenOpportunityReportLayout.page new file mode 100644 index 00000000..6f50aa5c --- /dev/null +++ b/src/pages/OpenOpportunityReportLayout.page @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/tabs/Report_Settings.tab b/src/tabs/Report_Settings.tab new file mode 100644 index 00000000..2113f012 --- /dev/null +++ b/src/tabs/Report_Settings.tab @@ -0,0 +1,7 @@ + + + + false + Custom26: Flag + OpenOpportunityFieldSelection + diff --git a/src/triggers/OpenOpportunityUpdateTrigger.trigger b/src/triggers/OpenOpportunityUpdateTrigger.trigger new file mode 100644 index 00000000..d5f49324 --- /dev/null +++ b/src/triggers/OpenOpportunityUpdateTrigger.trigger @@ -0,0 +1,9 @@ +trigger OpenOpportunityUpdateTrigger on Opportunity (before insert, before update) { + + if (trigger.isInsert) { + OpenOpportunityUtils.updateStageName(trigger.new); + } + else if (trigger.isUpdate) { + OpenOpportunityUtils.updateStageName(trigger.new, trigger.oldMap); + } +} \ No newline at end of file diff --git a/src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml b/src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml new file mode 100644 index 00000000..6e684be3 --- /dev/null +++ b/src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + From 8c086a911290ff48cf965db35f81953c5c1a56c5 Mon Sep 17 00:00:00 2001 From: Fernando Rodriguez Date: Mon, 8 Apr 2013 12:56:16 -0300 Subject: [PATCH 4/4] deleteing stuff --- .../OpportunityLocationTrigger.trigger | 9 ------ ...pportunityLocationTrigger.trigger-meta.xml | 5 --- src/triggers/OpportunityProduct.trigger | 5 --- .../OpportunityProduct.trigger-meta.xml | 5 --- src/triggers/OpportunityToCase.trigger | 15 --------- .../OpportunityToCase.trigger-meta.xml | 5 --- src/triggers/OpportunityTrigger.trigger | 4 --- .../OpportunityTrigger.trigger-meta.xml | 5 --- src/triggers/ProjectLocationTrigger.trigger | 11 ------- .../ProjectLocationTrigger.trigger-meta.xml | 5 --- src/triggers/ProjectProduct.trigger | 9 ------ src/triggers/ProjectProduct.trigger-meta.xml | 5 --- src/triggers/ProjectTrigger.trigger | 6 ---- src/triggers/ProjectTrigger.trigger-meta.xml | 5 --- src/triggers/TaskToCaseNote.trigger | 32 ------------------- src/triggers/TaskToCaseNote.trigger-meta.xml | 5 --- 16 files changed, 131 deletions(-) delete mode 100644 src/triggers/OpportunityLocationTrigger.trigger delete mode 100644 src/triggers/OpportunityLocationTrigger.trigger-meta.xml delete mode 100644 src/triggers/OpportunityProduct.trigger delete mode 100644 src/triggers/OpportunityProduct.trigger-meta.xml delete mode 100644 src/triggers/OpportunityToCase.trigger delete mode 100644 src/triggers/OpportunityToCase.trigger-meta.xml delete mode 100644 src/triggers/OpportunityTrigger.trigger delete mode 100644 src/triggers/OpportunityTrigger.trigger-meta.xml delete mode 100644 src/triggers/ProjectLocationTrigger.trigger delete mode 100644 src/triggers/ProjectLocationTrigger.trigger-meta.xml delete mode 100644 src/triggers/ProjectProduct.trigger delete mode 100644 src/triggers/ProjectProduct.trigger-meta.xml delete mode 100644 src/triggers/ProjectTrigger.trigger delete mode 100644 src/triggers/ProjectTrigger.trigger-meta.xml delete mode 100644 src/triggers/TaskToCaseNote.trigger delete mode 100644 src/triggers/TaskToCaseNote.trigger-meta.xml diff --git a/src/triggers/OpportunityLocationTrigger.trigger b/src/triggers/OpportunityLocationTrigger.trigger deleted file mode 100644 index 1eb597af..00000000 --- a/src/triggers/OpportunityLocationTrigger.trigger +++ /dev/null @@ -1,9 +0,0 @@ -trigger OpportunityLocationTrigger on Opportunity_Location__c (before delete, after insert) { - - if (Trigger.isInsert) { - OpportunityTriggerSync.onInsert(Trigger.new); - } - else if (Trigger.isDelete) { - OpportunityTriggerSync.onDelete(Trigger.old); - } -} \ No newline at end of file diff --git a/src/triggers/OpportunityLocationTrigger.trigger-meta.xml b/src/triggers/OpportunityLocationTrigger.trigger-meta.xml deleted file mode 100644 index 1257ef61..00000000 --- a/src/triggers/OpportunityLocationTrigger.trigger-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 27.0 - Active - diff --git a/src/triggers/OpportunityProduct.trigger b/src/triggers/OpportunityProduct.trigger deleted file mode 100644 index 22ef899f..00000000 --- a/src/triggers/OpportunityProduct.trigger +++ /dev/null @@ -1,5 +0,0 @@ -trigger OpportunityProduct on OpportunityLineItem (after delete, after insert, after update) { - - - -} \ No newline at end of file diff --git a/src/triggers/OpportunityProduct.trigger-meta.xml b/src/triggers/OpportunityProduct.trigger-meta.xml deleted file mode 100644 index 1257ef61..00000000 --- a/src/triggers/OpportunityProduct.trigger-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 27.0 - Active - diff --git a/src/triggers/OpportunityToCase.trigger b/src/triggers/OpportunityToCase.trigger deleted file mode 100644 index 2a2054f8..00000000 --- a/src/triggers/OpportunityToCase.trigger +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Creates a FogBugz case upon Opportunity creation - * - * @todo Handle bulk insertions - * - * @author Antonio Grassi - * @date 11/13/2012 - */ - -trigger OpportunityToCase on Opportunity (after insert) { - - if (Trigger.new[0].Fogbugz_Ticket_Number__c == null) { - OpportunityTriggers.createInFogbugz(Trigger.new[0].Id); - } -} \ No newline at end of file diff --git a/src/triggers/OpportunityToCase.trigger-meta.xml b/src/triggers/OpportunityToCase.trigger-meta.xml deleted file mode 100644 index f9a06af0..00000000 --- a/src/triggers/OpportunityToCase.trigger-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 25.0 - Active - diff --git a/src/triggers/OpportunityTrigger.trigger b/src/triggers/OpportunityTrigger.trigger deleted file mode 100644 index 90fad743..00000000 --- a/src/triggers/OpportunityTrigger.trigger +++ /dev/null @@ -1,4 +0,0 @@ -trigger OpportunityTrigger on Opportunity (before insert, before update) { - - OpportunityTriggerSync.onOpportunityTrigger(Trigger.new); -} \ No newline at end of file diff --git a/src/triggers/OpportunityTrigger.trigger-meta.xml b/src/triggers/OpportunityTrigger.trigger-meta.xml deleted file mode 100644 index 1257ef61..00000000 --- a/src/triggers/OpportunityTrigger.trigger-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 27.0 - Active - diff --git a/src/triggers/ProjectLocationTrigger.trigger b/src/triggers/ProjectLocationTrigger.trigger deleted file mode 100644 index 16cfeedb..00000000 --- a/src/triggers/ProjectLocationTrigger.trigger +++ /dev/null @@ -1,11 +0,0 @@ -trigger ProjectLocationTrigger on Project_Location__c (after insert, before delete) { - - - if (Trigger.isInsert) { - ProjectTriggerSync.onInsert(Trigger.new); - } - else if (Trigger.isDelete) { - ProjectTriggerSync.onDelete(Trigger.old); - } - -} \ No newline at end of file diff --git a/src/triggers/ProjectLocationTrigger.trigger-meta.xml b/src/triggers/ProjectLocationTrigger.trigger-meta.xml deleted file mode 100644 index 1257ef61..00000000 --- a/src/triggers/ProjectLocationTrigger.trigger-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 27.0 - Active - diff --git a/src/triggers/ProjectProduct.trigger b/src/triggers/ProjectProduct.trigger deleted file mode 100644 index 702d2775..00000000 --- a/src/triggers/ProjectProduct.trigger +++ /dev/null @@ -1,9 +0,0 @@ -trigger ProjectProduct on Project_Product__c (after insert, after update, after delete) { - - if (Trigger.isDelete) { - ProjectProductTrigger.onUpdate(Trigger.old); - } - else ProjectProductTrigger.onUpdate(Trigger.new); - - -} \ No newline at end of file diff --git a/src/triggers/ProjectProduct.trigger-meta.xml b/src/triggers/ProjectProduct.trigger-meta.xml deleted file mode 100644 index 1257ef61..00000000 --- a/src/triggers/ProjectProduct.trigger-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 27.0 - Active - diff --git a/src/triggers/ProjectTrigger.trigger b/src/triggers/ProjectTrigger.trigger deleted file mode 100644 index 4789206b..00000000 --- a/src/triggers/ProjectTrigger.trigger +++ /dev/null @@ -1,6 +0,0 @@ -trigger ProjectTrigger on Project__c (before insert, before update) { - - ProjectTriggerSync.onProjectTrigger(Trigger.new); - ProjectTriggerArea.onUpdate(Trigger.new); - -} \ No newline at end of file diff --git a/src/triggers/ProjectTrigger.trigger-meta.xml b/src/triggers/ProjectTrigger.trigger-meta.xml deleted file mode 100644 index 1257ef61..00000000 --- a/src/triggers/ProjectTrigger.trigger-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 27.0 - Active - diff --git a/src/triggers/TaskToCaseNote.trigger b/src/triggers/TaskToCaseNote.trigger deleted file mode 100644 index f928eafa..00000000 --- a/src/triggers/TaskToCaseNote.trigger +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Adds a note to the FogBugz case upon Task creation - * - * @todo Handle bulk insertions - * - * @author Antonio Grassi - * @date 11/16/2012 - */ -trigger TaskToCaseNote on Task (after insert) { - - Set tasksInSet = new Set {}; - - for (Task t:Trigger.new) { - tasksInSet.add(t.Id); - } - - Task[] tasks = [select Id, - WhatId - from Task - where Id in :tasksInSet - and Subject like 'Email: %' - and What.Type = 'Opportunity']; - - if (!tasks.isEmpty()) { - - Opportunity o = [select Fogbugz_Ticket_Number__c from Opportunity where Id = :tasks[0].WhatId]; - - if (o.Fogbugz_Ticket_Number__c != null) { - TaskTriggers.addNoteInFogBugz(tasks[0].Id); - } - } -} \ No newline at end of file diff --git a/src/triggers/TaskToCaseNote.trigger-meta.xml b/src/triggers/TaskToCaseNote.trigger-meta.xml deleted file mode 100644 index f9a06af0..00000000 --- a/src/triggers/TaskToCaseNote.trigger-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 25.0 - Active -