diff --git a/backend/lcfs/tests/compliance_report/conftest.py b/backend/lcfs/tests/compliance_report/conftest.py index 0699cd20..58fccd34 100644 --- a/backend/lcfs/tests/compliance_report/conftest.py +++ b/backend/lcfs/tests/compliance_report/conftest.py @@ -195,6 +195,7 @@ def _create_compliance_report_summary( version=1, is_locked=False, quarter=None, + can_sign=False ): return ComplianceReportSummarySchema( @@ -206,6 +207,7 @@ def _create_compliance_report_summary( version=version, is_locked=is_locked, quarter=quarter, + can_sign=can_sign ) return _create_compliance_report_summary diff --git a/backend/lcfs/tests/compliance_report/test_compliance_report_repo.py b/backend/lcfs/tests/compliance_report/test_compliance_report_repo.py index cf05ce02..7dda1756 100644 --- a/backend/lcfs/tests/compliance_report/test_compliance_report_repo.py +++ b/backend/lcfs/tests/compliance_report/test_compliance_report_repo.py @@ -943,7 +943,8 @@ async def test_calculate_fuel_quantities_success_not_fossil_derived( ) result = await compliance_report_repo.calculate_fuel_quantities( - compliance_report=compliance_reports[0], + compliance_report_id=compliance_reports[0].compliance_report_id, + effective_fuel_supplies=non_fossil_fuel_supplies, fossil_derived=False, ) @@ -965,7 +966,8 @@ async def test_calculate_fuel_quantities_success_fossil_derived( ) result = await compliance_report_repo.calculate_fuel_quantities( - compliance_report=compliance_reports[0], + compliance_report_id=compliance_reports[0].compliance_report_id, + effective_fuel_supplies=fossil_fuel_supplies, fossil_derived=True, ) diff --git a/backend/lcfs/tests/compliance_report/test_compliance_report_views.py b/backend/lcfs/tests/compliance_report/test_compliance_report_views.py index 54101585..9741e568 100644 --- a/backend/lcfs/tests/compliance_report/test_compliance_report_views.py +++ b/backend/lcfs/tests/compliance_report/test_compliance_report_views.py @@ -11,6 +11,7 @@ from lcfs.web.api.base import FilterModel from lcfs.web.api.compliance_report.schema import ( ComplianceReportUpdateSchema, + ComplianceReportSummaryUpdateSchema, ) from lcfs.services.s3.client import DocumentService @@ -329,6 +330,7 @@ async def test_get_compliance_report_summary_success( mock_calculate_compliance_report_summary.assert_called_once_with(1) mock_validate_organization_access.assert_called_once_with(1) + @pytest.mark.anyio async def test_get_compliance_report_summary_invalid_payload( client: AsyncClient, @@ -386,6 +388,13 @@ async def test_update_compliance_report_summary_success( set_mock_user(fastapi_app, [RoleEnum.SUPPLIER]) mock_compliance_report_summary = compliance_report_summary_schema() + request_schema = ComplianceReportSummaryUpdateSchema( + compliance_report_id=1, + renewable_fuel_target_summary=mock_compliance_report_summary.renewable_fuel_target_summary, + low_carbon_fuel_target_summary=mock_compliance_report_summary.low_carbon_fuel_target_summary, + non_compliance_penalty_summary=mock_compliance_report_summary.non_compliance_penalty_summary, + summary_id=mock_compliance_report_summary.summary_id, + ) mock_validate_organization_access.return_value = None mock_update_compliance_report_summary.return_value = ( mock_compliance_report_summary @@ -393,7 +402,7 @@ async def test_update_compliance_report_summary_success( url = fastapi_app.url_path_for("update_compliance_report_summary", report_id=1) - payload = mock_compliance_report_summary.dict(by_alias=True) + payload = request_schema.model_dump(by_alias=True) response = await client.put(url, json=payload) @@ -404,9 +413,7 @@ async def test_update_compliance_report_summary_success( ) assert response.json() == expected_response - mock_update_compliance_report_summary.assert_called_once_with( - 1, mock_compliance_report_summary - ) + mock_update_compliance_report_summary.assert_called_once_with(1, request_schema) mock_validate_organization_access.assert_called_once_with(1) @@ -507,6 +514,7 @@ async def test_update_compliance_report_success( ) mock_validate_organization_access.assert_called_once_with(1) + @pytest.mark.anyio async def test_update_compliance_report_forbidden( client: AsyncClient, diff --git a/backend/lcfs/tests/compliance_report/test_summary_service.py b/backend/lcfs/tests/compliance_report/test_summary_service.py index e64e0022..fd61bf89 100644 --- a/backend/lcfs/tests/compliance_report/test_summary_service.py +++ b/backend/lcfs/tests/compliance_report/test_summary_service.py @@ -4,7 +4,9 @@ import pytest from lcfs.db.models.compliance.ComplianceReportSummary import ComplianceReportSummary -from lcfs.web.api.compliance_report.schema import ComplianceReportSummaryRowSchema +from lcfs.web.api.compliance_report.schema import ( + ComplianceReportSummaryRowSchema, +) @pytest.mark.anyio @@ -670,3 +672,94 @@ async def test_calculate_renewable_fuel_target_summary_no_copy_lines_6_and_8( assert result[7].gasoline == 0 # Line 8 should not be copied assert result[7].diesel == 0 assert result[7].jet_fuel == 0 + + +@pytest.mark.anyio +async def test_can_sign_flag_logic( + compliance_report_summary_service, mock_repo, mock_trxn_repo +): + # Scenario 1: All conditions met + mock_effective_fuel_supplies = [MagicMock()] + mock_notional_transfers = MagicMock(notional_transfers=[MagicMock()]) + mock_fuel_exports = [MagicMock()] + mock_allocation_agreements = [MagicMock()] + mock_compliance_report = MagicMock( + compliance_report_group_uuid="mock-group-uuid", + compliance_period=MagicMock(effective_date=MagicMock(year=2024)), + organization_id=1, + compliance_report_id=1, + summary=MagicMock(is_locked=False), + ) + + mock_trxn_repo.calculate_available_balance_for_period.return_value = 1000 + + # Mock previous retained and obligation dictionaries + previous_retained = {"gasoline": 10, "diesel": 20, "jet_fuel": 30} + previous_obligation = {"gasoline": 5, "diesel": 10, "jet_fuel": 15} + + # Mock repository methods + mock_repo.get_compliance_report_by_id = AsyncMock( + return_value=mock_compliance_report + ) + mock_repo.calculate_fuel_quantities = AsyncMock( + return_value={ + "gasoline": 100, + "diesel": 50, + "jet_fuel": 25, + } + ) + mock_repo.get_assessed_compliance_report_by_period = AsyncMock( + return_value=MagicMock( + summary=MagicMock( + line_6_renewable_fuel_retained_gasoline=previous_retained["gasoline"], + line_6_renewable_fuel_retained_diesel=previous_retained["diesel"], + line_6_renewable_fuel_retained_jet_fuel=previous_retained["jet_fuel"], + line_8_obligation_deferred_gasoline=previous_obligation["gasoline"], + line_8_obligation_deferred_diesel=previous_obligation["diesel"], + line_8_obligation_deferred_jet_fuel=previous_obligation["jet_fuel"], + ) + ) + ) + + compliance_report_summary_service.fuel_supply_repo.get_effective_fuel_supplies = ( + AsyncMock(return_value=mock_effective_fuel_supplies) + ) + compliance_report_summary_service.notional_transfer_service.calculate_notional_transfers = AsyncMock( + return_value=mock_notional_transfers + ) + compliance_report_summary_service.fuel_export_repo.get_effective_fuel_exports = ( + AsyncMock(return_value=mock_fuel_exports) + ) + compliance_report_summary_service.allocation_agreement_repo.get_allocation_agreements = AsyncMock( + return_value=mock_allocation_agreements + ) + + # Call the method + result = ( + await compliance_report_summary_service.calculate_compliance_report_summary(1) + ) + + # Assert that `can_sign` is True + assert result.can_sign is True + + # Scenario 2: No conditions met + compliance_report_summary_service.fuel_supply_repo.get_effective_fuel_supplies = ( + AsyncMock(return_value=[]) + ) + compliance_report_summary_service.notional_transfer_service.calculate_notional_transfers = AsyncMock( + return_value=MagicMock(notional_transfers=[]) + ) + compliance_report_summary_service.fuel_export_repo.get_effective_fuel_exports = ( + AsyncMock(return_value=[]) + ) + compliance_report_summary_service.allocation_agreement_repo.get_allocation_agreements = AsyncMock( + return_value=[] + ) + + # Call the method again + result = ( + await compliance_report_summary_service.calculate_compliance_report_summary(1) + ) + + # Assert that `can_sign` is False + assert result.can_sign is False diff --git a/backend/lcfs/tests/compliance_report/test_update_service.py b/backend/lcfs/tests/compliance_report/test_update_service.py index 077d560b..07285f21 100644 --- a/backend/lcfs/tests/compliance_report/test_update_service.py +++ b/backend/lcfs/tests/compliance_report/test_update_service.py @@ -14,14 +14,16 @@ ComplianceReportSummaryRowSchema, ComplianceReportSummarySchema, ) -from lcfs.web.exception.exceptions import DataNotFoundException +from lcfs.web.exception.exceptions import DataNotFoundException, ServiceException + # Mock for user_has_roles function @pytest.fixture def mock_user_has_roles(): - with patch('lcfs.web.api.compliance_report.update_service.user_has_roles') as mock: + with patch("lcfs.web.api.compliance_report.update_service.user_has_roles") as mock: yield mock + # Mock for adjust_balance method within the OrganizationsService @pytest.fixture def mock_org_service(): @@ -29,6 +31,7 @@ def mock_org_service(): mock_org_service.adjust_balance = AsyncMock() # Mock the adjust_balance method return mock_org_service + # update_compliance_report @pytest.mark.anyio async def test_update_compliance_report_status_change( @@ -61,7 +64,9 @@ async def test_update_compliance_report_status_change( # Assertions assert updated_report == mock_report - mock_repo.get_compliance_report_by_id.assert_called_once_with(report_id, is_model=True) + mock_repo.get_compliance_report_by_id.assert_called_once_with( + report_id, is_model=True + ) mock_repo.get_compliance_report_status_by_desc.assert_called_once_with( report_data.status ) @@ -109,7 +114,9 @@ async def test_update_compliance_report_no_status_change( # Assertions assert updated_report == mock_report - mock_repo.get_compliance_report_by_id.assert_called_once_with(report_id, is_model=True) + mock_repo.get_compliance_report_by_id.assert_called_once_with( + report_id, is_model=True + ) mock_repo.get_compliance_report_status_by_desc.assert_called_once_with( report_data.status ) @@ -140,7 +147,10 @@ async def test_update_compliance_report_not_found( report_id, report_data ) - mock_repo.get_compliance_report_by_id.assert_called_once_with(report_id, is_model=True) + mock_repo.get_compliance_report_by_id.assert_called_once_with( + report_id, is_model=True + ) + @pytest.mark.anyio async def test_handle_submitted_status_insufficient_permissions( @@ -161,12 +171,17 @@ async def test_handle_submitted_status_insufficient_permissions( assert exc_info.value.status_code == 403 assert exc_info.value.detail == "Forbidden." + # SUBMIT STATUS TESTS @pytest.mark.anyio async def test_handle_submitted_status_with_existing_summary( - compliance_report_update_service, mock_repo, mock_user_has_roles, mock_org_service, compliance_report_summary_service + compliance_report_update_service, + mock_repo, + mock_user_has_roles, + mock_org_service, + compliance_report_summary_service, ): # Mock data report_id = 1 @@ -216,6 +231,7 @@ async def test_handle_submitted_status_with_existing_summary( line="21", field="non_compliance_penalty_payable", value=0 ), ], + can_sign=True, ) # Set up mocks @@ -236,7 +252,7 @@ async def test_handle_submitted_status_with_existing_summary( # Assertions mock_user_has_roles.assert_called_once_with( compliance_report_update_service.request.user, - [RoleEnum.SUPPLIER, RoleEnum.SIGNING_AUTHORITY] + [RoleEnum.SUPPLIER, RoleEnum.SIGNING_AUTHORITY], ) mock_repo.get_summary_by_report_id.assert_called_once_with(report_id) compliance_report_summary_service.calculate_compliance_report_summary.assert_called_once_with( @@ -260,7 +276,11 @@ async def test_handle_submitted_status_with_existing_summary( @pytest.mark.anyio async def test_handle_submitted_status_without_existing_summary( - compliance_report_update_service, mock_repo, mock_user_has_roles, mock_org_service, compliance_report_summary_service + compliance_report_update_service, + mock_repo, + mock_user_has_roles, + mock_org_service, + compliance_report_summary_service, ): # Mock data report_id = 1 @@ -307,6 +327,7 @@ async def test_handle_submitted_status_without_existing_summary( line="21", field="non_compliance_penalty_payable", value=0 ), ], + can_sign=True, ) # Set up mocks @@ -346,7 +367,11 @@ async def test_handle_submitted_status_without_existing_summary( @pytest.mark.anyio async def test_handle_submitted_status_partial_existing_values( - compliance_report_update_service, mock_repo, mock_user_has_roles, mock_org_service, compliance_report_summary_service + compliance_report_update_service, + mock_repo, + mock_user_has_roles, + mock_org_service, + compliance_report_summary_service, ): # Mock data report_id = 1 @@ -397,6 +422,7 @@ async def test_handle_submitted_status_partial_existing_values( line="21", field="non_compliance_penalty_payable", value=0 ), ], + can_sign=True, ) # Set up mocks @@ -427,7 +453,11 @@ async def test_handle_submitted_status_partial_existing_values( @pytest.mark.anyio async def test_handle_submitted_status_no_user_edits( - compliance_report_update_service, mock_repo, mock_user_has_roles, mock_org_service, compliance_report_summary_service + compliance_report_update_service, + mock_repo, + mock_user_has_roles, + mock_org_service, + compliance_report_summary_service, ): # Mock data report_id = 1 @@ -482,6 +512,7 @@ async def test_handle_submitted_status_no_user_edits( line="21", field="non_compliance_penalty_payable", value=0 ), ], + can_sign=True, ) # Set up mocks @@ -508,3 +539,46 @@ async def test_handle_submitted_status_no_user_edits( assert ( saved_summary.renewable_fuel_target_summary[2].jet_fuel == 900 ) # Used calculated value + + +@pytest.mark.anyio +async def test_handle_submitted_no_sign( + compliance_report_update_service, + mock_repo, + mock_user_has_roles, + mock_org_service, + compliance_report_summary_service, +): + # Mock data + report_id = 1 + mock_report = MagicMock(spec=ComplianceReport) + mock_report.compliance_report_id = report_id + mock_report.summary = MagicMock(spec=ComplianceReportSummary) + mock_report.summary.summary_id = 100 + # Mock user roles (user has required roles) + mock_user_has_roles.return_value = True + compliance_report_update_service.request = MagicMock() + compliance_report_update_service.request.user = MagicMock() + # Mock existing summary with no user-edited values + existing_summary = MagicMock(spec=ComplianceReportSummary) + + # Mock calculated summary + calculated_summary = ComplianceReportSummarySchema( + summary_id=100, + compliance_report_id=report_id, + renewable_fuel_target_summary=[], + low_carbon_fuel_target_summary=[], + non_compliance_penalty_summary=[], + can_sign=False, + ) + + # Set up mocks + mock_repo.get_summary_by_report_id.return_value = existing_summary + compliance_report_summary_service.calculate_compliance_report_summary = AsyncMock( + return_value=calculated_summary + ) + # Inject the mocked org_service into the service being tested + compliance_report_update_service.org_service = mock_org_service + + with pytest.raises(ServiceException): + await compliance_report_update_service.handle_submitted_status(mock_report) diff --git a/backend/lcfs/web/api/compliance_report/repo.py b/backend/lcfs/web/api/compliance_report/repo.py index f0429c58..8211b53c 100644 --- a/backend/lcfs/web/api/compliance_report/repo.py +++ b/backend/lcfs/web/api/compliance_report/repo.py @@ -30,6 +30,7 @@ from lcfs.web.api.compliance_report.schema import ( ComplianceReportBaseSchema, ComplianceReportSummarySchema, + ComplianceReportSummaryUpdateSchema, ) from lcfs.db.models.compliance.ComplianceReportHistory import ComplianceReportHistory from lcfs.web.core.decorators import repo_handler @@ -532,7 +533,7 @@ async def add_compliance_report_summary( @repo_handler async def save_compliance_report_summary( - self, summary: ComplianceReportSummarySchema + self, summary: ComplianceReportSummaryUpdateSchema ): """ Save the compliance report summary to the database. @@ -667,18 +668,16 @@ async def get_issued_compliance_units( @repo_handler async def calculate_fuel_quantities( - self, compliance_report: ComplianceReport, fossil_derived: bool + self, + compliance_report_id: int, + effective_fuel_supplies: List[FuelSupply], + fossil_derived: bool, ) -> Dict[str, float]: """ Calculate the total quantities of fuels, separated by fuel category and fossil_derived flag. """ fuel_quantities = defaultdict(float) - # Get effective fuel supplies using the updated logic - effective_fuel_supplies = await self.fuel_supply_repo.get_effective_fuel_supplies( - compliance_report_group_uuid=compliance_report.compliance_report_group_uuid - ) - # Filter fuel supplies based on fossil_derived flag filtered_fuel_supplies = [ fs @@ -706,8 +705,7 @@ async def calculate_fuel_quantities( OtherUses.fuel_category_id == FuelCategory.fuel_category_id, ) .where( - OtherUses.compliance_report_id - == compliance_report.compliance_report_id, + OtherUses.compliance_report_id == compliance_report_id, FuelType.fossil_derived.is_(fossil_derived), FuelType.other_uses_fossil_derived.is_(fossil_derived), ) @@ -738,8 +736,7 @@ async def calculate_fuel_quantities( == FuelCategory.fuel_category_id, ) .where( - AllocationAgreement.compliance_report_id - == compliance_report.compliance_report_id, + AllocationAgreement.compliance_report_id == compliance_report_id, FuelType.fossil_derived.is_(False), FuelType.other_uses_fossil_derived.is_(False), ) diff --git a/backend/lcfs/web/api/compliance_report/schema.py b/backend/lcfs/web/api/compliance_report/schema.py index ee66da92..d427ee7d 100644 --- a/backend/lcfs/web/api/compliance_report/schema.py +++ b/backend/lcfs/web/api/compliance_report/schema.py @@ -2,7 +2,7 @@ from typing import ClassVar, Optional, List, Union from datetime import datetime, date from enum import Enum -from lcfs.web.api.fuel_code.schema import EndUseTypeSchema,EndUserTypeSchema +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 @@ -26,10 +26,12 @@ class ReportingFrequency(str, Enum): ANNUAL = "Annual" QUARTERLY = "Quarterly" + class PortsEnum(str, Enum): SINGLE = "Single port" DUAL = "Dual port" + class CompliancePeriodSchema(BaseSchema): compliance_period_id: int description: str @@ -185,6 +187,7 @@ class ComplianceReportSummarySchema(BaseSchema): renewable_fuel_target_summary: List[ComplianceReportSummaryRowSchema] low_carbon_fuel_target_summary: List[ComplianceReportSummaryRowSchema] non_compliance_penalty_summary: List[ComplianceReportSummaryRowSchema] + can_sign: bool = False summary_id: Optional[int] = None compliance_report_id: Optional[int] = None version: Optional[int] = None @@ -192,6 +195,14 @@ class ComplianceReportSummarySchema(BaseSchema): quarter: Optional[int] = None +class ComplianceReportSummaryUpdateSchema(BaseSchema): + compliance_report_id: int + renewable_fuel_target_summary: List[ComplianceReportSummaryRowSchema] + low_carbon_fuel_target_summary: List[ComplianceReportSummaryRowSchema] + non_compliance_penalty_summary: List[ComplianceReportSummaryRowSchema] + summary_id: int + + class CommonPaginatedReportRequestSchema(BaseSchema): compliance_report_id: int = Field(..., alias="complianceReportId") filters: Optional[List[FilterModel]] = None diff --git a/backend/lcfs/web/api/compliance_report/summary_service.py b/backend/lcfs/web/api/compliance_report/summary_service.py index a5f03a00..4d952153 100644 --- a/backend/lcfs/web/api/compliance_report/summary_service.py +++ b/backend/lcfs/web/api/compliance_report/summary_service.py @@ -9,6 +9,7 @@ from lcfs.db.models.compliance.ComplianceReport import ComplianceReport from lcfs.db.models.compliance.ComplianceReportSummary import ComplianceReportSummary +from lcfs.web.api.allocation_agreement.repo import AllocationAgreementRepository from lcfs.web.api.compliance_report.constants import ( RENEWABLE_FUEL_TARGET_DESCRIPTIONS, LOW_CARBON_FUEL_TARGET_DESCRIPTIONS, @@ -20,6 +21,7 @@ from lcfs.web.api.compliance_report.schema import ( ComplianceReportSummaryRowSchema, ComplianceReportSummarySchema, + ComplianceReportSummaryUpdateSchema, ) from lcfs.web.api.fuel_export.repo import FuelExportRepository from lcfs.web.api.fuel_supply.repo import FuelSupplyRepository @@ -42,12 +44,16 @@ def __init__( ), fuel_supply_repo: FuelSupplyRepository = Depends(FuelSupplyRepository), fuel_export_repo: FuelExportRepository = Depends(FuelExportRepository), + allocation_agreement_repo: AllocationAgreementRepository = Depends( + AllocationAgreementRepository + ), ): self.repo = repo self.notional_transfer_service = notional_transfer_service self.trxn_repo = trxn_repo self.fuel_supply_repo = fuel_supply_repo self.fuel_export_repo = fuel_export_repo + self.allocation_agreement_repo = allocation_agreement_repo def convert_summary_to_dict( self, summary_obj: ComplianceReportSummary @@ -65,6 +71,7 @@ def convert_summary_to_dict( renewable_fuel_target_summary=[], low_carbon_fuel_target_summary=[], non_compliance_penalty_summary=[], + can_sign=False, ) for column in inspector.mapper.column_attrs: match = re.search(r"line_(\d+)_", column.key) @@ -218,7 +225,7 @@ def convert_summary_to_dict( async def update_compliance_report_summary( self, report_id: int, - summary_data: ComplianceReportSummarySchema, + summary_data: ComplianceReportSummaryUpdateSchema, ) -> ComplianceReportSummarySchema: """ Autosave compliance report summary details for a specific summary by ID. @@ -255,6 +262,7 @@ async def calculate_compliance_report_summary( # After the report has been submitted, the summary becomes locked # so we can return the existing summary rather than re-calculating if summary_model.is_locked: + print("LOCKED") return self.convert_summary_to_dict(compliance_report.summary) compliance_period_start = compliance_report.compliance_period.effective_date @@ -307,12 +315,21 @@ async def calculate_compliance_report_summary( elif transfer.received_or_transferred.lower() == "transferred": notional_transfers_sums[normalized_category] -= transfer.quantity + # Get effective fuel supplies using the updated logic + effective_fuel_supplies = await self.fuel_supply_repo.get_effective_fuel_supplies( + compliance_report_group_uuid=compliance_report.compliance_report_group_uuid + ) + # Fetch fuel quantities fossil_quantities = await self.repo.calculate_fuel_quantities( - compliance_report, fossil_derived=True + compliance_report.compliance_report_id, + effective_fuel_supplies, + fossil_derived=True, ) renewable_quantities = await self.repo.calculate_fuel_quantities( - compliance_report, fossil_derived=False + compliance_report.compliance_report_id, + effective_fuel_supplies, + fossil_derived=False, ) renewable_fuel_target_summary = self.calculate_renewable_fuel_target_summary( @@ -338,12 +355,30 @@ async def calculate_compliance_report_summary( existing_summary = self.convert_summary_to_dict(summary_model) + fuel_export_records = await self.fuel_export_repo.get_effective_fuel_exports( + compliance_report.compliance_report_group_uuid + ) + + allocation_agreements = ( + await self.allocation_agreement_repo.get_allocation_agreements( + compliance_report_id=compliance_report.compliance_report_id + ) + ) + + can_sign = ( + len(effective_fuel_supplies) > 0 + or len(notional_transfers.notional_transfers) > 0 + or len(fuel_export_records) > 0 + or len(allocation_agreements) > 0 + ) + summary = self.map_to_schema( compliance_report, renewable_fuel_target_summary, low_carbon_fuel_target_summary, non_compliance_penalty_summary, summary_model, + can_sign, ) # Only save if summary has changed @@ -361,6 +396,7 @@ def map_to_schema( low_carbon_fuel_target_summary, non_compliance_penalty_summary, summary_model, + can_sign, ): summary = ComplianceReportSummarySchema( summary_id=summary_model.summary_id, @@ -371,6 +407,7 @@ def map_to_schema( renewable_fuel_target_summary=renewable_fuel_target_summary, low_carbon_fuel_target_summary=low_carbon_fuel_target_summary, non_compliance_penalty_summary=non_compliance_penalty_summary, + can_sign=can_sign, ) return summary diff --git a/backend/lcfs/web/api/compliance_report/update_service.py b/backend/lcfs/web/api/compliance_report/update_service.py index 4754ba34..dd79fc6a 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 @@ from fastapi import Depends, HTTPException, Request +from sqlalchemy.exc import InvalidRequestError from lcfs.db.models.compliance.ComplianceReport import ComplianceReport from lcfs.db.models.compliance.ComplianceReportStatus import ComplianceReportStatusEnum @@ -117,6 +118,9 @@ async def handle_submitted_status(self, report: ComplianceReport): ) ) + if not calculated_summary.can_sign: + raise ServiceException("ComplianceReportSummary is not able to be signed") + # If there's an existing summary, preserve user-edited values if existing_summary: for row in calculated_summary.renewable_fuel_target_summary: @@ -177,12 +181,15 @@ async def handle_submitted_status(self, report: ComplianceReport): ) # Update the report with the new summary report.summary = new_summary - # 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.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, + ) await self.repo.update_compliance_report(report) return calculated_summary @@ -213,9 +220,11 @@ async def handle_assessed_status(self, report: ComplianceReport): ) if not has_director_role: raise HTTPException(status_code=403, detail="Forbidden.") - # Update the transaction to assessed - report.transaction.transaction_action = TransactionActionEnum.Adjustment - report.transaction.update_user = user.keycloak_username + + if report.transaction: + # Update the transaction to assessed + report.transaction.transaction_action = TransactionActionEnum.Adjustment + report.transaction.update_user = user.keycloak_username await self.repo.update_compliance_report(report) async def handle_reassessed_status(self, report: ComplianceReport): diff --git a/backend/lcfs/web/api/compliance_report/views.py b/backend/lcfs/web/api/compliance_report/views.py index 07875423..688f1788 100644 --- a/backend/lcfs/web/api/compliance_report/views.py +++ b/backend/lcfs/web/api/compliance_report/views.py @@ -25,7 +25,7 @@ ComplianceReportBaseSchema, ComplianceReportListSchema, ComplianceReportSummarySchema, - ComplianceReportUpdateSchema, + ComplianceReportUpdateSchema, ComplianceReportSummaryUpdateSchema, ) from lcfs.web.api.compliance_report.services import ComplianceReportServices from lcfs.web.api.compliance_report.summary_service import ( @@ -120,7 +120,7 @@ async def get_compliance_report_summary( async def update_compliance_report_summary( request: Request, report_id: int, - summary_data: ComplianceReportSummarySchema, + summary_data: ComplianceReportSummaryUpdateSchema, summary_service: ComplianceReportSummaryService = Depends(), validate: ComplianceReportValidation = Depends(), ) -> ComplianceReportSummarySchema: diff --git a/frontend/src/assets/locales/en/reports.json b/frontend/src/assets/locales/en/reports.json index ead17a17..572809e7 100644 --- a/frontend/src/assets/locales/en/reports.json +++ b/frontend/src/assets/locales/en/reports.json @@ -126,6 +126,7 @@ "compareReports": "Compare summary data between the following 2 report versions", "fuelType": "Fuel Type", "signingAuthorityDeclaration": "Signing authority declaration", + "cannotSubmit": "You must report fuel activity information to sign and submit a report (FSE data alone is not sufficient)", "declarationText": "I certify that the information in this report is true and complete to the best of my knowledge and I understand that I may be required to provide to the Director records evidencing the truth of that information.", "submitReport": "Submit Report", "pleaseCheckDeclaration": "Please certify the information before submitting", diff --git a/frontend/src/views/ComplianceReports/components/ComplianceReportSummary.jsx b/frontend/src/views/ComplianceReports/components/ComplianceReportSummary.jsx index a752d983..76909078 100644 --- a/frontend/src/views/ComplianceReports/components/ComplianceReportSummary.jsx +++ b/frontend/src/views/ComplianceReports/components/ComplianceReportSummary.jsx @@ -36,6 +36,7 @@ const ComplianceReportSummary = ({ alertRef }) => { const [summaryData, setSummaryData] = useState(null) + const [canSign, setCanSign] = useState(false) const { t } = useTranslation(['report']) const { data, isLoading, isError, error } = @@ -63,9 +64,13 @@ const ComplianceReportSummary = ({ data?.nonCompliancePenaltySummary[0]?.totalValue <= 0 || data?.nonCompliancePenaltySummary[1].totalValue <= 0 ) + setCanSign(data && data.canSign) } if (isError) { - alertRef.current?.triggerAlert({ message: error?.response?.data?.detail || error.message, severity: 'error' }) + alertRef.current?.triggerAlert({ + message: error?.response?.data?.detail || error.message, + severity: 'error' + }) } }, [alertRef, data, error, isError, setHasMet]) @@ -133,6 +138,7 @@ const ComplianceReportSummary = ({ {currentStatus === COMPLIANCE_REPORT_STATUSES.DRAFT && ( <> diff --git a/frontend/src/views/ComplianceReports/components/SigningAuthorityDeclaration.jsx b/frontend/src/views/ComplianceReports/components/SigningAuthorityDeclaration.jsx index 8375a413..b285ac0a 100644 --- a/frontend/src/views/ComplianceReports/components/SigningAuthorityDeclaration.jsx +++ b/frontend/src/views/ComplianceReports/components/SigningAuthorityDeclaration.jsx @@ -2,8 +2,10 @@ import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Checkbox, FormControlLabel, Paper } from '@mui/material' import BCTypography from '@/components/BCTypography' +import BCAlert from '@/components/BCAlert' +import Box from '@mui/material/Box' -const SigningAuthorityDeclaration = ({ onChange }) => { +const SigningAuthorityDeclaration = ({ onChange, disabled }) => { const { t } = useTranslation(['report']) const [checked, setChecked] = useState(false) @@ -28,9 +30,17 @@ const SigningAuthorityDeclaration = ({ onChange }) => { {t('report:signingAuthorityDeclaration')} + {disabled && ( + + + {t('report:cannotSubmit')} + + + )}