Skip to content

Commit

Permalink
Add regulated ownerships report
Browse files Browse the repository at this point in the history
  • Loading branch information
indigane committed May 30, 2024
1 parent 3f25bed commit bd0a2f6
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 16 deletions.
21 changes: 21 additions & 0 deletions backend/hitas/services/owner.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@ def obfuscate_owners_without_regulated_apartments() -> list[OwnerT]:
return obfuscated_owners


def find_regulated_ownerships() -> list[Ownership]:
return list(
Ownership.objects.select_related(
"owner",
"sale__apartment__building__real_estate__housing_company__postal_code",
"sale__apartment__building__real_estate__housing_company",
)
.filter(
sale__apartment__building__real_estate__housing_company__regulation_status=RegulationStatus.REGULATED,
)
.exclude(
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.HALF_HITAS,
)
.order_by(
"owner__name",
"sale__apartment__building__real_estate__housing_company__postal_code__value",
"sale__apartment__street_address",
)
)


def find_owners_with_multiple_ownerships() -> list[OwnershipWithApartmentCount]:
return list(
Ownership.objects.select_related(
Expand Down
57 changes: 56 additions & 1 deletion backend/hitas/services/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
HousingCompanyWithUnregulatedReportAnnotations,
RegulationStatus,
)
from hitas.models.ownership import OwnershipWithApartmentCount
from hitas.models.ownership import Ownership, OwnershipWithApartmentCount
from hitas.utils import format_sheet, resize_columns

T = TypeVar("T")
Expand Down Expand Up @@ -90,6 +90,16 @@ class SalesInfo(TypedDict):
maximum: Decimal


class OwnershipReportColumns(NamedTuple):
owner_name: str
apartment_address: str
postal_code: str
owner_identifier: str
housing_company_name: str
housing_company_completion_date: datetime.date | str
cost_area: int | str


class MultipleOwnershipReportColumns(NamedTuple):
owner_name: str
apartment_address: str
Expand Down Expand Up @@ -713,6 +723,51 @@ def sort_sales_by_cost_area(sales: list[ApartmentSale]) -> SalesByCostArea:
)


def build_regulated_ownerships_report_excel(ownerships: list[Ownership]) -> Workbook:
workbook = Workbook()
worksheet: Worksheet = workbook.active

column_headers = OwnershipReportColumns(
owner_name="Omistajan nimi",
apartment_address="Asunnon osoite",
postal_code="Postinumero",
owner_identifier="Omistajan henkilö- tai Y-tunnus",
housing_company_name="Yhtiön nimi",
housing_company_completion_date="Yhtiön valmistumispäivä",
cost_area="Kalleusalue",
)
worksheet.append(column_headers)

completion_dates_by_housing_company_id = {}

for ownership in ownerships:
# Cache completion_date as it is the most expensive operation here
# because it queries all apartment completion dates on each iteration
# and there are relatively few housing companies.
completion_date = completion_dates_by_housing_company_id.get(
ownership.apartment.building.real_estate.housing_company.pk, None
)
if completion_date is None:
completion_date = ownership.apartment.building.real_estate.housing_company.completion_date
completion_dates_by_housing_company_id[
ownership.apartment.building.real_estate.housing_company.pk
] = completion_date
worksheet.append(
OwnershipReportColumns(
owner_name=Owner.OBFUSCATED_OWNER_NAME if ownership.owner.non_disclosure else ownership.owner.name,
apartment_address=ownership.apartment.address,
postal_code=ownership.apartment.postal_code.value,
owner_identifier="" if ownership.owner.non_disclosure else ownership.owner.identifier,
housing_company_name=ownership.apartment.building.real_estate.housing_company.display_name,
housing_company_completion_date=completion_date,
cost_area=ownership.apartment.postal_code.cost_area,
)
)

_basic_format_sheet(column_headers, worksheet)
return workbook


def build_multiple_ownerships_report_excel(ownerships: list[OwnershipWithApartmentCount]) -> Workbook:
workbook = Workbook()
worksheet: Worksheet = workbook.active
Expand Down
77 changes: 76 additions & 1 deletion backend/hitas/tests/apis/test_api_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,17 @@ def test__api__multiple_ownerships_report__no_owners(api_client: HitasAPIClient)
]


@pytest.mark.django_db
def test__api__regulated_ownerships_report__no_owners(api_client: HitasAPIClient):
url = reverse("hitas:regulated-ownerships-report-list")
response: HttpResponse = api_client.get(url)

workbook: Workbook = load_workbook(BytesIO(response.content), data_only=False)
worksheet: Worksheet = workbook.worksheets[0]

assert len(list(worksheet.values)) == 1, "There should be only the header row"


