Skip to content

Commit 65f49a6

Browse files
authored
Merge pull request #4250 from bcgov/bug-4234-registration-purpose-change-effects
chore: updated supplementary report create logic
2 parents 8964977 + 2da89b2 commit 65f49a6

11 files changed

Lines changed: 1088 additions & 729 deletions

File tree

bc_obps/reporting/api/report_supplementary_version.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
from django.http import HttpRequest
33
from reporting.constants import EMISSIONS_REPORT_TAGS
44
from reporting.schema.generic import Message
5-
from reporting.service.report_supplementary_version_service import ReportSupplementaryVersionService
5+
from reporting.service.report_supplementary_version_service.report_supplementary_version_service import (
6+
ReportSupplementaryVersionService,
7+
)
68
from service.report_version_service import ReportVersionService
79
from service.error_service.custom_codes_4xx import custom_codes_4xx
810
from .router import router
911
from reporting.api.permissions import approved_industry_user_report_version_composite_auth
10-
from ..models import ReportVersion, ReportOperation
1112

1213

1314
@router.get(
@@ -34,17 +35,5 @@ def is_supplementary_report_version(request: HttpRequest, version_id: int) -> Tu
3435
auth=approved_industry_user_report_version_composite_auth,
3536
)
3637
def create_report_supplementary_version(request: HttpRequest, version_id: int) -> Tuple[Literal[201], int]:
37-
# Get registration_purpose from Operation
38-
report_version = ReportVersion.objects.select_related("report__operation").get(id=version_id)
39-
operation_registration_purpose = report_version.report.operation.registration_purpose
40-
41-
# Get registration_purpose from ReportOperation
42-
report_operation = ReportOperation.objects.get(report_version_id=version_id)
43-
report_operation_registration_purpose = report_operation.registration_purpose
44-
45-
if operation_registration_purpose == report_operation_registration_purpose:
46-
new_version = ReportSupplementaryVersionService.create_report_supplementary_version(version_id)
47-
else:
48-
new_version = ReportVersionService.create_report_version(report_version.report)
49-
38+
new_version = ReportSupplementaryVersionService.create_or_clone_report_version(version_id)
5039
return 201, new_version.id

bc_obps/reporting/service/report_supplementary_version_service.py

Lines changed: 0 additions & 564 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# ruff: noqa: F401
2+
3+
from .report_supplementary_version_service import ReportSupplementaryVersionService

bc_obps/reporting/service/report_supplementary_version_service/report_supplementary_cloning.py

