Skip to content

Commit b2a0d34

Browse files
committed
chore: add service to start report for reporting year
chore: cleanup chore: cleanup chore: add registration purpose to startReport schema chore: cleanup chore: cleanup chore: cleanup chore: cleanup chore: cleanup
1 parent 87b318e commit b2a0d34

19 files changed

Lines changed: 491 additions & 91 deletions

File tree

bc_obps/common/tests/endpoints/auth/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"kwargs": {"operation_id": MOCK_UUID},
4242
},
4343
{"method": "get", "endpoint_name": "list_current_users_operations"},
44+
{"method": "get", "endpoint_name": "list_previous_reportable_operations"},
4445
{
4546
"method": "get",
4647
"endpoint_name": "get_initial_activity_data",
@@ -174,6 +175,7 @@
174175
"endpoint_name": "register_create_operation_information",
175176
},
176177
{"method": "post", "endpoint_name": "start_report"},
178+
{"method": "post", "endpoint_name": "create_report_for_reporting_year"},
177179
{
178180
"method": "post",
179181
"endpoint_name": "save_report",

bc_obps/registration/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@
4444
from . import transfer_events
4545
from ._transfer_events import transfer_id
4646
from .router import router # django_ninja router object # noqa: F401
47+
from ._operations.reportable import list_previous_reportable_operations
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import List, Literal, Tuple
2+
from django.http import HttpRequest
3+
from common.api.utils import get_current_user_guid
4+
from registration.schema import OperationReportableOut, Message
5+
from service.error_service.custom_codes_4xx import custom_codes_4xx
6+
from service.operation_service import OperationService
7+
from registration.api.router import router
8+
from common.permissions import authorize
9+
from registration.constants import OPERATION_TAGS
10+
11+
##### GET #####
12+
13+
14+
@router.get(
15+
"/operations/reportable/previous",
16+
response={200: List[OperationReportableOut], custom_codes_4xx: Message},
17+
tags=OPERATION_TAGS,
18+
auth=authorize("approved_industry_user"),
19+
description="""
20+
Returns the reporting year/operation combinations for which the current user is eligible to create a report
21+
""",
22+
)
23+
def list_previous_reportable_operations(
24+
request: HttpRequest,
25+
) -> Tuple[Literal[200], list[dict]]:
26+
return 200, OperationService.list_previous_reportable_operations(
27+
get_current_user_guid(request),
28+
)

bc_obps/registration/schema/operation.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,9 @@ class OperationUpdateStatusOut(ModelSchema):
316316
class Meta:
317317
model = Operation
318318
fields = ["id", "status"]
319+
320+
321+
class OperationReportableOut(Schema):
322+
operation_id: UUID
323+
operation_name: str
324+
reporting_year: int

bc_obps/reporting/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ruff: noqa: F401
22
from .router import router
33
from .reports import start_report
4+
from .reports import create_report_for_reporting_year
45
from .build_form_schema import build_form_schema
56
from .reporting_dashboard import get_dashboard_operations_list
67
from .activity_data import get_initial_activity_data

bc_obps/reporting/api/reports.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from reporting.api.permissions import check_operation_ownership
99
from reporting.constants import EMISSIONS_REPORT_TAGS
1010
from reporting.schema.generic import Message
11-
from reporting.schema.report import StartReportIn, CreateReportVersionIn
11+
from reporting.schema.report import StartReportIn, CreateReportVersionIn, CreateReportForReportingYearIn
1212
from service.report_service import ReportService
1313
from service.report_version_service import ReportVersionService, ReportVersionData
1414
from service.reporting_year_service import ReportingYearService
@@ -23,6 +23,7 @@
2323
approved_industry_user_report_composite_auth,
2424
)
2525
from reporting.models import ReportingYear, ReportVersion
26+
from common.api.utils.current_user_utils import get_current_user_guid
2627

2728

