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')}
+
+
+ )}