Lines changed: 510 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from django.db import transaction
2+
3+
from reporting.models import ReportOperation, ReportVersion
4+
from reporting.service.report_supplementary_version_service.report_supplementary_cloning import clone_all
5+
from service.reporting_year_service import ReportingYearService
6+
7+
8+
class ReportSupplementaryVersionService:
9+
10+
@staticmethod
11+
@transaction.atomic
12+
def create_or_clone_report_version(report_version_id: int) -> ReportVersion:
13+
"""
14+
Creates a supplementary (cloned copy) or blank report version depending on whether
15+
the registration purpose, operator, or reporting year have changed.
16+
"""
17+
report_version = ReportVersion.objects.select_related(
18+
"report__operation", "report__reporting_year", "report_operation"
19+
).get(id=report_version_id)
20+
operation = report_version.report.operation
21+
operator_changed = operation.operator_id != report_version.report.operator_id
22+
purpose_changed = operation.registration_purpose != report_version.report_operation.registration_purpose
23+
24+
current_year = ReportingYearService.get_current_reporting_year()
25+
is_past_year = report_version.report.reporting_year.reporting_year < current_year.reporting_year
26+
27+
should_clone = not purpose_changed or operator_changed or is_past_year
28+
29+
if should_clone:
30+
return ReportSupplementaryVersionService._create_supplementary_version(
31+
report_version, operator_changed, purpose_changed
32+
)
33+
34+
return ReportSupplementaryVersionService._create_blank_report_version(report_version)
35+
36+
@staticmethod
37+
@transaction.atomic
38+
def _create_blank_report_version(report_version: ReportVersion) -> ReportVersion:
39+
"""
40+
Creates a new blank Draft report version with a ReportOperation reflecting the
41+
current operation's registration purpose, but no pre-populated facility or
42+
personal data.
43+
"""
44+
operation = report_version.report.operation
45+
operator = report_version.report.operator
46+
47+
new_version = ReportVersion.objects.create(
48+
report=report_version.report,
49+
report_type=report_version.report_type,
50+
status=ReportVersion.ReportVersionStatus.Draft,
51+
is_latest_submitted=False,
52+
)
53+
54+
report_operation = ReportOperation.objects.create(
55+
operator_legal_name=operator.legal_name,
56+
operator_trade_name=operator.trade_name,
57+
operation_name=operation.name,
58+
operation_type=operation.type,
59+
operation_bcghgid=operation.bcghg_id.id if operation.bcghg_id else None,
60+
bc_obps_regulated_operation_id=(
61+
operation.bc_obps_regulated_operation.id if operation.bc_obps_regulated_operation else ""
62+
),
63+
report_version=new_version,
64+
registration_purpose=operation.registration_purpose or 'OBPS Regulated Operation',
65+
)
66+
report_operation.activities.add(*list(operation.activities.all()))
67+
report_operation.regulated_products.add(*list(operation.regulated_products.all()))
68+
69+
return new_version
70+
71+
@staticmethod
72+
@transaction.atomic
73+
def _create_supplementary_version(
74+
report_version: ReportVersion,
75+
operator_changed: bool = False,
76+
purpose_changed: bool = False,
77+
) -> ReportVersion:
78+
"""
79+
Creates a new Draft version by cloning the data from the source version..
80+
"""
81+
source = ReportSupplementaryVersionService._resolve_source_version(
82+
report_version, operator_changed, purpose_changed
83+
)
84+
85+
if source is None:
86+
raise ValueError(
87+
"Cannot create supplementary version: no submitted version found matching the new registration purpose."
88+
)
89+
90+
new_version = ReportVersion.objects.create(
91+
report=report_version.report,
92+
report_type=source.report_type,
93+
status=ReportVersion.ReportVersionStatus.Draft,
94+
is_latest_submitted=False,
95+
)
96+
97+
clone_all(source, new_version)
98+
return new_version
99+
100+
@staticmethod
101+
def _resolve_source_version(
102+
report_version: ReportVersion,
103+
operator_changed: bool,
104+
purpose_changed: bool,
105+
) -> ReportVersion | None:
106+
"""
107+
Returns the version to clone from.
108+
- If both the operator and registration purpose have changed, returns the latest
109+
submitted version from the original operator with the matching registration purpose.
110+
- Otherwise, returns the current report version.
111+
"""
112+
if purpose_changed:
113+
matching_versions = ReportVersion.objects.filter(
114+
report__operation=report_version.report.operation,
115+
report__reporting_year=report_version.report.reporting_year,
116+
report_operation__registration_purpose=report_version.report_operation.registration_purpose,
117+
status=ReportVersion.ReportVersionStatus.Submitted,
118+
)
119+
if operator_changed:
120+
matching_versions = matching_versions.filter(report__operator_id=report_version.report.operator_id)
121+
return matching_versions.order_by("-created_at").first()
122+
return report_version
Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
22
from typing import Type, Any
3+
from django.db.models import F
34
from django.dispatch import receiver
45
from registration.signals.signals import operation_registration_purpose_changed
56
from reporting.models import ReportVersion
67
from service.report_version_service import ReportVersionService
8+
from service.reporting_year_service import ReportingYearService
79

810
logger = logging.getLogger(__name__)
911

