diff --git a/backend/lcfs/db/models/compliance/listeners.py b/backend/lcfs/db/models/compliance/listeners.py
index 49ab43c34..1ea2b22b8 100644
--- a/backend/lcfs/db/models/compliance/listeners.py
+++ b/backend/lcfs/db/models/compliance/listeners.py
@@ -9,9 +9,3 @@ def prevent_update_if_locked(mapper, connection, target):
raise InvalidRequestError("Cannot update a locked ComplianceReportSummary")
-@event.listens_for(ComplianceReportSummary.is_locked, "set")
-def prevent_unlock(target, value, oldvalue, initiator):
- if oldvalue and not value:
- raise InvalidRequestError(
- "Cannot unlock a ComplianceReportSummary once it's locked"
- )
diff --git a/backend/lcfs/web/api/compliance_report/repo.py b/backend/lcfs/web/api/compliance_report/repo.py
index 59696d6ab..bb84ef308 100644
--- a/backend/lcfs/web/api/compliance_report/repo.py
+++ b/backend/lcfs/web/api/compliance_report/repo.py
@@ -9,7 +9,7 @@
from lcfs.db.models.fuel.FuelType import FuelType
from lcfs.db.models.fuel.FuelCategory import FuelCategory
from lcfs.db.models.fuel.ExpectedUseType import ExpectedUseType
-from sqlalchemy import func, select, and_, asc, desc
+from sqlalchemy import func, select, and_, asc, desc, update
from sqlalchemy.orm import joinedload
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import Depends
@@ -570,6 +570,16 @@ async def add_compliance_report_summary(
await self.db.refresh(summary)
return summary
+ @repo_handler
+ async def reset_summary_lock(self, compliance_report_id: int):
+ query = (
+ update(ComplianceReportSummary)
+ .where(ComplianceReportSummary.compliance_report_id == compliance_report_id)
+ .values(is_locked=False)
+ )
+ await self.db.execute(query)
+ return True
+
@repo_handler
async def save_compliance_report_summary(
self, summary: ComplianceReportSummaryUpdateSchema
diff --git a/backend/lcfs/web/api/compliance_report/schema.py b/backend/lcfs/web/api/compliance_report/schema.py
index e839bee11..aa8e34858 100644
--- a/backend/lcfs/web/api/compliance_report/schema.py
+++ b/backend/lcfs/web/api/compliance_report/schema.py
@@ -1,12 +1,13 @@
from enum import Enum
-from typing import ClassVar, Optional, List, Union
+from typing import ClassVar, Optional, List
from datetime import datetime, date
from enum import Enum
+from lcfs.db.models.compliance.ComplianceReportStatus import ComplianceReportStatusEnum
from lcfs.web.api.fuel_code.schema import EndUseTypeSchema, EndUserTypeSchema
from lcfs.web.api.base import BaseSchema, FilterModel, SortOrder
from lcfs.web.api.base import PaginationResponseSchema
-from pydantic import Field, Extra
+from pydantic import Field
"""
Base - all shared attributes of a resource
@@ -17,6 +18,18 @@
"""
+class ReturnStatus(Enum):
+ ANALYST = "Return to analyst"
+ MANAGER = "Return to manager"
+ SUPPLIER = "Return to supplier"
+
+RETURN_STATUS_MAPPER = {
+ ReturnStatus.ANALYST.value: ComplianceReportStatusEnum.Submitted.value,
+ ReturnStatus.MANAGER.value: ComplianceReportStatusEnum.Recommended_by_analyst.value,
+ ReturnStatus.SUPPLIER.value: ComplianceReportStatusEnum.Draft.value,
+}
+
+
class SupplementalInitiatorType(str, Enum):
SUPPLIER_SUPPLEMENTAL = "Supplier Supplemental"
GOVERNMENT_REASSESSMENT = "Government Reassessment"
@@ -45,7 +58,7 @@ class SummarySchema(BaseSchema):
is_locked: bool
class Config:
- extra = Extra.allow
+ extra = 'allow'
class ComplianceReportStatusSchema(BaseSchema):
diff --git a/backend/lcfs/web/api/compliance_report/update_service.py b/backend/lcfs/web/api/compliance_report/update_service.py
index 1a1d7d9c7..d58c51cff 100644
--- a/backend/lcfs/web/api/compliance_report/update_service.py
+++ b/backend/lcfs/web/api/compliance_report/update_service.py
@@ -1,4 +1,5 @@
import json
+from typing import Tuple
from fastapi import Depends, HTTPException, Request
from lcfs.web.api.notification.schema import (
COMPLIANCE_REPORT_STATUS_NOTIFICATION_MAPPER,
@@ -12,7 +13,11 @@
from lcfs.db.models.transaction.Transaction import TransactionActionEnum
from lcfs.db.models.user.Role import RoleEnum
from lcfs.web.api.compliance_report.repo import ComplianceReportRepository
-from lcfs.web.api.compliance_report.schema import ComplianceReportUpdateSchema
+from lcfs.web.api.compliance_report.schema import (
+ RETURN_STATUS_MAPPER,
+ ComplianceReportUpdateSchema,
+ ReturnStatus,
+)
from lcfs.web.api.compliance_report.summary_service import (
ComplianceReportSummaryService,
)
@@ -39,48 +44,67 @@ def __init__(
self.trx_service = trx_service
self.notfn_service = notfn_service
- async def update_compliance_report(
- self, report_id: int, report_data: ComplianceReportUpdateSchema
- ) -> ComplianceReport:
- """Updates an existing compliance report."""
- RETURN_STATUSES = ["Return to analyst", "Return to manager"]
+ async def _handle_return_status(
+ self, report_data: ComplianceReportUpdateSchema
+ ) -> Tuple[str, bool]:
+ """Handle return status logic and return new status and change flag."""
+ mapped_status = RETURN_STATUS_MAPPER.get(report_data.status)
+ return mapped_status, False
+
+ async def _check_report_exists(self, report_id: int) -> ComplianceReport:
+ """Verify report exists and return it."""
report = await self.repo.get_compliance_report_by_id(report_id, is_model=True)
if not report:
raise DataNotFoundException(
f"Compliance report with ID {report_id} not found"
)
+ return report
+
+ async def update_compliance_report(
+ self, report_id: int, report_data: ComplianceReportUpdateSchema
+ ) -> ComplianceReport:
+ """Updates an existing compliance report."""
+ # Get and validate report
+ report = await self._check_report_exists(report_id)
+
+ # Store original status
current_status = report_data.status
- # if we're just returning the compliance report back to either compliance manager or analyst,
- # then neither history nor any updates to summary is required.
- if report_data.status in RETURN_STATUSES:
- status_has_changed = False
- notifications = COMPLIANCE_REPORT_STATUS_NOTIFICATION_MAPPER.get(
- report_data.status
+
+ # Handle status changes
+ if report_data.status in [status.value for status in ReturnStatus]:
+ new_status, status_has_changed = await self._handle_return_status(
+ report_data
)
- if report_data.status == "Return to analyst":
- report_data.status = ComplianceReportStatusEnum.Submitted.value
- else:
- report_data.status = (
- ComplianceReportStatusEnum.Recommended_by_analyst.value
- )
+ report_data.status = new_status
+
+ # Handle "Return to supplier"
+ if current_status == ReturnStatus.SUPPLIER.value:
+ await self.repo.reset_summary_lock(report.compliance_report_id)
else:
+ # Handle normal status change
status_has_changed = report.current_status.status != getattr(
ComplianceReportStatusEnum, report_data.status.replace(" ", "_")
)
+
+ # Get new status object
new_status = await self.repo.get_compliance_report_status_by_desc(
report_data.status
)
- # Update fields
+
+ # Update report
report.current_status = new_status
report.supplemental_note = report_data.supplemental_note
-
updated_report = await self.repo.update_compliance_report(report)
+
+ # Handle status change related actions
if status_has_changed:
await self.handle_status_change(report, new_status.status)
# Add history record
await self.repo.add_compliance_report_history(report, self.request.user)
+ # Handle notifications
await self._perform_notification_call(report, current_status)
+
return updated_report
async def _perform_notification_call(self, report, status):
@@ -102,7 +126,7 @@ async def _perform_notification_call(self, report, status):
"status": status.lower(),
}
notification_data = NotificationMessageSchema(
- type=f"Compliance report {status.lower()}",
+ type=f"Compliance report {status.lower().replace('return', 'returned')}",
related_transaction_id=f"CR{report.compliance_report_id}",
message=json.dumps(message_data),
related_organization_id=report.organization_id,
@@ -228,12 +252,18 @@ async def handle_submitted_status(self, report: ComplianceReport):
report.summary = new_summary
if report.summary.line_20_surplus_deficit_units != 0:
- # Create a new reserved transaction for receiving organization
- report.transaction = await self.org_service.adjust_balance(
- transaction_action=TransactionActionEnum.Reserved,
- compliance_units=report.summary.line_20_surplus_deficit_units,
- organization_id=report.organization_id,
- )
+ if report.transaction is not None:
+ # Update existing transaction
+ report.transaction.compliance_units = (
+ report.summary.line_20_surplus_deficit_units
+ )
+ else:
+ # Create a new reserved transaction for receiving organization
+ report.transaction = await self.org_service.adjust_balance(
+ transaction_action=TransactionActionEnum.Reserved,
+ compliance_units=report.summary.line_20_surplus_deficit_units,
+ organization_id=report.organization_id,
+ )
await self.repo.update_compliance_report(report)
return calculated_summary
diff --git a/backend/lcfs/web/api/notification/schema.py b/backend/lcfs/web/api/notification/schema.py
index 30466bfa8..31f0f7270 100644
--- a/backend/lcfs/web/api/notification/schema.py
+++ b/backend/lcfs/web/api/notification/schema.py
@@ -117,6 +117,9 @@ class NotificationRequestSchema(BaseSchema):
"Return to manager": [
NotificationTypeEnum.IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__ANALYST_RECOMMENDATION
],
+ "Return to supplier": [
+ NotificationTypeEnum.BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT
+ ]
}
diff --git a/frontend/src/assets/locales/en/reports.json b/frontend/src/assets/locales/en/reports.json
index 91ced911b..a9d80c348 100644
--- a/frontend/src/assets/locales/en/reports.json
+++ b/frontend/src/assets/locales/en/reports.json
@@ -39,17 +39,19 @@
"submitReportBtn": "Submit report",
"returnToAnalyst": "Return to analyst",
"returnToManager": "Return to compliance manager",
+ "returnToSupplier": "Return report to the supplier",
"recommendReportAnalystBtn": "Recommend to compliance manager",
"recommendReportManagerBtn": "Recommend to director",
"assessReportBtn": "Issue assessment",
"reAssessReportBtn": "Re-assess report"
},
- "savedSuccessText": "Compliance report successfully saved",
+ "savedSuccessText": "Compliance report successfully {{status}}",
"submitConfirmText": "Are you sure you want to sign and submit this compliance report?",
"deleteConfirmText": "Are you sure you want to delete this compliance report?",
"recommendConfirmText": "Are you sure you want to recommend this compliance report?",
"returnToAnalystConfirmText": "Are you sure you want to return this compliance report back to analyst?",
"returnToManagerConfirmText": "Are you sure you want to return this compliance report back to compliance manager?",
+ "returnToSupplierConfirmText": "Are you sure you want to return this compliance report back to the supplier?",
"assessConfirmText": "Are you sure you want to assess this compliance report?",
"uploadLabel": "Upload supporting documents for your report.",
"introduction": "Introduction",
diff --git a/frontend/src/components/BCDataGrid/components/Filters/BCColumnSetFilter.jsx b/frontend/src/components/BCDataGrid/components/Filters/BCColumnSetFilter.jsx
index 2bc840392..fec6ab853 100644
--- a/frontend/src/components/BCDataGrid/components/Filters/BCColumnSetFilter.jsx
+++ b/frontend/src/components/BCDataGrid/components/Filters/BCColumnSetFilter.jsx
@@ -71,7 +71,12 @@ export const BCColumnSetFilter = forwardRef((props, ref) => {
limitTags={1}
className="bc-column-set-filter ag-list ag-select-list ag-ltr ag-popup-child ag-popup-positioned-under"
role="list-box"
- sx={{ width: '100%' }}
+ sx={{
+ width: '100%',
+ '.MuiInputBase-root': {
+ borderRadius: 'inherit'
+ }
+ }}
options={options}
loading={optionsIsLoading}
autoHighlight
@@ -126,13 +131,6 @@ export const BCColumnSetFilter = forwardRef((props, ref) => {
BCColumnSetFilter.displayName = 'BCColumnSetFilter'
-BCColumnSetFilter.defaultProps = {
- // apiQuery: () => ({ data: [], isLoading: false }),
- apiOptionField: 'name',
- multiple: false,
- disableCloseOnSelect: false
-}
-
BCColumnSetFilter.propTypes = {
apiQuery: PropTypes.func.isRequired, // react query or a fetch query which will return data, isLoading and Error fields.
// for static data, use the following format:
diff --git a/frontend/src/components/BCNavbar/components/DefaultNavbarLink.jsx b/frontend/src/components/BCNavbar/components/DefaultNavbarLink.jsx
index 347b5d6b3..4031baa69 100644
--- a/frontend/src/components/BCNavbar/components/DefaultNavbarLink.jsx
+++ b/frontend/src/components/BCNavbar/components/DefaultNavbarLink.jsx
@@ -66,16 +66,17 @@ function DefaultNavbarLink({
onMouseLeave={() => setHover(false)}
onClick={onClick}
>
- {icon && (
+ {icon && typeof icon === 'string' ? (
- light ? primary.main : secondary.main,
+ color: '#fff',
verticalAlign: 'middle'
}}
>
{icon}
+ ) : (
+ <>{icon}>
)}
{
},
onSettled: () => {
queryClient.invalidateQueries(['compliance-report', reportID])
+ queryClient.invalidateQueries(['compliance-report-summary', reportID])
+ queryClient.invalidateQueries(['compliance-reports'])
}
})
}
diff --git a/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx b/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx
index 37ebd0557..cfdf3c4ec 100644
--- a/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx
+++ b/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import BCBox from '@/components/BCBox'
import BCButton from '@/components/BCButton'
import BCTypography from '@/components/BCTypography'
+import DefaultNavbarLink from '@/components/BCNavbar/components/DefaultNavbarLink'
import { useCurrentUser } from '@/hooks/useCurrentUser'
import { useNotificationsCount } from '@/hooks/useNotifications'
import {
@@ -15,14 +16,13 @@ import {
Tooltip
} from '@mui/material'
import NotificationsIcon from '@mui/icons-material/Notifications'
-import { useNavigate, useLocation } from 'react-router-dom'
+import { useLocation } from 'react-router-dom'
import { ROUTES } from '@/constants/routes'
export const UserProfileActions = () => {
const { t } = useTranslation()
const { data: currentUser } = useCurrentUser()
const { keycloak } = useKeycloak()
- const navigate = useNavigate()
const location = useLocation()
// TODO:
@@ -44,6 +44,22 @@ export const UserProfileActions = () => {
refetch()
}, [location, refetch])
+ const iconBtn = (
+
+ 0 ? notificationsCount : null}
+ color="error"
+ >
+
+
+
+ )
+
return (
keycloak.authenticated && (
{
) : (
- navigate(ROUTES.NOTIFICATIONS)}
- aria-label={t('Notifications')}
- >
- 0 ? notificationsCount : null
- }
- color="error"
- >
-
-
-
+
)}
li:hover, & > li:focus, & > li:blur': {
backgroundColor: primary.light,
diff --git a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx
index 3b85bf216..f5129eb4f 100644
--- a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx
+++ b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import { useLocation, useParams } from 'react-router-dom'
+import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { FloatingAlert } from '@/components/BCAlert'
import BCBox from '@/components/BCBox'
@@ -7,14 +7,13 @@ import BCModal from '@/components/BCModal'
import BCButton from '@/components/BCButton'
import Loading from '@/components/Loading'
import { Role } from '@/components/Role'
-import { roles } from '@/constants/roles'
+import { roles, govRoles } from '@/constants/roles'
import { Fab, Stack, Tooltip } from '@mui/material'
import BCTypography from '@/components/BCTypography'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
import colors from '@/themes/base/colors.js'
-import { govRoles } from '@/constants/roles'
import { useTranslation } from 'react-i18next'
import { useCurrentUser } from '@/hooks/useCurrentUser'
import { useOrganization } from '@/hooks/useOrganization'
@@ -27,6 +26,7 @@ import { ActivityListCard } from './components/ActivityListCard'
import { AssessmentCard } from './components/AssessmentCard'
import InternalComments from '@/components/InternalComments'
import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses'
+import { ROUTES } from '@/constants/routes'
const iconStyle = {
width: '2rem',
@@ -42,6 +42,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => {
const [isSigningAuthorityDeclared, setIsSigningAuthorityDeclared] =
useState(false)
const alertRef = useRef()
+ const navigate = useNavigate()
const { compliancePeriod, complianceReportId } = useParams()
const [isScrollingUp, setIsScrollingUp] = useState(false)
@@ -64,8 +65,16 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => {
setInternalComment(newComment)
}, [])
const handleScroll = useCallback(() => {
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop
- setIsScrollingUp(scrollTop < lastScrollTop || scrollTop === 0)
+ const scrollTop = window.scrollY || document.documentElement.scrollTop
+ const scrollPosition = window.scrollY + window.innerHeight
+ const documentHeight = document.documentElement.scrollHeight
+ if (scrollTop === 0) {
+ setIsScrollingUp(false)
+ } else if (scrollPosition >= documentHeight - 10) {
+ setIsScrollingUp(true)
+ } else {
+ setIsScrollingUp(scrollTop < lastScrollTop || scrollTop === 0)
+ }
setLastScrollTop(scrollTop)
}, [lastScrollTop])
@@ -81,8 +90,9 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => {
hasRoles
} = useCurrentUser()
const isGovernmentUser = currentUser?.isGovernmentUser
- const isAnalystRole = currentUser?.roles?.some(role => role.name === roles.analyst) || false;
-
+ const isAnalystRole =
+ currentUser?.roles?.some((role) => role.name === roles.analyst) || false
+
const currentStatus = reportData?.report.currentStatus?.status
const { data: orgData, isLoading } = useOrganization(
reportData?.report.organizationId
@@ -92,9 +102,14 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => {
{
onSuccess: (response) => {
setModalData(null)
- alertRef.current?.triggerAlert({
- message: t('report:savedSuccessText'),
- severity: 'success'
+ const updatedStatus = JSON.parse(response.config.data)?.status
+ navigate(ROUTES.REPORTS, {
+ state: {
+ message: t('report:savedSuccessText', {
+ status: updatedStatus.toLowerCase().replace('return', 'returned')
+ }),
+ severity: 'success'
+ }
})
},
onError: (error) => {
@@ -117,7 +132,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => {
t,
setModalData,
updateComplianceReport,
-
+ compliancePeriod,
isGovernmentUser,
isSigningAuthorityDeclared
}),
@@ -127,7 +142,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => {
t,
setModalData,
updateComplianceReport,
-
+ compliancePeriod,
isGovernmentUser,
isSigningAuthorityDeclared
]
@@ -211,7 +226,10 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => {
{!location.state?.newReport && (
<>
-
+
{
@@ -120,9 +118,7 @@ export const buttonClusterConfigFn = ({
}
},
returnToManager: {
- ...outlinedButton(
- t('report:actionBtns.returnToManager')
- ),
+ ...outlinedButton(t('report:actionBtns.returnToManager')),
id: 'return-report-manager-btn',
handler: (formData) => {
setModalData({
@@ -138,6 +134,23 @@ export const buttonClusterConfigFn = ({
})
}
},
+ returnToSupplier: {
+ ...outlinedButton(t('report:actionBtns.returnToSupplier')),
+ id: 'return-report-supplier-btn',
+ handler: (formData) => {
+ setModalData({
+ primaryButtonAction: () =>
+ updateComplianceReport({
+ ...formData,
+ status: COMPLIANCE_REPORT_STATUSES.RETURN_TO_SUPPLIER
+ }),
+ primaryButtonText: t('report:actionBtns.returnToSupplier'),
+ secondaryButtonText: t('cancelBtn'),
+ title: t('confirmation'),
+ content: t('report:returnToSupplierConfirmText')
+ })
+ }
+ },
assessReport: {
...containedButton(t('report:actionBtns.assessReportBtn')),
id: 'assess-report-btn',
@@ -174,13 +187,21 @@ export const buttonClusterConfigFn = ({
}
}
+ const canReturnToSupplier = () => {
+ const compliancePeriodYear = parseInt(compliancePeriod)
+ const deadlineDate = new Date(compliancePeriodYear + 1, 2, 31) // Month is 0-based, so 2 = March
+ const currentDate = new Date()
+ return currentDate <= deadlineDate
+ }
+
const buttons = {
- [COMPLIANCE_REPORT_STATUSES.DRAFT]: [
- reportButtons.submitReport
- ],
+ [COMPLIANCE_REPORT_STATUSES.DRAFT]: [reportButtons.submitReport],
[COMPLIANCE_REPORT_STATUSES.SUBMITTED]: [
...(isGovernmentUser && hasRoles('Analyst')
- ? [reportButtons.recommendByAnalyst]
+ ? [
+ reportButtons.recommendByAnalyst,
+ ...(canReturnToSupplier() ? [reportButtons.returnToSupplier] : [])
+ ]
: [])
],
[COMPLIANCE_REPORT_STATUSES.RECOMMENDED_BY_ANALYST]: [