@pytest.mark.django_db
@pytest.mark.parametrize("non_disclosure", [False, True])
def test__api__multiple_ownerships_report__single_owner(api_client: HitasAPIClient, non_disclosure):
Expand All @@ -1452,7 +1463,7 @@ def test__api__multiple_ownerships_report__single_owner(api_client: HitasAPIClie
ownership_2: Ownership = OwnershipFactory.create(
owner=owner,
sale__apartment__building__real_estate__housing_company__postal_code__value="00002",
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.NEW_HITAS_II,
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.NEW_HITAS_I,
)

url = reverse("hitas:multiple-ownerships-report-list")
Expand Down Expand Up @@ -1499,6 +1510,30 @@ def test__api__multiple_ownerships_report__single_owner(api_client: HitasAPIClie
]


@pytest.mark.django_db
@pytest.mark.parametrize("non_disclosure", [False, True])
def test__api__regulated_ownerships_report__single_owner(api_client: HitasAPIClient, non_disclosure):
owner: Owner = OwnerFactory.create(non_disclosure=non_disclosure)
OwnershipFactory.create(
owner=owner,
sale__apartment__building__real_estate__housing_company__postal_code__value="00001",
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.NEW_HITAS_I,
)
OwnershipFactory.create(
owner=owner,
sale__apartment__building__real_estate__housing_company__postal_code__value="00002",
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.NEW_HITAS_I,
)

url = reverse("hitas:regulated-ownerships-report-list")
response: HttpResponse = api_client.get(url)

workbook: Workbook = load_workbook(BytesIO(response.content), data_only=False)
worksheet: Worksheet = workbook.worksheets[0]

assert len(list(worksheet.values)) == 3, "There should be 2 ownership rows and 1 header row"


@pytest.mark.django_db
def test__api__multiple_ownerships_report__multiple_owners(api_client: HitasAPIClient):
owner_1: Owner = OwnerFactory.create(name="Owner 1")
Expand Down Expand Up @@ -1610,6 +1645,46 @@ def test__api__multiple_ownerships_report__multiple_owners(api_client: HitasAPIC
]


@pytest.mark.django_db
def test__api__regulated_ownerships_report__multiple_owners(api_client: HitasAPIClient):
owner_1: Owner = OwnerFactory.create(name="Owner 1")
OwnershipFactory.create(
owner=owner_1,
sale__apartment__building__real_estate__housing_company__postal_code__value="00001",
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.NEW_HITAS_I,
)
OwnershipFactory.create(
owner=owner_1,
sale__apartment__building__real_estate__housing_company__postal_code__value="00002",
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.NEW_HITAS_I,
)

owner_2: Owner = OwnerFactory.create(name="Owner 2", non_disclosure=True)
OwnershipFactory.create(
owner=owner_2,
sale__apartment__building__real_estate__housing_company__postal_code__value="00001",
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.NEW_HITAS_I,
)
OwnershipFactory.create(
owner=owner_2,
sale__apartment__building__real_estate__housing_company__postal_code__value="00002",
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.NEW_HITAS_I,
)
OwnershipFactory.create(
owner=owner_2,
sale__apartment__building__real_estate__housing_company__postal_code__value="00003",
sale__apartment__building__real_estate__housing_company__hitas_type=HitasType.NEW_HITAS_I,
)

url = reverse("hitas:regulated-ownerships-report-list")
response: HttpResponse = api_client.get(url)

workbook: Workbook = load_workbook(BytesIO(response.content), data_only=False)
worksheet: Worksheet = workbook.worksheets[0]

assert len(list(worksheet.values)) == 6, "There should be 5 ownership rows and 1 header row"


@pytest.mark.django_db
def test__api__download_ownerships_by_housing_company(api_client: HitasAPIClient):
housing_company = HousingCompanyFactory(
Expand Down
7 changes: 7 additions & 0 deletions backend/hitas/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@
basename="sales-by-postal-code-and-area-report",
)

# /api/v1/reports/download-regulated-ownerships-report
router.register(
r"reports/download-regulated-ownerships-report",
views.RegulatedOwnershipsReportView,
basename="regulated-ownerships-report",
)

# /api/v1/reports/download-multiple-ownerships-report
router.register(
r"reports/download-multiple-ownerships-report",
Expand Down
1 change: 1 addition & 0 deletions backend/hitas/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
OwnershipsByHousingCompanyReport,
RegulatedHalfHitasHousingCompaniesReportView,
RegulatedHousingCompaniesReportView,
RegulatedOwnershipsReportView,
SalesByPostalCodeAndAreaReportView,
SalesReportView,
UnregulatedHousingCompaniesReportView,
Expand Down
17 changes: 16 additions & 1 deletion backend/hitas/views/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
find_regulated_housing_companies_for_reporting,
find_unregulated_housing_companies_for_reporting,
)
from hitas.services.owner import find_apartments_by_housing_company, find_owners_with_multiple_ownerships
from hitas.services.owner import (
find_apartments_by_housing_company,
find_owners_with_multiple_ownerships,
find_regulated_ownerships,
)
from hitas.services.reports import (
build_housing_company_state_report_excel,
build_multiple_ownerships_report_excel,
build_owners_by_housing_companies_report_excel,
build_regulated_housing_companies_report_excel,
build_regulated_ownerships_report_excel,
build_sales_by_postal_code_and_area_report_excel,
build_sales_report_excel,
build_unregulated_housing_companies_report_excel,
Expand Down Expand Up @@ -131,6 +136,16 @@ def list(self, request: Request, *args, **kwargs) -> HttpResponse:
return get_excel_response(filename=filename, excel=workbook)


class RegulatedOwnershipsReportView(ViewSet):
renderer_classes = [HitasJSONRenderer, ExcelRenderer]

def list(self, request: Request, *args, **kwargs) -> HttpResponse:
ownerships = find_regulated_ownerships()
workbook = build_regulated_ownerships_report_excel(ownerships)
filename = "Sääntelyn piirissä olevien asuntojen omistajat.xlsx"
return get_excel_response(filename=filename, excel=workbook)


class MultipleOwnershipsReportView(ViewSet):
renderer_classes = [HitasJSONRenderer, ExcelRenderer]

Expand Down
21 changes: 21 additions & 0 deletions backend/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4130,6 +4130,27 @@ paths:
"500":
$ref: "#/components/responses/InternalServerError"

/api/v1/reports/download-regulated-ownerships-report:
get:
description: Download an Excel report of owners with ownerships to regulated hitas apartments
operationId: fetch-regulated-ownerships-report-excel
tags:
- Reports
responses:
'200':
description: Successfully downloaded a report of owners with ownerships to regulated hitas apartments
content:
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
schema:
type: string
format: binary
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'

/api/v1/reports/download-multiple-ownerships-report:
get:
description: Download an Excel report of owners with multiple ownerships to regulated hitas apartments
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/common/services/hitasApi/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export const downloadUnregulatedHousingCompaniesPDF = () =>
export const downloadHousingCompanyStatesReportPDF = () =>
fetchAndDownloadPDF("/reports/download-housing-company-states-report");

export const downloadRegulatedOwnershipsReportExcel = () =>
fetchAndDownloadPDF("/reports/download-regulated-ownerships-report");

export const downloadMultipleOwnershipsReportPDF = () =>
fetchAndDownloadPDF("/reports/download-multiple-ownerships-report");

Expand Down
44 changes: 31 additions & 13 deletions frontend/src/features/reports/components/OwnerReports.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
import {DownloadButton, Heading} from "../../../common/components";
import {downloadMultipleOwnershipsReportPDF} from "../../../common/services";
import {downloadMultipleOwnershipsReportPDF, downloadRegulatedOwnershipsReportExcel} from "../../../common/services";

const OwnerReports = () => {
return (
<div className="report-container">
<div className="column">
<Heading type="sub">Usean Hitas-asunnon omistajat</Heading>
<span>
Listaus henkilöistä, jotka omistavat useamman kuin yhden Hitas-asunnon, sekä heidän omistuksistaan.
</span>
<div>
<DownloadButton
buttonText="Lataa raportti"
onClick={downloadMultipleOwnershipsReportPDF}
/>
<>
<div className="report-container">
<div className="column">
<Heading type="sub">Usean Hitas-asunnon omistajat</Heading>
<span>
Listaus henkilöistä, jotka omistavat useamman kuin yhden Hitas-asunnon, sekä heidän
omistuksistaan.
</span>
<div>
<DownloadButton
buttonText="Lataa raportti"
onClick={downloadMultipleOwnershipsReportPDF}
/>
</div>
</div>
</div>
</div>
<div className="report-container">
<div className="column">
<Heading type="sub">Sääntelyn piirissä olevien Hitas-asuntojen omistajat</Heading>
<span>
Listaus henkilöistä, jotka omistavat yhden tai useamman sääntelyn piirissä olevan Hitas-asunnon,
sekä heidän omistuksistaan.
</span>
<div>
<DownloadButton
buttonText="Lataa raportti"
onClick={downloadRegulatedOwnershipsReportExcel}
/>
</div>
</div>
</div>
</>
);
};

Expand Down

0 comments on commit bd0a2f6

Please sign in to comment.