From 5ee59cc07549139e7f27910f6a2ea5d6ca389551 Mon Sep 17 00:00:00 2001 From: chrisala Date: Fri, 14 Feb 2025 14:34:05 +1100 Subject: [PATCH] Keep RCS funding totals up to date #3429 --- grails-app/assets/javascripts/budget.js | 37 ++++++++-- grails-app/assets/javascripts/services.js | 12 ++++ .../ala/merit/OrganisationController.groovy | 10 ++- .../org/ala/merit/OrganisationService.groovy | 69 ++++++++++++++++++- grails-app/views/organisation/_funding.gsp | 12 +++- .../views/organisation/_serviceTargets.gsp | 2 + grails-app/views/organisation/index.gsp | 1 + ...cityServicesReportLifecycleListener.groovy | 11 +-- .../scripts/releases/4.2/updateIPPRSScores.js | 35 ++++++++-- .../4.2/updateOrganisationFundingMetadata.js | 30 ++++++++ 10 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 src/main/scripts/releases/4.2/updateOrganisationFundingMetadata.js diff --git a/grails-app/assets/javascripts/budget.js b/grails-app/assets/javascripts/budget.js index 706485eb7..cfa7a7a96 100644 --- a/grails-app/assets/javascripts/budget.js +++ b/grails-app/assets/javascripts/budget.js @@ -1,3 +1,8 @@ +var BudgetConstants = { + PERIODICALLY_REVISED_TOTAL : 'periodicallyRevisedTotal', + PERIODIC_TOTAL : 'perPeriodBreakdown' +}; + function BudgetViewModel(o, period) { var self = this; if (!o) o = {}; @@ -71,11 +76,15 @@ function BudgetTotalViewModel(rows, index) { function BudgetRowViewModel(o, period) { var self = this; + + if (!o) o = {}; if (!o.activities || !_.isArray(o.activities)) o.activities = []; self.shortLabel = ko.observable(o.shortLabel); self.description = ko.observable(o.description); self.activities = ko.observableArray(o.activities); + self.type = o.type || BudgetConstants.PERIODIC_TOTAL; + var arr = []; // Have at least one period to record, which will essentially be a project total. @@ -94,10 +103,30 @@ function BudgetRowViewModel(o, period) { self.rowTotal = ko.computed(function () { var total = 0.0; - ko.utils.arrayForEach(this.costs(), function (cost) { - if (cost.dollar()) - total += parseFloat(cost.dollar()); - }); + // The Periodically Revised Total is a special case used by the IPPRS system whereby each year they + // revise the total contract value. For this case, the rowTotal is determined by starting in the + // current year and working backwards until a non-zero value is found. + if (self.type === BudgetConstants.PERIODICALLY_REVISED_TOTAL) { + var currentDateString = convertToIsoDate(new Date()); + var i = 0; + + // Find the current period. + while (i0 && (isNaN(total) || total == 0)) { + i--; + total = parseFloat(self.costs()[i].dollar()); + } + } + else { //self.type === PERIODIC_TOTAL is the default - i.e. the default behaviour is to sum the columns + ko.utils.arrayForEach(this.costs(), function (cost) { + if (cost.dollar()) + total += parseFloat(cost.dollar()); + }); + } + return total; }, this).extend({currency: {}}); }; diff --git a/grails-app/assets/javascripts/services.js b/grails-app/assets/javascripts/services.js index dad0336a0..4e9c64619 100644 --- a/grails-app/assets/javascripts/services.js +++ b/grails-app/assets/javascripts/services.js @@ -5,6 +5,18 @@ function OrganisationDetailsViewModel(o, organisation, budgetHeaders, allService targets = o.services && o.services.targets || []; self.areTargetsAndFundingEditable = config.areTargetsAndFundingEditable; self.services = new OrganisationServicesViewModel(serviceIds, config.services, targets, budgetHeaders, {areTargetsEditable:config.areTargetsAndFundingEditable}); + + if (!o.funding) { + o.funding = { + rows: [ + { + type:BudgetConstants.PERIODICALLY_REVISED_TOTAL, + shortLabel: 'rcsContractedFunding', + description: 'RCS Contracted Funding' + } + ] + } + } self.funding = new BudgetViewModel(o.funding, budgetHeaders); self.funding.isEditable = config.areTargetsAndFundingEditable; diff --git a/grails-app/controllers/au/org/ala/merit/OrganisationController.groovy b/grails-app/controllers/au/org/ala/merit/OrganisationController.groovy index 3c87dfe1b..702e2145e 100644 --- a/grails-app/controllers/au/org/ala/merit/OrganisationController.groovy +++ b/grails-app/controllers/au/org/ala/merit/OrganisationController.groovy @@ -84,6 +84,12 @@ class OrganisationController { dashboardData = organisationService.scoresForOrganisation(organisation, scores?.collect{it.scoreId}, !hasEditorAccess) } boolean showTargets = userService.userIsSiteAdmin() && services && targetPeriods + // This call is used to ensure the organisation funding total is kept up to date as the algorithm + // for selecting the current total is based on the current date. The funding total is used when + // calculating data for the dashboard. + if (showTargets) { + organisationService.checkAndUpdateFundingTotal(organisation) + } boolean targetsEditable = userService.userIsAlaOrFcAdmin() List reportOrder = null if (reportingVisible) { @@ -100,7 +106,7 @@ class OrganisationController { } } - boolean showEditAnnoucements = organisation.projects?.find{Status.isActive(it.status)} + boolean showEditAnnouncements = organisation.projects?.find{Status.isActive(it.status)} List adHocReportTypes =[ [type: ReportService.PERFORMANCE_MANAGEMENT_REPORT]] @@ -114,7 +120,7 @@ class OrganisationController { projects : [label: 'Reporting', template:"/shared/projectListByProgram", visible: reportingVisible, stopBinding:true, default:reportingVisible, type: 'tab', reports:organisation.reports, adHocReportTypes:adHocReportTypes, reportOrder:reportOrder, hideDueDate:true, displayedPrograms:projectGroups.displayedPrograms, reportsFirst:true, declarationType:SettingPageType.RDP_REPORT_DECLARATION], sites : [label: 'Sites', visible: reportingVisible, type: 'tab', stopBinding:true, projectCount:organisation.projects?.size()?:0, showShapefileDownload:adminVisible], dashboard : [label: 'Dashboard', visible: reportingVisible, stopBinding:true, type: 'tab', template:'dashboard', reports:dashboardReports, dashboardData:dashboardData], - admin : [label: 'Admin', visible: adminVisible, type: 'tab', template:'admin', showEditAnnoucements:showEditAnnoucements, availableReportCategories:availableReportCategories, targetPeriods:targetPeriods, services: services, showTargets:showTargets, targetsEditable:targetsEditable]] + admin : [label: 'Admin', visible: adminVisible, type: 'tab', template:'admin', showEditAnnoucements:showEditAnnouncements, availableReportCategories:availableReportCategories, targetPeriods:targetPeriods, services: services, showTargets:showTargets, targetsEditable:targetsEditable]] } diff --git a/grails-app/services/au/org/ala/merit/OrganisationService.groovy b/grails-app/services/au/org/ala/merit/OrganisationService.groovy index 4d30720a0..3c4ac8137 100644 --- a/grails-app/services/au/org/ala/merit/OrganisationService.groovy +++ b/grails-app/services/au/org/ala/merit/OrganisationService.groovy @@ -13,7 +13,8 @@ import org.joda.time.Period class OrganisationService { - def grailsApplication,webService, metadataService, projectService, userService, searchService, activityService, emailService, reportService, documentService + public static final String RCS_CONTRACTED_FUNDING = 'rcsContractedFunding' + def grailsApplication, webService, metadataService, projectService, userService, searchService, activityService, emailService, reportService, documentService AbnLookupService abnLookupService private static def APPROVAL_STATUS = ['unpublished', 'pendingApproval', 'published'] @@ -146,6 +147,72 @@ class OrganisationService { reportService.generateTargetPeriods(targetsReportConfig, owner, targetsConfig.periodLabelFormat) } + double getRcsFundingForPeriod(Map organisation, String periodEndDate) { + + int index = findIndexOfPeriod(organisation, periodEndDate) + def result = 0 + if (index >= 0) { + Map rcsFunding = getRcsFunding(organisation) + result = rcsFunding?.costs[index]?.dollar ?: 0 + } + result + } + + private static int findIndexOfPeriod(Map organisation, String periodEndDate) { + List fundingHeaders = organisation.custom?.details?.funding?.headers + String previousPeriod = '' + fundingHeaders.findIndexOf { + String period = it.data.value + boolean result = previousPeriod < periodEndDate && period >= periodEndDate + previousPeriod = period + result + } + + } + + /** Returns the funding row used to collect RCS funding data */ + private static Map getRcsFunding(Map organisation) { + // The funding recorded for an organisation is specific to RCS reporting. + // Instead of being a "funding per financial year" it is a annually revised total funding amount. + // This is used in calculations alongside data reported in the RCS report. + List fundingRows = organisation.custom?.details?.funding?.rows + fundingRows?.find{it.shortLabel == RCS_CONTRACTED_FUNDING } + + } + + void checkAndUpdateFundingTotal(Map organisation) { + + String today = DateUtils.formatAsISOStringNoMillis(new Date()) + + Map rcsFunding = getRcsFunding(organisation) + if (!rcsFunding) { + return + } + double funding = 0 + int index = findIndexOfPeriod(organisation, today) + + while (index >= 0 && funding == 0) { + def fundingStr = rcsFunding.costs[index]?.dollar + if (fundingStr) { + try { + funding = Double.parseDouble(fundingStr) + } catch (NumberFormatException e) { + log.error("Error parsing funding amount for organisation ${organisation.organisationId} at index $index") + } + } + index-- + + } + + if (funding != rcsFunding.rowTotal) { + rcsFunding.rowTotal = funding + organisation.custom.details.funding.overallTotal = rcsFunding.rowTotal + log.info("Updating the funding information for organisation ${organisation.organisationId} to $funding") + update(organisation.organisationId, organisation.custom) + } + + } + private void regenerateOrganisationReports(Map organisation, List reportCategories = null) { diff --git a/grails-app/views/organisation/_funding.gsp b/grails-app/views/organisation/_funding.gsp index 9bbab0431..a37d3e402 100644 --- a/grails-app/views/organisation/_funding.gsp +++ b/grails-app/views/organisation/_funding.gsp @@ -2,12 +2,15 @@
- -