2829
@router.post(
@@ -38,6 +39,30 @@ def start_report(request: HttpRequest, payload: StartReportIn) -> Tuple[Literal[
3839
return 201, report_version_id
3940

4041

42+
@router.post(
43+
"/create-report-for-reporting-year",
44+
response={201: int, custom_codes_4xx: Message},
45+
tags=EMISSIONS_REPORT_TAGS,
46+
description="""
47+
Creates a report for a given operation and reporting year
48+
49+
Allows a new report creation for a selected year when the authenticated
50+
user's operator owned the operation during any portion of the reporting year
51+
""",
52+
auth=authorize("approved_industry_user"),
53+
)
54+
def create_report_for_reporting_year(
55+
request: HttpRequest,
56+
payload: CreateReportForReportingYearIn,
57+
) -> Tuple[Literal[201], int]:
58+
report_version_id = ReportService.create_report_for_reporting_year(
59+
user_guid=get_current_user_guid(request),
60+
data=payload,
61+
)
62+
63+
return 201, report_version_id
64+
65+
4166
@router.post(
4267
"/report-version/{version_id}/change-report-type",
4368
response={201: int, custom_codes_4xx: Message},

bc_obps/reporting/schema/report.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@ class CreateReportVersionIn(Schema):
1717
"""
1818

1919
operation_id: UUID
20+
21+
22+
class CreateReportForReportingYearIn(Schema):
23+
"""
24+
Schema for the start report for selected reporting year endpoint request
25+
"""
26+
27+
operation_id: UUID
28+
reporting_year: int
29+
registration_purpose: str

bc_obps/service/operation_designated_operator_timeline_service.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,62 @@ def get_operation_designated_operator_for_reporting_year(
6262
start_date=timeline.start_date,
6363
end_date=timeline.end_date,
6464
)
65+
66+
@classmethod
67+
def get_operation_designated_operators_for_reporting_years(
68+
cls,
69+
operation_ids: set[UUID],
70+
reporting_years: set[int],
71+
) -> dict[tuple[UUID, int], OperationDesignatedOperatorTimelinePlus]:
72+
"""
73+
Bulk version of get_operation_designated_operator_for_reporting_year
74+
75+
It is intended for scenarios where many operation/reporting year
76+
combinations need to be evaluated, such as determining the list of reportable operations
77+
78+
Returns a lookup keyed by (operation_id, reporting_year)
79+
"""
80+
81+
if not operation_ids or not reporting_years:
82+
return {}
83+
84+
min_year = min(reporting_years)
85+
max_year = max(reporting_years)
86+
87+
timelines = (
88+
OperationDesignatedOperatorTimeline.objects.select_related(
89+
"operation",
90+
"operator",
91+
)
92+
.filter(
93+
operation_id__in=operation_ids,
94+
start_date__year__lte=max_year,
95+
)
96+
.filter(Q(end_date__year__gt=min_year) | Q(end_date__isnull=True))
97+
.order_by("operation__name", "start_date")
98+
)
99+
100+
lookup: dict[tuple[UUID, int], OperationDesignatedOperatorTimelinePlus] = {}
101+
102+
for timeline in timelines:
103+
start_date = timeline.start_date
104+
end_date = timeline.end_date
105+
106+
if start_date is None:
107+
continue
108+
109+
for reporting_year in reporting_years:
110+
if start_date.year > reporting_year:
111+
continue
112+
113+
if end_date is not None and end_date.year <= reporting_year:
114+
continue
115+
116+
lookup[(timeline.operation_id, reporting_year)] = OperationDesignatedOperatorTimelinePlus(
117+
operation=timeline.operation,
118+
operator=timeline.operator,
119+
start_date=start_date,
120+
end_date=end_date,
121+
)
122+
123+
return lookup

bc_obps/service/operation_service.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@
5050
from django.db.models import Q
5151
from django.utils import timezone
5252
from registration.models.operation_designated_operator_timeline import OperationDesignatedOperatorTimeline
53+
from reporting.models.reporting_year import ReportingYear
54+
from service.reporting_year_service import ReportingYearService
5355
from django.conf import settings
56+
from service.operation_designated_operator_timeline_service import OperationDesignatedOperatorTimelineService
57+
from reporting.models.report import Report
5458

5559

5660
class OperationService:
@@ -749,3 +753,157 @@ def handle_change_of_registration_purpose(
749753
payload.regulated_products = []
750754

751755
return payload
756+
757+
@classmethod
758+
def _get_previous_reporting_years(cls) -> QuerySet[ReportingYear]:
759+
"""
760+
Returns reporting years that are eligible for the Start Past Report workflow
761+
762+
Only reporting years prior to the current reporting year are returned
763+
"""
764+
765+
current_reporting_year = ReportingYearService.get_current_reporting_year()
766+
767+
return ReportingYear.objects.filter(reporting_year__lt=current_reporting_year.reporting_year).order_by(
768+
"-reporting_year"
769+
)
770+
771+
@staticmethod
772+
def _build_reportable_operation_row(
773+
timeline: OperationDesignatedOperatorTimeline,
774+
reporting_year: ReportingYear,
775+
) -> dict[str, UUID | str | int]:
776+
"""
777+
Builds a reportable operation response row
778+
"""
779+
return {
780+
"operation_id": timeline.operation.id,
781+
"operation_name": timeline.operation.name,
782+
"reporting_year": reporting_year.reporting_year,
783+
}
784+
785+
@classmethod
786+
def list_previous_reportable_operations(
787+
cls,
788+
user_guid: UUID,
789+
) -> list[dict]:
790+
"""
791+
Returns the reporting year/operation combinations for which the current user is eligible to create a report
792+
793+
An reporting year/operation combination is eligible if:
794+
1. The operation was designated to the user's operator for that reporting year, or
795+
2. The operation is currently registered to the user's operator as a fallback
796+
797+
Existing reports are excluded
798+
"""
799+
800+
user_operator = UserDataAccessService.get_user_operator_by_user(user_guid)
801+
802+
reporting_years = cls._get_previous_reporting_years()
803+
year_values = {reporting_year.reporting_year for reporting_year in reporting_years}
804+
805+
# Find all registered operation timelines associated with the user's operator
806+
timelines = list(
807+
OperationDesignatedOperatorTimeline.objects.select_related("operation")
808+
.filter(
809+
operator_id=user_operator.operator_id,
810+
operation__status=Operation.Statuses.REGISTERED,
811+
)
812+
.order_by("operation__name", "start_date")
813+
)
814+
815+
# Find all currently registered operations for fallback handling
816+
current_registered_operations = list(
817+
Operation.objects.filter(
818+
operator_id=user_operator.operator_id,
819+
status=Operation.Statuses.REGISTERED,
820+
).order_by("name")
821+
)
822+
823+
all_operation_ids = {timeline.operation.id for timeline in timelines} | {
824+
operation.id for operation in current_registered_operations
825+
}
826+
827+
# Bulk fetch existing reports
828+
existing_reports = set(
829+
Report.objects.filter(
830+
operation_id__in=all_operation_ids,
831+
reporting_year_id__in=year_values,
832+
).values_list(
833+
"operation_id",
834+
"reporting_year_id",
835+
)
836+
)
837+
838+
# Bulk fetch designated operator timelines
839+
designations_lookup = (
840+
OperationDesignatedOperatorTimelineService.get_operation_designated_operators_for_reporting_years(
841+
operation_ids=all_operation_ids,
842+
reporting_years=year_values,
843+
)
844+
)
845+
846+
reportable_operations: list[dict] = []
847+
added_operation_years: set[tuple[UUID, int]] = set()
848+
849+
# Add combinations that are valid based on historical designated operator timelines
850+
for timeline in timelines:
851+
for reporting_year in reporting_years:
852+
operation_year = (
853+
timeline.operation.id,
854+
reporting_year.reporting_year,
855+
)
856+
857+
if operation_year in added_operation_years:
858+
continue
859+
860+
if operation_year in existing_reports:
861+
continue
862+
863+
designated_operator_timeline = designations_lookup.get(operation_year)
864+
865+
if not designated_operator_timeline:
866+
continue
867+
868+
if designated_operator_timeline.operator.id != user_operator.operator_id:
869+
continue
870+
871+
reportable_operations.append(
872+
{
873+
**cls._build_reportable_operation_row(
874+
timeline,
875+
reporting_year,
876+
),
877+
"is_current_registered_fallback": False,
878+
}
879+
)
880+
881+
added_operation_years.add(operation_year)
882+
883+
# Fallback: include current registered operations for operation/year combinations not already returned by the historical designation logic
884+
for operation in current_registered_operations:
885+
for reporting_year in reporting_years:
886+
operation_year = (
887+
operation.id,
888+
reporting_year.reporting_year,
889+
)
890+
891+
if operation_year in added_operation_years:
892+
continue
893+
894+
if operation_year in existing_reports:
895+
continue
896+
897+
reportable_operations.append(
898+
{
899+
"operation_id": operation.id,
900+
"operation_name": operation.name,
901+
"reporting_year": reporting_year.reporting_year,
902+
"registration_purpose": operation.registration_purpose,
903+
"is_current_registered_fallback": True,
904+
}
905+
)
906+
907+
added_operation_years.add(operation_year)
908+
909+
return reportable_operations

0 commit comments

Comments
 (0)