Skip to content

Commit

Permalink
Feat: LCFS - Add a Return report to the supplier button #1507
Browse files Browse the repository at this point in the history
  • Loading branch information
prv-proton committed Dec 23, 2024
1 parent ec4fba4 commit 6f383dd
Show file tree
Hide file tree
Showing 14 changed files with 195 additions and 91 deletions.
6 changes: 0 additions & 6 deletions backend/lcfs/db/models/compliance/listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
12 changes: 11 additions & 1 deletion backend/lcfs/web/api/compliance_report/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 16 additions & 3 deletions backend/lcfs/web/api/compliance_report/schema.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -45,7 +58,7 @@ class SummarySchema(BaseSchema):
is_locked: bool

class Config:
extra = Extra.allow
extra = 'allow'


class ComplianceReportStatusSchema(BaseSchema):
Expand Down
84 changes: 57 additions & 27 deletions backend/lcfs/web/api/compliance_report/update_service.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
)
Expand All @@ -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):
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions backend/lcfs/web/api/notification/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
}


Expand Down
4 changes: 3 additions & 1 deletion frontend/src/assets/locales/en/reports.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,17 @@ function DefaultNavbarLink({
onMouseLeave={() => setHover(false)}
onClick={onClick}
>
{icon && (
{icon && typeof icon === 'string' ? (
<Icon
sx={{
color: ({ palette: { secondary, primary } }) =>
light ? primary.main : secondary.main,
color: '#fff',
verticalAlign: 'middle'
}}
>
{icon}
</Icon>
) : (
<>{icon}</>
)}
<BCTypography
variant="body2"
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/constants/statuses.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export const COMPLIANCE_REPORT_STATUSES = {
RECOMMENDED_BY_ANALYST: 'Recommended by analyst',
RECOMMENDED_BY_MANAGER: 'Recommended by manager',
RETURN_TO_ANALYST: 'Return to analyst',
RETURN_TO_MANAGER: 'Return to manager'
RETURN_TO_MANAGER: 'Return to manager',
RETURN_TO_SUPPLIER: 'Return to supplier'
}
export function getAllOrganizationStatuses() {
return Object.values(ORGANIZATION_STATUSES)
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/hooks/useComplianceReports.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export const useUpdateComplianceReport = (reportID, options) => {
},
onSettled: () => {
queryClient.invalidateQueries(['compliance-report', reportID])
queryClient.invalidateQueries(['compliance-report-summary', reportID])
queryClient.invalidateQueries(['compliance-reports'])
}
})
}
Expand Down
42 changes: 25 additions & 17 deletions frontend/src/layouts/MainLayout/components/UserProfileActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand All @@ -44,6 +44,22 @@ export const UserProfileActions = () => {
refetch()
}, [location, refetch])

const iconBtn = (
<IconButton
color="inherit"
className="small-icon"
sx={{ mx: 1 }}
aria-label={t('Notifications')}
>
<Badge
badgeContent={notificationsCount > 0 ? notificationsCount : null}
color="error"
>
<NotificationsIcon />
</Badge>
</IconButton>
)

return (
keycloak.authenticated && (
<BCBox
Expand Down Expand Up @@ -87,21 +103,13 @@ export const UserProfileActions = () => {
<CircularProgress size={24} sx={{ color: '#fff', mx: 2 }} />
) : (
<Tooltip title={t('Notifications')}>
<IconButton
color="inherit"
sx={{ mx: 1 }}
onClick={() => navigate(ROUTES.NOTIFICATIONS)}
aria-label={t('Notifications')}
>
<Badge
badgeContent={
notificationsCount > 0 ? notificationsCount : null
}
color="error"
>
<NotificationsIcon />
</Badge>
</IconButton>
<DefaultNavbarLink
icon={iconBtn}
name={''}
route={ROUTES.NOTIFICATIONS}
light={false}
isMobileView={false}
/>
</Tooltip>
)}
<Divider
Expand Down
Loading

0 comments on commit 6f383dd

Please sign in to comment.