@@ -14,22 +16,33 @@ def handle_registration_purpose_changed(sender: Type[Any], **kwargs: Any) -> Non
1416
Signal handler that deletes the draft report version for an operation
1517
whose registration purpose has changed.
1618
17-
Args:
18-
sender: The class that sent the signal
19-
**kwargs: Signal arguments including operation_id and user_guid
19+
Skips deletion if:
20+
- No draft report version exists for the current reporting year
21+
- The draft belongs to a previous operator (operation was transferred)
2022
"""
21-
operation_id = kwargs.get('operation_id')
23+
operation_id = kwargs.get("operation_id")
2224
if not operation_id:
23-
logger.warning("Missing operation_id in signal kwargs")
25+
logger.warning("Signal received without operation_id in kwargs")
2426
return
2527

26-
draft_version = ReportVersion.objects.filter(
27-
report__operation_id=operation_id, status=ReportVersion.ReportVersionStatus.Draft
28-
).first()
28+
current_reporting_year = ReportingYearService.get_current_reporting_year()
29+
30+
draft_version = (
31+
ReportVersion.objects.select_related("report__operation", "report__reporting_year", "report__operator")
32+
.filter(
33+
report__operation_id=operation_id,
34+
report__operation__operator_id=F("report__operator_id"),
35+
status=ReportVersion.ReportVersionStatus.Draft,
36+
report__reporting_year=current_reporting_year,
37+
)
38+
.first()
39+
)
2940

3041
if not draft_version:
31-
logger.info(f"No draft report version found for operation_id={operation_id}")
42+
logger.info("No draft report version found for operation_id=%s", operation_id)
3243
return
3344

34-
logger.info(f"Deleting draft report version ID={draft_version.id} for operation_id={operation_id}")
35-
ReportVersionService.delete_report_version(draft_version.id)
45+
version_id = draft_version.id
46+
47+
logger.info("Deleting draft report version id=%s for operation_id=%s", version_id, operation_id)
48+
ReportVersionService.delete_report_version(version_id)

bc_obps/reporting/tests/api/test_report_supplementary_version_api.py

Lines changed: 6 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import pytest
21
from unittest.mock import patch, MagicMock
32
from model_bakery import baker
4-
from registration.models import Operation
53
from registration.tests.utils.helpers import CommonTestSetup, TestUtils
64
from registration.utils import custom_reverse_lazy
7-
from reporting.models import ReportEmission, ReportProduct
85
from reporting.tests.utils.report_access_validation import assert_report_version_ownership_is_validated
96

107

@@ -20,43 +17,17 @@ def setup_method(self):
2017
self.new_report_version = baker.make_recipe("reporting.tests.utils.report_version")
2118
TestUtils.authorize_current_user_as_operator_user(self, operator=self.old_report_version.report.operator)
2219

23-
@pytest.mark.parametrize(
24-
"existing_purpose, new_purpose, expected_called_method",
25-
[
26-
# Purpose is same → create_report_supplementary_version is called
27-
(Operation.Purposes.OBPS_REGULATED_OPERATION, Operation.Purposes.OBPS_REGULATED_OPERATION, "supplementary"),
28-
# Purpose is different → create_report_version is called
29-
(Operation.Purposes.OBPS_REGULATED_OPERATION, "different-purpose", "new_version"),
30-
],
31-
)
3220
@patch(
3321
"reporting.service.report_supplementary_version_service.ReportSupplementaryVersionService"
34-
".create_report_supplementary_version"
22+
".create_or_clone_report_version"
3523
)
36-
@patch("service.report_version_service.ReportVersionService.create_report_version")
37-
def test_create_version_based_on_purpose(
24+
def test_create_or_clone_report_version_is_called(
3825
self,
39-
mock_create_report_version: MagicMock,
40-
mock_create_supplementary: MagicMock,
41-
existing_purpose,
42-
new_purpose,
43-
expected_called_method,
26+
mock_create_or_clone: MagicMock,
4427
):
28+
"""Test that create_or_clone_report_version is called with the correct version ID."""
4529
report_version = self.old_report_version
46-
47-
report_version.report.operation.registration_purpose = existing_purpose
48-
report_version.report.operation.save()
49-
50-
baker.make_recipe(
51-
"reporting.tests.utils.report_operation",
52-
report_version=report_version,
53-
registration_purpose=new_purpose,
54-
)
55-
56-
if expected_called_method == "supplementary":
57-
mock_create_supplementary.return_value = self.new_report_version
58-
else:
59-
mock_create_report_version.return_value = self.new_report_version
30+
mock_create_or_clone.return_value = self.new_report_version
6031

6132
response = TestUtils.mock_post_with_auth_role(
6233
self,
@@ -71,51 +42,7 @@ def test_create_version_based_on_purpose(
7142

7243
assert response.status_code == 201
7344
assert response.json() == self.new_report_version.id
74-
75-
if expected_called_method == "supplementary":
76-
mock_create_supplementary.assert_called_once_with(report_version.id)
77-
mock_create_report_version.assert_not_called()
78-
else:
79-
mock_create_report_version.assert_called_once_with(report_version.report)
80-
mock_create_supplementary.assert_not_called()
81-
82-
def test_new_version_has_no_emission_or_product_records(self):
83-
baker.make_recipe(
84-
"reporting.tests.utils.report_operation",
85-
report_version=self.old_report_version,
86-
registration_purpose="different-purpose",
87-
)
88-
baker.make_recipe("reporting.tests.utils.report_emission", report_version=self.old_report_version, _quantity=3)
89-
90-
facility_report = baker.make_recipe(
91-
"reporting.tests.utils.facility_report",
92-
report_version=self.old_report_version,
93-
)
94-
baker.make_recipe(
95-
"reporting.tests.utils.report_product",
96-
report_version=self.old_report_version,
97-
facility_report=facility_report,
98-
product_id=1,
99-
)
100-
101-
self.old_report_version.status = "Submitted"
102-
self.old_report_version.save()
103-
104-
response = TestUtils.mock_post_with_auth_role(
105-
self,
106-
"industry_user",
107-
self.content_type,
108-
{},
109-
custom_reverse_lazy(
110-
"create_report_supplementary_version",
111-
kwargs={"version_id": self.old_report_version.id},
112-
),
113-
)
114-
assert response.status_code == 201
115-
new_report_version_id = response.json()
116-
# Verify that the new report version has no related entries
117-
assert not ReportEmission.objects.filter(report_version_id=new_report_version_id).exists()
118-
assert not ReportProduct.objects.filter(report_version_id=new_report_version_id).exists()
45+
mock_create_or_clone.assert_called_once_with(report_version.id)
11946

12047
@patch("service.report_version_service.ReportVersionService.is_initial_report_version")
12148
def test_returns_data_as_provided_by_is_initial_version(self, mock_is_initial_report_version: MagicMock):

0 commit comments

Comments
 (0)