diff --git a/backend/lcfs/db/migrations/versions/2024-12-17-23-58_851e09cf8661.py b/backend/lcfs/db/migrations/versions/2024-12-17-23-58_851e09cf8661.py
new file mode 100644
index 000000000..e7f10a2c3
--- /dev/null
+++ b/backend/lcfs/db/migrations/versions/2024-12-17-23-58_851e09cf8661.py
@@ -0,0 +1,62 @@
+"""Add Default CI to Categories
+
+Revision ID: 851e09cf8661
+Revises: 5b374dd97469
+Create Date: 2024-12-17 23:58:07.462215
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "851e09cf8661"
+down_revision = "5b374dd97469"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column(
+ "fuel_category",
+ sa.Column(
+ "default_carbon_intensity",
+ sa.Numeric(precision=10, scale=2),
+ nullable=True,
+ comment="Default carbon intensity of the fuel category",
+ ),
+ )
+
+ # Populate default values for existing records
+ op.execute(
+ """
+ UPDATE "fuel_category" SET "default_carbon_intensity" = 88.83 WHERE "description" = 'Jet fuel';
+ """
+ )
+ op.execute(
+ """
+ UPDATE "fuel_category" SET "default_carbon_intensity" = 100.21 WHERE "description" = 'Diesel';
+ """
+ )
+ op.execute(
+ """
+ UPDATE "fuel_category" SET "default_carbon_intensity" = 93.67 WHERE "description" = 'Gasoline';
+ """
+ )
+
+ # Now set the column to NOT NULL after populating defaults
+ op.alter_column(
+ "fuel_category",
+ "default_carbon_intensity",
+ existing_type=sa.Numeric(precision=10, scale=2),
+ nullable=False,
+ )
+
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column("fuel_category", "default_carbon_intensity")
+ # ### end Alembic commands ###
diff --git a/backend/lcfs/db/models/fuel/FuelCategory.py b/backend/lcfs/db/models/fuel/FuelCategory.py
index f06dd753a..f4d3f0791 100644
--- a/backend/lcfs/db/models/fuel/FuelCategory.py
+++ b/backend/lcfs/db/models/fuel/FuelCategory.py
@@ -1,4 +1,4 @@
-from sqlalchemy import Column, Integer, Text, Enum
+from sqlalchemy import Column, Integer, Text, Enum, Float, Numeric
from lcfs.db.base import BaseModel, Auditable, DisplayOrder, EffectiveDates
from sqlalchemy.orm import relationship
@@ -25,7 +25,14 @@ class FuelCategory(BaseModel, Auditable, DisplayOrder, EffectiveDates):
nullable=False,
comment="Name of the fuel category",
)
- description = Column(Text, nullable=True, comment="Description of the fuel categor")
+ description = Column(
+ Text, nullable=True, comment="Description of the fuel category"
+ )
+ default_carbon_intensity = Column(
+ Numeric(10, 2),
+ nullable=False,
+ comment="Default carbon intensity of the fuel category",
+ )
energy_effectiveness_ratio = relationship("EnergyEffectivenessRatio")
target_carbon_intensities = relationship(
diff --git a/backend/lcfs/db/seeders/common/seed_fuel_data.json b/backend/lcfs/db/seeders/common/seed_fuel_data.json
index ac68f21d7..c80c5f972 100644
--- a/backend/lcfs/db/seeders/common/seed_fuel_data.json
+++ b/backend/lcfs/db/seeders/common/seed_fuel_data.json
@@ -249,17 +249,20 @@
{
"fuel_category_id": 1,
"category": "Gasoline",
- "description": "Gasoline"
+ "description": "Gasoline",
+ "default_carbon_intensity": 93.67
},
{
"fuel_category_id": 2,
"category": "Diesel",
- "description": "Diesel"
+ "description": "Diesel",
+ "default_carbon_intensity": 100.21
},
{
"fuel_category_id": 3,
"category": "Jet fuel",
- "description": "Jet fuel"
+ "description": "Jet fuel",
+ "default_carbon_intensity": 88.83
}
],
"end_use_types": [
@@ -1092,4 +1095,4 @@
"display_order": 4
}
]
-}
\ No newline at end of file
+}
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 84ed2520b..fdce6f9ee 100644
--- a/backend/lcfs/tests/compliance_report/test_compliance_report_repo.py
+++ b/backend/lcfs/tests/compliance_report/test_compliance_report_repo.py
@@ -48,8 +48,12 @@ async def expected_uses(dbsession):
@pytest.fixture
async def fuel_categories(dbsession):
fuel_categories = [
- FuelCategory(fuel_category_id=998, category="Gasoline"),
- FuelCategory(fuel_category_id=999, category="Diesel"),
+ FuelCategory(
+ fuel_category_id=998, category="Gasoline", default_carbon_intensity=0
+ ),
+ FuelCategory(
+ fuel_category_id=999, category="Diesel", default_carbon_intensity=0
+ ),
]
dbsession.add_all(fuel_categories)
diff --git a/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py b/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py
index 91b3d6b09..99bf2341d 100644
--- a/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py
+++ b/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py
@@ -145,7 +145,9 @@ async def test_get_fuel_type_by_id_not_found(fuel_code_repo, mock_db):
@pytest.mark.anyio
async def test_get_fuel_categories(fuel_code_repo, mock_db):
- mock_fc = FuelCategory(fuel_category_id=1, category="Renewable")
+ mock_fc = FuelCategory(
+ fuel_category_id=1, category="Renewable", default_carbon_intensity=0
+ )
mock_result = MagicMock()
mock_result.scalars.return_value.all.return_value = [mock_fc]
mock_db.execute.return_value = mock_result
@@ -157,12 +159,14 @@ async def test_get_fuel_categories(fuel_code_repo, mock_db):
@pytest.mark.anyio
async def test_get_fuel_category_by_name(fuel_code_repo, mock_db):
- mock_fc = FuelCategory(fuel_category_id=2, category="Fossil")
+ mock_fc = FuelCategory(
+ fuel_category_id=2, category="Fossil", default_carbon_intensity=0
+ )
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = mock_fc
mock_db.execute.return_value = mock_result
- result = await fuel_code_repo.get_fuel_category_by_name("Fossil")
+ result = await fuel_code_repo.get_fuel_category_by(category="Fossil")
assert result == mock_fc
@@ -646,6 +650,88 @@ async def test_get_standardized_fuel_data(fuel_code_repo, mock_db):
assert result.uci == 5.0
+@pytest.mark.anyio
+async def test_get_standardized_fuel_data_unrecognized(fuel_code_repo, mock_db):
+ # Mock an unrecognized fuel type
+ mock_fuel_type = FuelType(
+ fuel_type_id=1,
+ fuel_type="UnknownFuel",
+ default_carbon_intensity=None,
+ unrecognized=True,
+ )
+
+ # Mock a fuel category with a default CI
+ mock_fuel_category = FuelCategory(
+ fuel_category_id=2, category="SomeCategory", default_carbon_intensity=93.67
+ )
+
+ # The repo uses get_one to get the fuel type.
+ mock_db.get_one.return_value = mock_fuel_type
+
+ # Mock the repo method to get the fuel category
+ fuel_code_repo.get_fuel_category_by = AsyncMock(return_value=mock_fuel_category)
+
+ # Setup side effects for subsequent queries:
+ # Energy Density
+ energy_density_result = MagicMock(
+ scalars=MagicMock(
+ return_value=MagicMock(
+ first=MagicMock(return_value=EnergyDensity(density=35.0))
+ )
+ )
+ )
+ # EER
+ eer_result = MagicMock(
+ scalars=MagicMock(
+ return_value=MagicMock(
+ first=MagicMock(return_value=EnergyEffectivenessRatio(ratio=2.0))
+ )
+ )
+ )
+ # Target Carbon Intensities
+ tci_result = MagicMock(
+ scalars=MagicMock(
+ return_value=MagicMock(
+ all=MagicMock(
+ return_value=[TargetCarbonIntensity(target_carbon_intensity=50.0)]
+ )
+ )
+ )
+ )
+ # Additional Carbon Intensity
+ aci_result = MagicMock(
+ scalars=MagicMock(
+ return_value=MagicMock(
+ one_or_none=MagicMock(
+ return_value=AdditionalCarbonIntensity(intensity=5.0)
+ )
+ )
+ )
+ )
+
+ # Set the side_effect for the mock_db.execute calls in the order they're invoked
+ mock_db.execute.side_effect = [
+ energy_density_result,
+ eer_result,
+ tci_result,
+ aci_result,
+ ]
+
+ result = await fuel_code_repo.get_standardized_fuel_data(
+ fuel_type_id=1, fuel_category_id=2, end_use_id=3, compliance_period="2024"
+ )
+
+ # Since fuel_type is unrecognized, it should use the FuelCategory's default CI
+ assert result.effective_carbon_intensity == 93.67
+ assert result.target_ci == 50.0
+ assert result.eer == 2.0
+ assert result.energy_density == 35.0
+ assert result.uci == 5.0
+
+ # Ensure get_fuel_category_by was called once with the correct parameter
+ fuel_code_repo.get_fuel_category_by.assert_awaited_once_with(fuel_category_id=2)
+
+
@pytest.mark.anyio
async def test_get_additional_carbon_intensity(fuel_code_repo, mock_db):
aci = AdditionalCarbonIntensity(additional_uci_id=1, intensity=10.0)
diff --git a/backend/lcfs/web/api/allocation_agreement/services.py b/backend/lcfs/web/api/allocation_agreement/services.py
index 31aa4ffdf..59ada9e41 100644
--- a/backend/lcfs/web/api/allocation_agreement/services.py
+++ b/backend/lcfs/web/api/allocation_agreement/services.py
@@ -49,8 +49,8 @@ async def convert_to_model(
allocation_agreement.allocation_transaction_type
)
)
- fuel_category = await self.fuel_repo.get_fuel_category_by_name(
- allocation_agreement.fuel_category
+ fuel_category = await self.fuel_repo.get_fuel_category_by(
+ category=allocation_agreement.fuel_category
)
fuel_type = await self.fuel_repo.get_fuel_type_by_name(
allocation_agreement.fuel_type
@@ -226,8 +226,8 @@ async def update_allocation_agreement(
!= allocation_agreement_data.fuel_category
):
existing_allocation_agreement.fuel_category = (
- await self.fuel_repo.get_fuel_category_by_name(
- allocation_agreement_data.fuel_category
+ await self.fuel_repo.get_fuel_category_by(
+ category=allocation_agreement_data.fuel_category
)
)
diff --git a/backend/lcfs/web/api/fuel_code/repo.py b/backend/lcfs/web/api/fuel_code/repo.py
index 594bd156f..325c3579b 100644
--- a/backend/lcfs/web/api/fuel_code/repo.py
+++ b/backend/lcfs/web/api/fuel_code/repo.py
@@ -1,10 +1,10 @@
+from dataclasses import dataclass
from datetime import date
from typing import List, Dict, Any, Union, Optional, Sequence
import structlog
from fastapi import Depends
from sqlalchemy import and_, or_, select, func, text, update, distinct, desc, asc
-from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, contains_eager
@@ -33,7 +33,6 @@
)
from lcfs.web.api.fuel_code.schema import FuelCodeCloneSchema, FuelCodeSchema
from lcfs.web.core.decorators import repo_handler
-from dataclasses import dataclass
logger = structlog.get_logger(__name__)
@@ -175,9 +174,9 @@ async def get_fuel_categories(self) -> List[FuelCategory]:
return (await self.db.execute(select(FuelCategory))).scalars().all()
@repo_handler
- async def get_fuel_category_by_name(self, name: str) -> FuelCategory:
- """Get a fuel category by its name"""
- result = await self.db.execute(select(FuelCategory).filter_by(category=name))
+ async def get_fuel_category_by(self, **filters: Any) -> FuelCategory:
+ """Get a fuel category by any filters"""
+ result = await self.db.execute(select(FuelCategory).filter_by(**filters))
return result.scalar_one_or_none()
@repo_handler
@@ -861,6 +860,12 @@ async def get_standardized_fuel_data(
if fuel_code_id:
fuel_code = await self.get_fuel_code(fuel_code_id)
effective_carbon_intensity = fuel_code.carbon_intensity
+ # Other Fuel uses the Default CI of the Category
+ elif fuel_type.unrecognized:
+ fuel_category = await self.get_fuel_category_by(
+ fuel_category_id=fuel_category_id
+ )
+ effective_carbon_intensity = fuel_category.default_carbon_intensity
else:
effective_carbon_intensity = fuel_type.default_carbon_intensity
diff --git a/frontend/src/components/BCAlert/BCAlert2.jsx b/frontend/src/components/BCAlert/BCAlert2.jsx
index e7b1d684d..fd92913ab 100644
--- a/frontend/src/components/BCAlert/BCAlert2.jsx
+++ b/frontend/src/components/BCAlert/BCAlert2.jsx
@@ -70,6 +70,9 @@ export const BCAlert2 = forwardRef(
setSeverity(severity)
setMessage(message)
setTriggerCount((prevCount) => prevCount + 1)
+ },
+ clearAlert: () => {
+ setAlertStatus('fadeOut')
}
}))
diff --git a/frontend/src/components/BCDataGrid/BCGridEditor.jsx b/frontend/src/components/BCDataGrid/BCGridEditor.jsx
index 03c9b4649..7482add28 100644
--- a/frontend/src/components/BCDataGrid/BCGridEditor.jsx
+++ b/frontend/src/components/BCDataGrid/BCGridEditor.jsx
@@ -181,6 +181,7 @@ export const BCGridEditor = ({
params.event.target.dataset.action &&
onAction
) {
+ alertRef.current.clearAlert()
const action = params.event.target.dataset.action
const transaction = await onAction(action, params)
@@ -207,6 +208,7 @@ export const BCGridEditor = ({
const handleAddRowsInternal = useCallback(
async (numRows) => {
+ alertRef.current.clearAlert()
let newRows = []
if (onAction) {
diff --git a/frontend/src/views/ComplianceReports/components/ActivityLinkList.jsx b/frontend/src/views/ComplianceReports/components/ActivityLinkList.jsx
index c968f8e4a..dc525b791 100644
--- a/frontend/src/views/ComplianceReports/components/ActivityLinkList.jsx
+++ b/frontend/src/views/ComplianceReports/components/ActivityLinkList.jsx
@@ -22,30 +22,33 @@ export const ActivityLinksList = () => {
compliancePeriod
).replace(':complianceReportId', complianceReportId)
)
- }
- },
- {
- name: t('report:activityLists.finalSupplyEquipment'),
- action: () => {
- navigate(
- ROUTES.REPORTS_ADD_FINAL_SUPPLY_EQUIPMENTS.replace(
- ':compliancePeriod',
- compliancePeriod
- ).replace(':complianceReportId', complianceReportId)
- )
- }
- },
- {
- name: t('report:activityLists.allocationAgreements'),
- action: () => {
- navigate(
- ROUTES.REPORTS_ADD_ALLOCATION_AGREEMENTS.replace(
- ':compliancePeriod',
- compliancePeriod
- ).replace(':complianceReportId', complianceReportId)
- )
- }
+ },
+ children: [
+ {
+ name: t('report:activityLists.finalSupplyEquipment'),
+ action: () => {
+ navigate(
+ ROUTES.REPORTS_ADD_FINAL_SUPPLY_EQUIPMENTS.replace(
+ ':compliancePeriod',
+ compliancePeriod
+ ).replace(':complianceReportId', complianceReportId)
+ )
+ }
+ },
+ {
+ name: t('report:activityLists.allocationAgreements'),
+ action: () => {
+ navigate(
+ ROUTES.REPORTS_ADD_ALLOCATION_AGREEMENTS.replace(
+ ':compliancePeriod',
+ compliancePeriod
+ ).replace(':complianceReportId', complianceReportId)
+ )
+ }
+ }
+ ]
},
+
{
name: t('report:activityLists.notionalTransfers'),
action: () => {
@@ -89,24 +92,59 @@ export const ActivityLinksList = () => {
sx={{ maxWidth: '100%', listStyleType: 'disc' }}
>
{activityList.map((activity, index) => (
-
-
+
- {activity.name}
-
-
+
+ {activity.name}
+
+
+ {activity.children && (
+
+ {activity.children.map((activity, index) => (
+
+
+ {activity.name}
+
+
+ ))}
+
+ )}
+ >
))}
)
diff --git a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx
index 4badf3167..9d3ae3134 100644
--- a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx
+++ b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx
@@ -64,24 +64,30 @@ export const AddEditFuelSupplies = () => {
severity: location.state.severity || 'info'
})
}
- }, [location?.state?.message, location?.state?.severity]);
+ }, [location?.state?.message, location?.state?.severity])
- const validate = (params, validationFn, errorMessage, alertRef, field = null) => {
- const value = field ? params.node?.data[field] : params;
+ const validate = (
+ params,
+ validationFn,
+ errorMessage,
+ alertRef,
+ field = null
+ ) => {
+ const value = field ? params.node?.data[field] : params
if (field && params.colDef.field !== field) {
- return true;
+ return true
}
if (!validationFn(value)) {
alertRef.current?.triggerAlert({
message: errorMessage,
- severity: 'error',
- });
- return false;
+ severity: 'error'
+ })
+ return false
}
- return true; // Proceed with the update
- };
+ return true // Proceed with the update
+ }
const onGridReady = useCallback(
async (params) => {
@@ -172,12 +178,12 @@ export const AddEditFuelSupplies = () => {
const isValid = validate(
params,
(value) => {
- return value !== null && !isNaN(value) && value > 0;
+ return value !== null && !isNaN(value) && value > 0
},
'Quantity supplied must be greater than 0.',
alertRef,
- 'quantity',
- );
+ 'quantity'
+ )
if (!isValid) {
return
diff --git a/frontend/src/views/OtherUses/OtherUsesSummary.jsx b/frontend/src/views/OtherUses/OtherUsesSummary.jsx
index 7c8020739..797ff8476 100644
--- a/frontend/src/views/OtherUses/OtherUsesSummary.jsx
+++ b/frontend/src/views/OtherUses/OtherUsesSummary.jsx
@@ -109,6 +109,7 @@ export const OtherUsesSummary = ({ data }) => {
gridKey={'other-uses'}
getRowId={getRowId}
columnDefs={columns}
+ defaultColDef={{ filter: false, floatingFilter: false }}
query={useGetOtherUses}
queryParams={{ complianceReportId }}
dataKey={'otherUses'}