${explanation}

-
+

The 'Current funding' in the table below is used to calculate the Indigenous supply chain performance metric on the dashboard and is determined by the most recent non-zero funding amount starting from the current year. + Future funding amounts will not affect the current funding.

+ + @@ -16,6 +19,9 @@ +
+ Current fundingThe current funding is used to calculate the Indigenous supply chain performance metric on the dashboard and is determined by the most recent non-zero funding amount starting from the current year +
$
+ + diff --git a/grails-app/views/organisation/_serviceTargets.gsp b/grails-app/views/organisation/_serviceTargets.gsp index ff0d34c33..becc7278e 100644 --- a/grails-app/views/organisation/_serviceTargets.gsp +++ b/grails-app/views/organisation/_serviceTargets.gsp @@ -1,5 +1,7 @@

${title ?: "Organisation targets"}

+

The overall targets in the table below is calculated as the average of every non-blank target in the table. If future year targets are entered in to the table, they will be included in the overall target calculation.

+

The overall target is display on the Organisation dashboard tab.

diff --git a/grails-app/views/organisation/index.gsp b/grails-app/views/organisation/index.gsp index 46eb22ea2..b5f6cd711 100644 --- a/grails-app/views/organisation/index.gsp +++ b/grails-app/views/organisation/index.gsp @@ -45,6 +45,7 @@ returnTo: '${g.createLink(action:'index', id:"${organisation.organisationId}")}', dashboardCategoryUrl: "${g.createLink(controller: 'report', action: 'activityOutputs', params: [fq:'organisationFacet:'+organisation.name])}", reportOwner: {organisationId:'${organisation.organisationId}'}, + i18nURL: "${g.createLink(controller: 'home', action: 'i18n')}", projects : }; diff --git a/src/main/groovy/au/org/ala/merit/reports/RegionalCapacityServicesReportLifecycleListener.groovy b/src/main/groovy/au/org/ala/merit/reports/RegionalCapacityServicesReportLifecycleListener.groovy index 847b2d176..48f0e5a0a 100644 --- a/src/main/groovy/au/org/ala/merit/reports/RegionalCapacityServicesReportLifecycleListener.groovy +++ b/src/main/groovy/au/org/ala/merit/reports/RegionalCapacityServicesReportLifecycleListener.groovy @@ -24,16 +24,9 @@ class RegionalCapacityServicesReportLifecycleListener extends ReportLifecycleLis [periodTargets:periodTargets, totalContractValue:funding, reportedFundingExcludingThisReport:reportedFundingExcludingThisReport] } - private static def getFundingForPeriod(Map organisation, Map report) { + private double getFundingForPeriod(Map organisation, Map report) { String endDate = report.toDate - String previousPeriod = '' - def index = organisation.custom?.details?.funding?.headers?.findIndexOf { - String period = it.data.value - boolean result = previousPeriod < endDate && period >= endDate - previousPeriod = period - result - } - index >= 0 ? organisation.custom?.details?.funding?.rows[0].costs[index].dollar : 0 + organisationService.getRcsFundingForPeriod(organisation, endDate) } diff --git a/src/main/scripts/releases/4.2/updateIPPRSScores.js b/src/main/scripts/releases/4.2/updateIPPRSScores.js index 1a1ff7630..c0aa580ee 100644 --- a/src/main/scripts/releases/4.2/updateIPPRSScores.js +++ b/src/main/scripts/releases/4.2/updateIPPRSScores.js @@ -14,14 +14,27 @@ var scores = [ type: "filter" }, "childAggregations": [{ - "property": "data.workforcePerformancePercentage", - "type": "AVERAGE" + "expression":"totalFirstNationsFteWorkforce/totalOrganisationFteWorkforce*100", + "defaultValue": 0, + "childAggregations": [ + { + "label":"totalFirstNationsFteWorkforce", + "property":"data.organisationFteIndigenousWorkforce", + "type":"SUM" + }, + { + "label":"totalOrganisationFteWorkforce", + "property":"data.organisationFteWorkforce", + "type":"SUM" + } + ] }] }, "description": "", "displayType": "", "entity": "Activity", "entityTypes": [], + "units":"percentage", "isOutputTarget": true, "label": "Indigenous workforce performance (% of Indigenous FTE achieved to date/% FTE target for Indigenous employment to date)", "outputType": "Regional capacity services - reporting", @@ -38,14 +51,28 @@ var scores = [ type: "filter" }, "childAggregations": [{ - "property": "data.supplyChainPerformancePercentage", - "type": "SUM" + "expression":"totalFirstNationsProcurement/currentTotalProcurement*100", + "defaultValue": 0, + "childAggregations": [ + { + "label": "totalFirstNationsProcurement", + "property": "data.servicesContractedValueFirstNations", + "type": "SUM" + }, + { + "label": "currentTotalProcurement", + "property": "activity.organisation.custom.details.funding.overallTotal", + "type": "DISTINCT_SUM", + "keyProperty": "activity.organisation.organisationId" + } + ] }] }, "description": "", "displayType": "", "entity": "Activity", "entityTypes": [], + "units": "$", "isOutputTarget": true, "label": "Indigenous supply chain performance (% of procurement from Indigenous suppliers achieved to date/% procurement target of procurement from Indigenous suppliers at end of Deed period)", "outputType": "Regional capacity services - reporting", diff --git a/src/main/scripts/releases/4.2/updateOrganisationFundingMetadata.js b/src/main/scripts/releases/4.2/updateOrganisationFundingMetadata.js new file mode 100644 index 000000000..da4ca9d8a --- /dev/null +++ b/src/main/scripts/releases/4.2/updateOrganisationFundingMetadata.js @@ -0,0 +1,30 @@ +load('../../utils/audit.js'); +const adminUserId = 'system'; +let organisations = db.organisation.find({'custom.details.funding':{$exists:true}}); +while (organisations.hasNext()) { + let organisation = organisations.next(); + let funding = organisation.custom.details.funding; + + if (!funding) { + continue; + } + if (funding.rows.length != 1) { + print("Organisation " + organisation.organisationId + " has " + funding.rows.length + " funding rows"); + } + + const metadata = { + type: 'periodicallyRevisedTotal', + shortLabel: 'rcsContractedFunding', + description: 'RCS Contracted Funding' + }; + + if (funding.rows[0].type != metadata.type || funding.rows[0].shortLabel != metadata.shortLabel || funding.rows[0].description != metadata.description) { + print("Updating funding metadata for organisation " + organisation.organisationId+", "+organisation.name); + + funding.rows[0].type = metadata.type; + funding.rows[0].shortLabel = metadata.shortLabel; + funding.rows[0].description = metadata.description; + db.organisation.replaceOne({organisationId: organisation.organisationId}, organisation); + audit(organisation, organisation.organisationId, 'au.org.ala.ecodata.Organisation', adminUserId); + } +} \ No newline at end of file