Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions backend/lcfs/web/api/charging_equipment/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
SpreadsheetColumn("Status", "text"),
SpreadsheetColumn("Site name", "text"),
SpreadsheetColumn("Organization", "text"),
SpreadsheetColumn("Allocating organization", "text"),
SpreadsheetColumn("Registration #", "text"),
SpreadsheetColumn("Version #", "int"),
SpreadsheetColumn("Serial #", "text"),
Expand All @@ -58,6 +59,7 @@
CE_MANAGE_EXPORT_COLUMNS = [
SpreadsheetColumn("Status", "text"),
SpreadsheetColumn("Site name", "text"),
SpreadsheetColumn("Allocating organization", "text"),
SpreadsheetColumn("Registration #", "text"),
SpreadsheetColumn("Version #", "int"),
SpreadsheetColumn("Serial #", "text"),
Expand Down Expand Up @@ -375,9 +377,15 @@ async def export_filtered(
equipment.update_date.date() if getattr(equipment, "update_date", None) else None
)

allocating_organization_name = (
equipment.charging_site.allocating_organization_name
if equipment.charging_site
else ""
) or ""
common_values = [
status,
site_name,
allocating_organization_name,
registration_number,
equipment.version,
equipment.serial_number,
Expand All @@ -398,6 +406,7 @@ async def export_filtered(
status,
site_name,
organization_name,
allocating_organization_name,
registration_number,
equipment.version,
equipment.serial_number,
Expand Down
11 changes: 8 additions & 3 deletions backend/lcfs/web/api/charging_site/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,16 @@ async def export(
async def _create_validators(self, organization, builder):
validators: List[DataValidation] = []

# Get allocating organization options (from allocation agreements)
allocating_org_options = await self.repo.get_allocation_agreement_organizations(
all_names = await self.repo.get_allocating_organization_names(
organization.organization_id
)
allocating_org_names = [org.name for org in allocating_org_options]
# Exclude the exporting organization's own name (own-org exclusion is also
# enforced at the ID level inside get_allocation_agreement_organizations)
user_org_name_lower = organization.name.lower() if organization.name else None
allocating_org_names = [
n for n in all_names
if not user_org_name_lower or n.lower() != user_org_name_lower
]

# Site Name column - helpful prompt
site_name_validator = DataValidation(
Expand Down
30 changes: 24 additions & 6 deletions backend/lcfs/web/api/charging_site/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ async def import_data(
org_code: str,
file: UploadFile,
overwrite: bool = False,
organization_name: str = "",
) -> str:
"""
Initiates the import job in a separate thread executor.
Expand Down Expand Up @@ -94,7 +95,13 @@ async def import_data(
# Start the import task without blocking
asyncio.create_task(
import_async(
organization_id, user, org_code, copied_file, job_id, overwrite
organization_id,
user,
org_code,
copied_file,
job_id,
overwrite,
organization_name,
)
)

Expand Down Expand Up @@ -134,6 +141,7 @@ async def import_async(
file: UploadFile,
job_id: str,
overwrite: bool = False,
organization_name: str = "",
):
"""
Performs the actual import in an async context.
Expand Down Expand Up @@ -233,7 +241,9 @@ async def import_async(
# row[8] is now allocating_organization_name (optional string)

# Validate row
error = _validate_row(row, row_idx, valid_org_names)
error = _validate_row(
row, row_idx, valid_org_names, organization_name
)
if error:
errors.append(error)
rejected += 1
Expand Down Expand Up @@ -320,6 +330,7 @@ def _validate_row(
row: tuple,
row_idx: int,
valid_org_names: set[str],
organization_name: str = "",
) -> str | None:
"""
Validates a single row of data and returns an error string if invalid.
Expand Down Expand Up @@ -383,10 +394,17 @@ def _validate_row(
except (ValueError, TypeError):
return f"Row {row_idx}: Invalid longitude value '{longitude}'. Must be a valid number"

# Validate allocating organization (optional field)
# Validation disabled - any value is now accepted
# if allocating_org_name and allocating_org_name not in valid_org_names:
# return f"Row {row_idx}: Invalid allocating organization: {allocating_org_name}. Must be from your allocation agreements."
# Validate that the allocating organization is not the user's own organization
if (
allocating_org_name
and organization_name
and str(allocating_org_name).strip().lower()
== organization_name.strip().lower()
):
return (
f"Row {row_idx}: You cannot select your own organization "
f"as the Allocating organization."
)

return None

Expand Down
30 changes: 29 additions & 1 deletion backend/lcfs/web/api/charging_site/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ async def get_allocation_agreement_organizations(
AllocationAgreement.compliance_report_id
== ComplianceReport.compliance_report_id,
)
.where(ComplianceReport.organization_id == organization_id)
.where(
ComplianceReport.organization_id == organization_id,
Organization.organization_id != organization_id,
)
.distinct()
.order_by(Organization.name)
)
Expand Down Expand Up @@ -917,6 +920,31 @@ async def search_organizations_by_name(
)
return result.scalars().all()

@repo_handler
async def get_allocating_organization_names(
self, organization_id: int
) -> List[str]:
matched_orgs = await self.get_allocation_agreement_organizations(
organization_id
)
transaction_partners = (
await self.get_transaction_partners_from_allocation_agreements(
organization_id
)
)
historical_names = await self.get_distinct_allocating_organization_names(
organization_id
)

seen: dict[str, str] = {}
for org in matched_orgs:
seen[org.name.lower()] = org.name
for name in transaction_partners + historical_names:
if name.lower() not in seen:
seen[name.lower()] = name

return sorted(seen.values(), key=str.lower)

@repo_handler
async def get_transaction_partners_from_allocation_agreements(
self, organization_id: int
Expand Down
46 changes: 20 additions & 26 deletions backend/lcfs/web/api/charging_site/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,44 +119,38 @@ async def search_allocation_organizations(
self, organization_id: int, query: str
) -> List[dict]:
"""
Search for allocating organization suggestions.
Return allocating organization suggestions filtered by query,
excluding the user's own organization.
"""
try:
query_lower = query.lower().strip()
user_org = self.request.user.organization
user_org_name_lower = user_org.name.lower() if user_org else None

# Use existing method to get matched organizations
matched_orgs = await self.repo.get_allocation_agreement_organizations(
organization_id
)

# Get unmatched names from allocation agreements and charging sites
transaction_partners = (
await self.repo.get_transaction_partners_from_allocation_agreements(
organization_id
)
)
historical_names = (
await self.repo.get_distinct_allocating_organization_names(
organization_id
)
all_names = await self.repo.get_allocating_organization_names(
organization_id
)

# Build suggestions dict - matched orgs take precedence
suggestions = {}
for org in matched_orgs:
if query_lower in org.name.lower():
suggestions[org.name.lower()] = {
"organizationId": org.organization_id,
"name": org.name,
}
# Build a lookup so matched orgs (with IDs) take precedence
org_id_by_name = {org.name.lower(): org.organization_id for org in matched_orgs}

# Add unmatched names (transaction partners + historical)
for name in transaction_partners + historical_names:
suggestions = []
for name in all_names:
name_lower = name.lower()
if name_lower not in suggestions and query_lower in name_lower:
suggestions[name_lower] = {"organizationId": None, "name": name}
if user_org_name_lower and name_lower == user_org_name_lower:
continue
if query_lower in name_lower:
suggestions.append(
{
"organizationId": org_id_by_name.get(name_lower),
"name": name,
}
)

return sorted(suggestions.values(), key=lambda x: x["name"].lower())[:50]
return suggestions[:50]
except Exception as e:
logger.error("Error searching allocation organizations", error=str(e))
raise HTTPException(status_code=500, detail="Internal Server Error")
Expand Down
30 changes: 30 additions & 0 deletions backend/lcfs/web/api/charging_site/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ async def charging_site_create_access(
detail="Organization ID in URL and request body do not match",
)

self._validate_allocating_organization(organization_id, data)

return True

async def charging_site_delete_update_access(
Expand Down Expand Up @@ -104,8 +106,36 @@ async def charging_site_delete_update_access(
detail="A charging site with this name already exists for your organization. Please use a unique site name.",
)

if data:
self._validate_allocating_organization(organization_id, data)

return True

def _validate_allocating_organization(
self, organization_id: int, data: ChargingSiteCreateSchema
):
"""
Validates that the allocating organization is not the user's own organization.
"""
if (
data.allocating_organization_id
and data.allocating_organization_id == organization_id
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You cannot select your own organization as the Allocating organization.",
)
user_org = self.request.user.organization
if user_org and data.allocating_organization_name:
if (
data.allocating_organization_name.strip().lower()
== user_org.name.strip().lower()
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You cannot select your own organization as the Allocating organization.",
)

async def validate_organization_access(self, charging_site_id: int):
"""
Validates that the charging site exists and the user has access to it.
Expand Down
1 change: 1 addition & 0 deletions backend/lcfs/web/api/charging_site/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ async def import_charging_sites(
organization.organization_code,
file,
overwrite,
organization_name=organization.name or "",
)
return JSONResponse(content={"jobId": job_id})

Expand Down
Loading
Loading