diff --git a/backend/hitas/services/reports.py b/backend/hitas/services/reports.py
index bc571f2fa..d612df193 100644
--- a/backend/hitas/services/reports.py
+++ b/backend/hitas/services/reports.py
@@ -6,7 +6,7 @@
from statistics import mean
from typing import Any, Callable, Iterable, Literal, NamedTuple, TypeAlias, TypedDict, TypeVar, Union
-from openpyxl.styles import Alignment, Border, Side
+from openpyxl.styles import Alignment, Border, Font, Side
from openpyxl.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet
from rest_framework.exceptions import ValidationError
@@ -20,6 +20,7 @@
HousingCompanyWithUnregulatedReportAnnotations,
RegulationStatus,
)
+from hitas.models.indices import SurfaceAreaPriceCeiling
from hitas.models.ownership import Ownership, OwnershipWithApartmentCount
from hitas.utils import format_sheet, resize_columns
@@ -39,6 +40,18 @@ class SalesReportColumns(NamedTuple):
total_price_per_square_meter: Decimal | str
+class SalesAndMaximumPricesReportColumns(NamedTuple):
+ cost_area: int | str
+ postal_code: str
+ apartment_address: str
+ surface_area_square_meter: Decimal | str
+ purchase_date: datetime.date | str
+ total_price: Decimal | str
+ total_price_per_square_meter: Decimal | str
+ maximum_price: Decimal | str
+ maximum_price_per_square_meter: Decimal | str
+
+
class RegulatedHousingCompaniesReportColumns(NamedTuple):
cost_area: int | str
postal_code: str
@@ -169,7 +182,7 @@ def build_sales_report_excel(sales: list[ApartmentSale]) -> Workbook:
raise ValidationError(
detail={
api_settings.NON_FIELD_ERRORS_KEY: (
- f"Surface are zero or missing for apartment {sale.apartment.address!r}. "
+ f"Surface area zero or missing for apartment {sale.apartment.address!r}. "
f"Cannot calculate price per square meter."
)
},
@@ -319,6 +332,223 @@ def conditional_range(value_range: str, **comparison_ranges_to_values: Any) -> l
return workbook
+def build_sales_and_maximum_prices_report_excel(sales: list[ApartmentSale]) -> Workbook:
+ workbook = Workbook()
+ worksheet: Worksheet = workbook.active
+
+ column_headers = SalesAndMaximumPricesReportColumns(
+ cost_area="Kalleusalue",
+ postal_code="Postinumero",
+ apartment_address="Osoite",
+ surface_area_square_meter="m2",
+ purchase_date="Kauppapäivä",
+ total_price="Toteutunut kauppahinta",
+ total_price_per_square_meter="Kaupan neliöhinta",
+ maximum_price="Enimmäishinta",
+ maximum_price_per_square_meter="Enimmäishinnan neiöhinta",
+ )
+ worksheet.append(column_headers)
+
+ # Prefetch surface area price ceilings
+ surface_area_price_ceilings = {}
+ for month_obj, value in SurfaceAreaPriceCeiling.objects.all().values_list("month", "value"):
+ surface_area_price_ceilings[(month_obj.year, month_obj.month)] = value
+
+ for sale in sales:
+ if not sale.apartment.surface_area:
+ raise ValidationError(
+ detail={
+ api_settings.NON_FIELD_ERRORS_KEY: (
+ f"Surface area zero or missing for apartment {sale.apartment.address!r}. "
+ f"Cannot calculate price per square meter."
+ )
+ },
+ )
+
+ # Pick calculation closest to the purchase date (latest calculation)
+ # but only if it is valid on the purchase date.
+ maximum_price_calculation = (
+ sale.apartment.max_price_calculations.filter(
+ calculation_date__lte=sale.purchase_date,
+ valid_until__gte=sale.purchase_date,
+ )
+ .order_by("-calculation_date")
+ .first()
+ )
+
+ maximum_price = None
+ is_maximum_price_fallback = False
+ if maximum_price_calculation is not None:
+ maximum_price = maximum_price_calculation.maximum_price
+ else:
+ # Fall back to surface area price ceiling
+ surface_area_price_ceiling = surface_area_price_ceilings.get(
+ (sale.purchase_date.year, sale.purchase_date.month),
+ )
+ if surface_area_price_ceiling is not None:
+ maximum_price = sale.apartment.surface_area * surface_area_price_ceiling
+ is_maximum_price_fallback = True
+
+ worksheet.append(
+ SalesAndMaximumPricesReportColumns(
+ cost_area=sale.apartment.postal_code.cost_area,
+ postal_code=sale.apartment.postal_code.value,
+ apartment_address=sale.apartment.address,
+ surface_area_square_meter=sale.apartment.surface_area,
+ purchase_date=sale.purchase_date,
+ total_price=sale.total_price,
+ total_price_per_square_meter=sale.total_price / sale.apartment.surface_area,
+ maximum_price=maximum_price if maximum_price is not None else "",
+ maximum_price_per_square_meter=(
+ (maximum_price / sale.apartment.surface_area) if maximum_price is not None else ""
+ ),
+ )
+ )
+
+ if is_maximum_price_fallback:
+ maximum_price_cell = worksheet.cell(row=worksheet.max_row, column=8)
+ maximum_price_per_square_meter_cell = worksheet.cell(row=worksheet.max_row, column=9)
+ maximum_price_cell.font = Font(italic=True)
+ maximum_price_per_square_meter_cell.font = Font(italic=True)
+
+ last_row = worksheet.max_row
+ worksheet.auto_filter.ref = worksheet.dimensions
+
+ empty_row = SalesAndMaximumPricesReportColumns(
+ cost_area="",
+ postal_code="",
+ apartment_address="",
+ surface_area_square_meter="",
+ purchase_date="",
+ total_price="",
+ total_price_per_square_meter="",
+ maximum_price="",
+ maximum_price_per_square_meter="",
+ )
+
+ # There needs to be an empty row for sorting and filtering to work properly
+ worksheet.append(empty_row)
+
+ @cache
+ def unwrap_range(cell_range: str) -> list[Any]:
+ return [cell.value for row in worksheet[cell_range] for cell in row]
+
+ @cache
+ def conditional_range(value_range: str, **comparison_ranges_to_values: Any) -> list[Any]:
+ """
+ Returns values from `value_range` where the given comparison ranges
+ contain all values as indicated by the mapping.
+ """
+ comparison_values: list[Any] = list(comparison_ranges_to_values.values())
+ unwrapped_comparison_ranges = zip(*(unwrap_range(rang) for rang in comparison_ranges_to_values), strict=False)
+ zipped_ranges = zip(unwrap_range(value_range), unwrapped_comparison_ranges, strict=True)
+ return [
+ value
+ for value, range_values in zipped_ranges
+ if all(range_value == comparison_values[i] for i, range_value in enumerate(range_values))
+ ]
+
+ summary_start = worksheet.max_row + 1
+
+ summary_rows: list[SalesReportSummaryDefinition] = [
+ SalesReportSummaryDefinition(
+ title="Kaikki kaupat",
+ subtitle="Lukumäärä",
+ func=lambda x: len(unwrap_range(x)),
+ ),
+ SalesReportSummaryDefinition(
+ subtitle="Keskiarvo",
+ func=lambda x: mean(unwrap_range(x) or [0]),
+ ),
+ SalesReportSummaryDefinition(
+ subtitle="Maksimi",
+ func=lambda x: max(unwrap_range(x), default=0),
+ ),
+ SalesReportSummaryDefinition(
+ subtitle="Minimi",
+ func=lambda x: min(unwrap_range(x), default=0),
+ ),
+ ]
+
+ for cost_area in range(1, 5):
+ summary_rows += [
+ None, # empty row
+ SalesReportSummaryDefinition(
+ title=f"Kalleusalue {cost_area}",
+ subtitle="Lukumäärä",
+ func=lambda x, y=cost_area: len(conditional_range(x, **{f"A2:A{last_row}": y})),
+ ),
+ SalesReportSummaryDefinition(
+ subtitle="Keskiarvo",
+ func=lambda x, y=cost_area: mean(conditional_range(x, **{f"A2:A{last_row}": y}) or [0]),
+ ),
+ SalesReportSummaryDefinition(
+ subtitle="Maksimi",
+ func=lambda x, y=cost_area: max(conditional_range(x, **{f"A2:A{last_row}": y}), default=0),
+ ),
+ SalesReportSummaryDefinition(
+ subtitle="Minimi",
+ func=lambda x, y=cost_area: min(conditional_range(x, **{f"A2:A{last_row}": y}), default=0),
+ ),
+ ]
+
+ sales_count_rows: list[int] = []
+
+ for definition in summary_rows:
+ if definition is None:
+ worksheet.append(empty_row)
+ continue
+
+ if definition.subtitle == "Lukumäärä":
+ sales_count_rows.append(worksheet.max_row + 1)
+
+ worksheet.append(
+ SalesAndMaximumPricesReportColumns(
+ cost_area="",
+ postal_code="",
+ apartment_address="",
+ surface_area_square_meter=definition.title,
+ purchase_date=definition.subtitle,
+ total_price=definition.func(f"F2:F{last_row}"),
+ total_price_per_square_meter=definition.func(f"G2:G{last_row}"),
+ maximum_price="",
+ maximum_price_per_square_meter="",
+ ),
+ )
+
+ euro_format = "#,##0\\ \\€"
+ euro_per_square_meter_format = "#,##0.00\\ \\€\\/\\m²"
+ date_format = "DD.MM.YYYY"
+ column_letters = string.ascii_uppercase[: len(column_headers)]
+
+ format_sheet(
+ worksheet,
+ formatting_rules={
+ # Add a border to the header row
+ **{f"{letter}1": {"border": Border(bottom=Side(style="thin"))} for letter in column_letters},
+ # Add a border to the last data row
+ **{f"{letter}{last_row}": {"border": Border(bottom=Side(style="thin"))} for letter in column_letters},
+ # Align the summary titles to the right
+ **{
+ f"E{summary_start + i}": {"alignment": Alignment(horizontal="right")}
+ for i in range(0, len(summary_rows))
+ },
+ "B": {"alignment": Alignment(horizontal="right")},
+ "E": {"number_format": date_format},
+ "F": {"number_format": euro_format},
+ "G": {"number_format": euro_per_square_meter_format},
+ "H": {"number_format": euro_format},
+ "I": {"number_format": euro_per_square_meter_format},
+ # Reset number format for sales count cells
+ **{f"{letter}{row}": {"number_format": "General"} for row in sales_count_rows for letter in "FG"},
+ },
+ )
+
+ resize_columns(worksheet)
+ worksheet.protection.sheet = True
+ return workbook
+
+
def build_regulated_housing_companies_report_excel(
housing_companies: list[HousingCompanyWithRegulatedReportAnnotations],
) -> Workbook:
@@ -675,7 +905,7 @@ def sort_sales_by_cost_area(sales: list[ApartmentSale]) -> SalesByCostArea:
raise ValidationError(
detail={
api_settings.NON_FIELD_ERRORS_KEY: (
- f"Surface are zero or missing for apartment {sale.apartment.address!r}. "
+ f"Surface area zero or missing for apartment {sale.apartment.address!r}. "
f"Cannot calculate price per square meter."
)
},
diff --git a/backend/hitas/tests/apis/test_api_reports.py b/backend/hitas/tests/apis/test_api_reports.py
index dda9788ca..21bb36353 100644
--- a/backend/hitas/tests/apis/test_api_reports.py
+++ b/backend/hitas/tests/apis/test_api_reports.py
@@ -29,6 +29,8 @@
OwnerFactory,
OwnershipFactory,
)
+from hitas.tests.factories.apartment import ApartmentMaximumPriceCalculationFactory
+from hitas.tests.factories.indices import SurfaceAreaPriceCeilingFactory
# Sales report
@@ -412,7 +414,7 @@ def test__api__sales_report__surface_area_missing(api_client: HitasAPIClient):
{
"field": "non_field_errors",
"message": (
- f"Surface are zero or missing for apartment {sale.apartment.address!r}. "
+ f"Surface area zero or missing for apartment {sale.apartment.address!r}. "
f"Cannot calculate price per square meter."
),
}
@@ -423,6 +425,113 @@ def test__api__sales_report__surface_area_missing(api_client: HitasAPIClient):
}
+@pytest.mark.django_db
+def test__api__sales_and_maximum_prices_report(api_client: HitasAPIClient):
+ # Create sales in the report interval
+ sale_1: ApartmentSale = ApartmentSaleFactory.create(
+ purchase_date=datetime.date(2020, 1, 1),
+ purchase_price=50_000,
+ apartment_share_of_housing_company_loans=10_000,
+ apartment__surface_area=100,
+ apartment__building__real_estate__housing_company__postal_code__value="00001",
+ apartment__building__real_estate__housing_company__postal_code__cost_area=1,
+ )
+ sale_2: ApartmentSale = ApartmentSaleFactory.create(
+ purchase_date=datetime.date(2020, 2, 15),
+ purchase_price=130_000,
+ apartment_share_of_housing_company_loans=100,
+ apartment__surface_area=100,
+ apartment__building__real_estate__housing_company__postal_code__value="00002",
+ apartment__building__real_estate__housing_company__postal_code__cost_area=1,
+ )
+ # Valid calculation
+ ApartmentMaximumPriceCalculationFactory.create(
+ apartment=sale_1.apartment,
+ calculation_date=datetime.date(2019, 1, 1),
+ valid_until=datetime.date(2022, 1, 1),
+ maximum_price=150_000,
+ )
+ # Valid calculation but older
+ ApartmentMaximumPriceCalculationFactory.create(
+ apartment=sale_1.apartment,
+ calculation_date=datetime.date(2018, 1, 1),
+ valid_until=datetime.date(2021, 1, 1),
+ maximum_price=140_000,
+ )
+ # Invalid calculation, too old
+ ApartmentMaximumPriceCalculationFactory.create(
+ apartment=sale_1.apartment,
+ calculation_date=datetime.date(2018, 1, 1),
+ valid_until=datetime.date(2019, 1, 1),
+ maximum_price=120_000,
+ )
+ # Invalid calculation, too new
+ ApartmentMaximumPriceCalculationFactory.create(
+ apartment=sale_1.apartment,
+ calculation_date=datetime.date(2018, 1, 1),
+ valid_until=datetime.date(2019, 1, 1),
+ maximum_price=160_000,
+ )
+ # Sale 2 should fall back to using surface area price ceiling when no calculation is found
+ SurfaceAreaPriceCeilingFactory.create(month=datetime.date(2020, 2, 1), value=2000)
+
+ data = {
+ "start_date": "2020-01-01",
+ "end_date": "2020-02-28",
+ }
+ url = reverse("hitas:sales-and-maximum-prices-report-list") + "?" + urlencode(data)
+ response: HttpResponse = api_client.get(url)
+
+ assert response.status_code == status.HTTP_200_OK
+
+ workbook: Workbook = load_workbook(BytesIO(response.content), data_only=False)
+ worksheet: Worksheet = workbook.worksheets[0]
+
+ rows = list(worksheet.values)
+ assert len(rows[0][0]) > 0, "Row 1 column 1 should have a title"
+ assert len(rows[0][1]) > 0, "Row 1 column 2 should have a title"
+ assert len(rows[0][2]) > 0, "Row 1 column 3 should have a title"
+ assert len(rows[0][3]) > 0, "Row 1 column 4 should have a title"
+ assert len(rows[0][4]) > 0, "Row 1 column 5 should have a title"
+ assert len(rows[0][5]) > 0, "Row 1 column 6 should have a title"
+ assert len(rows[0][6]) > 0, "Row 1 column 7 should have a title"
+ # Sale row 1
+ assert rows[1][0] == sale_1.apartment.postal_code.cost_area
+ assert rows[1][1] == sale_1.apartment.postal_code.value
+ assert rows[1][2] == sale_1.apartment.address
+ assert rows[1][3] == sale_1.apartment.surface_area
+ assert rows[1][4] == datetime.datetime.fromisoformat(sale_1.purchase_date.isoformat())
+ assert rows[1][5] == 60_000 # 50_000 € + 10_000 €
+ assert rows[1][6] == 600 # (50_000 € + 10_000 €) / 100 m²
+ assert rows[1][7] == 150_000
+ assert rows[1][8] == 1_500 # 150_000 € / 100 m²
+ # Sale row 2
+ assert rows[2][0] == sale_2.apartment.postal_code.cost_area
+ assert rows[2][1] == sale_2.apartment.postal_code.value
+ assert rows[2][2] == sale_2.apartment.address
+ assert rows[2][3] == sale_2.apartment.surface_area
+ assert rows[2][4] == datetime.datetime.fromisoformat(sale_2.purchase_date.isoformat())
+ assert rows[2][5] == 130_100
+ assert rows[2][6] == 1_301 # (130_000 + 100) / 100 m²
+ assert rows[2][7] == 200_000 # 2000 € * 100 m²
+ assert rows[2][8] == 2_000
+ # Totals - all sales
+ assert rows[4][5] == 2, "Total amount of sales should be 2"
+ assert rows[5][5] == 95_050, "Mean should be 95_050"
+ assert rows[6][5] == 130_100, "Maximum should be 130_100"
+ assert rows[7][5] == 60_000, "Minimum should be 60_000"
+ # Totals - cost area 1
+ assert rows[9][5] == 2, "Total amount of sales in cost area 1 should be 2"
+ assert rows[10][5] == 95_050, "Mean should be 95_050"
+ assert rows[11][5] == 130_100, "Maximum should be 130_100"
+ assert rows[12][5] == 60_000, "Minimum should be 60_000"
+ # Totals - cost area 2
+ assert rows[14][5] == 0, "Total amount of sales in cost area 2 should be 0"
+ assert rows[15][5] == 0, "Mean should be 0"
+ assert rows[16][5] == 0, "Maximum should be 0"
+ assert rows[17][5] == 0, "Minimum should be 0"
+
+
# Regulated housing companies report
@@ -1203,7 +1312,7 @@ def test__api__sales_by_area_report__no_surface_area(api_client: HitasAPIClient)
{
"field": "non_field_errors",
"message": (
- f"Surface are zero or missing for apartment {sale.apartment.address!r}. "
+ f"Surface area zero or missing for apartment {sale.apartment.address!r}. "
f"Cannot calculate price per square meter."
),
}
diff --git a/backend/hitas/urls.py b/backend/hitas/urls.py
index d326f2960..00d5eb5b0 100644
--- a/backend/hitas/urls.py
+++ b/backend/hitas/urls.py
@@ -78,6 +78,13 @@
# /api/v1/reports/download-sales-report
router.register(r"reports/download-sales-report", views.SalesReportView, basename="sales-report")
+# /api/v1/reports/download-sales-and-maximum-prices-report
+router.register(
+ r"reports/download-sales-and-maximum-prices-report",
+ views.SalesAndMaximumPricesReportView,
+ basename="sales-and-maximum-prices-report",
+)
+
# /api/v1/reports/download-regulated-housing-companies-report
router.register(
r"reports/download-regulated-housing-companies-report",
diff --git a/backend/hitas/views/__init__.py b/backend/hitas/views/__init__.py
index 49e57c545..bdb3686fb 100644
--- a/backend/hitas/views/__init__.py
+++ b/backend/hitas/views/__init__.py
@@ -38,6 +38,7 @@
OwnershipsByHousingCompanyReport,
RegulatedHousingCompaniesReportView,
RegulatedOwnershipsReportView,
+ SalesAndMaximumPricesReportView,
SalesByPostalCodeAndAreaReportView,
SalesReportView,
UnregulatedHousingCompaniesReportView,
diff --git a/backend/hitas/views/reports.py b/backend/hitas/views/reports.py
index 9e55e7c0f..ebd8af1be 100644
--- a/backend/hitas/views/reports.py
+++ b/backend/hitas/views/reports.py
@@ -26,6 +26,7 @@
build_owners_by_housing_companies_report_excel,
build_regulated_housing_companies_report_excel,
build_regulated_ownerships_report_excel,
+ build_sales_and_maximum_prices_report_excel,
build_sales_by_postal_code_and_area_report_excel,
build_sales_report_excel,
build_unregulated_housing_companies_report_excel,
@@ -64,6 +65,22 @@ def list(self, request: Request, *args, **kwargs) -> HttpResponse:
return get_excel_response(filename=filename, excel=workbook)
+class SalesAndMaximumPricesReportView(ViewSet):
+ renderer_classes = [HitasJSONRenderer, ExcelRenderer]
+
+ def list(self, request: Request, *args, **kwargs) -> HttpResponse:
+ serializer = SalesReportSerializer(data=request.query_params)
+ serializer.is_valid(raise_exception=True)
+
+ start: datetime.date = serializer.validated_data["start_date"]
+ end: datetime.date = serializer.validated_data["end_date"]
+
+ sales = find_sales_on_interval_for_reporting(start_date=start, end_date=end)
+ workbook = build_sales_and_maximum_prices_report_excel(sales)
+ filename = f"Hitas kauppa- ja enimmäishinnat aikavälillä {start.isoformat()} - {end.isoformat()}.xlsx"
+ return get_excel_response(filename=filename, excel=workbook)
+
+
class RegulatedHousingCompaniesReportView(ViewSet):
renderer_classes = [HitasJSONRenderer, ExcelRenderer]
diff --git a/backend/openapi.yaml b/backend/openapi.yaml
index ebbb61944..cdd2ae314 100644
--- a/backend/openapi.yaml
+++ b/backend/openapi.yaml
@@ -4532,6 +4532,42 @@ paths:
"500":
$ref: "#/components/responses/InternalServerError"
+ /api/v1/reports/download-sales-and-maximum-prices-report:
+ get:
+ description: Download hitas sales and maximum prices on a defined interval as an Excel report
+ operationId: fetch-sales-and-maximum-prices-report-excel
+ tags:
+ - Reports
+ parameters:
+ - name: start_date
+ required: true
+ in: query
+ description: Start date for the sales report
+ schema:
+ type: string
+ example: 2022-02-01
+ - name: end_date
+ required: true
+ in: query
+ description: End date for the sales report
+ schema:
+ type: string
+ example: 2023-02-01
+ responses:
+ "200":
+ description: Successfully downloaded a sales and maximum prices report
+ 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-regulated-housing-companies-report:
get:
description: Download an Excel report of hitas regulated housing companies
diff --git a/frontend/src/common/services/hitasApi/reports.ts b/frontend/src/common/services/hitasApi/reports.ts
index 92fe045ad..5335b1b92 100644
--- a/frontend/src/common/services/hitasApi/reports.ts
+++ b/frontend/src/common/services/hitasApi/reports.ts
@@ -47,6 +47,12 @@ export const downloadSalesReportPDF = ({startDate, endDate}: {startDate: string;
fetchAndDownloadPDF(url);
};
+export const downloadSalesAndMaximumPricesReportPDF = ({startDate, endDate}: {startDate: string; endDate: string}) => {
+ const params = `start_date=${startDate}&end_date=${endDate}`;
+ const url = `/reports/download-sales-and-maximum-prices-report?${params}`;
+ fetchAndDownloadPDF(url);
+};
+
export const downloadSalesByPostalCodeAndAreaReportPDF = ({
startDate,
endDate,
diff --git a/frontend/src/features/reports/ReportsPage.tsx b/frontend/src/features/reports/ReportsPage.tsx
index 39c780899..be9983270 100644
--- a/frontend/src/features/reports/ReportsPage.tsx
+++ b/frontend/src/features/reports/ReportsPage.tsx
@@ -10,6 +10,7 @@ import OwnerReports from "./components/OwnerReports";
import {
FirstSalesReportByAreas,
ReSalesReportByAreas,
+ SalesAndMaximumPricesReport,
SalesReportAll,
SalesReportByAreas,
} from "./components/SalesReports";
@@ -28,6 +29,8 @@ const ReportsPage = () => {
+
+
diff --git a/frontend/src/features/reports/components/SalesReports.tsx b/frontend/src/features/reports/components/SalesReports.tsx
index 02b6211b4..713808f7e 100644
--- a/frontend/src/features/reports/components/SalesReports.tsx
+++ b/frontend/src/features/reports/components/SalesReports.tsx
@@ -5,6 +5,7 @@ import {SalesReportFormSchema} from "../../../common/schemas";
import {
downloadFirstSalesByPostalCodeAndAreaReportPDF,
downloadReSalesByPostalCodeAndAreaReportPDF,
+ downloadSalesAndMaximumPricesReportPDF,
downloadSalesByPostalCodeAndAreaReportPDF,
downloadSalesReportPDF,
} from "../../../common/services";
@@ -65,6 +66,15 @@ export const SalesReportAll = () => {
);
};
+export const SalesAndMaximumPricesReport = () => {
+ return (
+
+ );
+};
+
export const SalesReportByAreas = () => {
return (