mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
added eco packages to integration test
This commit is contained in:
parent
070d7d332c
commit
0170272abd
2 changed files with 268 additions and 28 deletions
|
|
@ -80,7 +80,8 @@ DESCRIPTIONS_TO_FUEL_TYPES = {
|
||||||
},
|
},
|
||||||
"Electric heat pump for water heating only": {"fuel": "Electricity", "cop": 1},
|
"Electric heat pump for water heating only": {"fuel": "Electricity", "cop": 1},
|
||||||
"Ground source heat pump, warm air, electric": {"fuel": "Electricity", "cop": AVERAGE_ASHP_EFFICIENCY / 100},
|
"Ground source heat pump, warm air, electric": {"fuel": "Electricity", "cop": AVERAGE_ASHP_EFFICIENCY / 100},
|
||||||
"Room heaters, mains gas, Electric storage heaters": {"fuel": "Natural Gas", "cop": 0.85}
|
"Room heaters, mains gas, Electric storage heaters": {"fuel": "Natural Gas", "cop": 0.85},
|
||||||
|
"Water source heat pump, radiators, electric": {"fuel": "Electricity", "cop": AVERAGE_ASHP_EFFICIENCY / 100},
|
||||||
}
|
}
|
||||||
|
|
||||||
# These are the measure types where if there is a ventilation recommendation, we force the inclusion of it
|
# These are the measure types where if there is a ventilation recommendation, we force the inclusion of it
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,11 @@ body = PlanTriggerRequest(
|
||||||
'sheet_name': None, 'sheet_count': None, 'index_start': None, 'index_end': None}
|
'sheet_name': None, 'sheet_count': None, 'index_start': None, 'index_end': None}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
eco_packages = {}
|
||||||
|
# For testing
|
||||||
|
for p in input_properties:
|
||||||
|
eco_packages[p.id] = (None, None, None)
|
||||||
|
|
||||||
for p in tqdm(input_properties):
|
for p in tqdm(input_properties):
|
||||||
if not recommendations.get(p.id):
|
if not recommendations.get(p.id):
|
||||||
continue
|
continue
|
||||||
|
|
@ -327,16 +332,16 @@ for p in tqdm(input_properties):
|
||||||
fixed_gain = optimiser_functions.calculate_fixed_gain(
|
fixed_gain = optimiser_functions.calculate_fixed_gain(
|
||||||
property_required_measures, recommendations, p, needs_ventilation
|
property_required_measures, recommendations, p, needs_ventilation
|
||||||
)
|
)
|
||||||
gain = optimiser_functions.calculate_gain(body=body, p=p, fixed_gain=fixed_gain)
|
gain = optimiser_functions.calculate_gain(body=body, p=p, fixed_gain=fixed_gain, eco_packages=eco_packages)
|
||||||
|
|
||||||
funding = Funding(
|
funding = Funding(
|
||||||
tenure="Social",
|
tenure=body.housing_type,
|
||||||
project_scores_matrix=project_scores_matrix,
|
project_scores_matrix=project_scores_matrix,
|
||||||
partial_project_scores_matrix=partial_project_scores_matrix,
|
partial_project_scores_matrix=partial_project_scores_matrix,
|
||||||
whlg_eligible_postcodes=whlg_eligible_postcodes,
|
whlg_eligible_postcodes=whlg_eligible_postcodes,
|
||||||
eco4_social_cavity_abs_rate=12.5,
|
eco4_social_cavity_abs_rate=13,
|
||||||
eco4_social_solid_abs_rate=17,
|
eco4_social_solid_abs_rate=17,
|
||||||
eco4_private_cavity_abs_rate=12.5,
|
eco4_private_cavity_abs_rate=13,
|
||||||
eco4_private_solid_abs_rate=17,
|
eco4_private_solid_abs_rate=17,
|
||||||
gbis_social_cavity_abs_rate=21,
|
gbis_social_cavity_abs_rate=21,
|
||||||
gbis_social_solid_abs_rate=25,
|
gbis_social_solid_abs_rate=25,
|
||||||
|
|
@ -380,7 +385,7 @@ for p in tqdm(input_properties):
|
||||||
r["uplift_project_score"]
|
r["uplift_project_score"]
|
||||||
) = funding.get_innovation_uplift(
|
) = funding.get_innovation_uplift(
|
||||||
measure=r,
|
measure=r,
|
||||||
starting_sap=p.data["current-energy-efficiency"],
|
starting_sap=int(p.data["current-energy-efficiency"]),
|
||||||
floor_area=p.floor_area,
|
floor_area=p.floor_area,
|
||||||
is_cavity=p.walls["is_cavity_wall"],
|
is_cavity=p.walls["is_cavity_wall"],
|
||||||
current_wall_uvalue=current_wall_u_value,
|
current_wall_uvalue=current_wall_u_value,
|
||||||
|
|
@ -391,8 +396,16 @@ for p in tqdm(input_properties):
|
||||||
mainheat_energy_eff=p.data["mainheat-energy-eff"],
|
mainheat_energy_eff=p.data["mainheat-energy-eff"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if r["already_installed"]:
|
||||||
|
# if already installed, we zero out the uplift and funding
|
||||||
|
(r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"],
|
||||||
|
r["uplift_project_score"]) = (
|
||||||
|
0, 0, 0, 0
|
||||||
|
)
|
||||||
|
|
||||||
input_measures = optimiser_functions.prepare_input_measures(
|
input_measures = optimiser_functions.prepare_input_measures(
|
||||||
measures_to_optimise_with_uplift, body.goal, needs_ventilation, funding=True
|
measures_to_optimise_with_uplift, body.goal, needs_ventilation, funding=True,
|
||||||
|
property_eco_packages=eco_packages.get(p.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# When the goal is Increasing EPC, we can run the funding optimiser
|
# When the goal is Increasing EPC, we can run the funding optimiser
|
||||||
|
|
@ -404,20 +417,14 @@ for p in tqdm(input_properties):
|
||||||
housing_type=body.housing_type,
|
housing_type=body.housing_type,
|
||||||
budget=body.budget,
|
budget=body.budget,
|
||||||
target_gain=gain,
|
target_gain=gain,
|
||||||
funding=funding
|
funding=funding,
|
||||||
|
work_package=eco_packages[p.id][2]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Given the solutions we select the optimal one
|
# If the solution isn't eligible, we can't really consider it
|
||||||
solutions["cost_less_full_project_funding"] = np.where(
|
solutions = solutions[
|
||||||
solutions["scheme"] == "eco4",
|
(solutions["is_eligible"] & (solutions["scheme"] != "none")) | (solutions["scheme"] == "none")
|
||||||
solutions["total_cost"] - solutions["full_project_funding"] - solutions["total_uplift"],
|
]
|
||||||
solutions["total_cost"] - solutions["partial_project_funding"] - solutions["total_uplift"]
|
|
||||||
)
|
|
||||||
|
|
||||||
solutions["cost_less_full_project_funding"] = (
|
|
||||||
solutions["total_cost"] - solutions["full_project_funding"] - solutions["total_uplift"]
|
|
||||||
)
|
|
||||||
solutions = solutions.sort_values("cost_less_full_project_funding", ascending=True)
|
|
||||||
|
|
||||||
if solutions["meets_upgrade_target"].any():
|
if solutions["meets_upgrade_target"].any():
|
||||||
# If we have a solution that meets the upgrade target, we select that one
|
# If we have a solution that meets the upgrade target, we select that one
|
||||||
|
|
@ -428,9 +435,13 @@ for p in tqdm(input_properties):
|
||||||
|
|
||||||
# This is the list of measures that we will recommend
|
# This is the list of measures that we will recommend
|
||||||
scheme = optimal_solution["scheme"]
|
scheme = optimal_solution["scheme"]
|
||||||
funded_measures = optimal_solution["items"] if scheme != "none" else []
|
|
||||||
solution = optimal_solution["items"] + optimal_solution["unfunded_items"]
|
# We create this full list of selected measures, which is used in the next section for setting
|
||||||
# This is the total amount of funding that the project will produce (including uplifts) (£)
|
# default measures
|
||||||
|
solution = deepcopy(optimal_solution["items"]) + deepcopy(optimal_solution["unfunded_items"])
|
||||||
|
funded_measures = deepcopy(optimal_solution["items"]) if scheme != "none" else []
|
||||||
|
|
||||||
|
# This is the total amount of funding that the project will produce (EXCLUDING uplifts) (£)
|
||||||
project_funding = optimal_solution["full_project_funding"] if scheme == "eco4" else \
|
project_funding = optimal_solution["full_project_funding"] if scheme == "eco4" else \
|
||||||
optimal_solution["partial_project_funding"]
|
optimal_solution["partial_project_funding"]
|
||||||
# This is the total amount of funding associated to the uplift (£)
|
# This is the total amount of funding associated to the uplift (£)
|
||||||
|
|
@ -470,8 +481,8 @@ for p in tqdm(input_properties):
|
||||||
|
|
||||||
funding.check_funding(
|
funding.check_funding(
|
||||||
measures=solution,
|
measures=solution,
|
||||||
starting_sap=p.data["current-energy-efficiency"],
|
starting_sap=int(p.data["current-energy-efficiency"]),
|
||||||
ending_sap=p.data["current-energy-efficiency"] + sum([x["gain"] for x in solution]),
|
ending_sap=int(p.data["current-energy-efficiency"]) + sum([x["gain"] for x in solution]),
|
||||||
floor_area=p.floor_area,
|
floor_area=p.floor_area,
|
||||||
mainheat_description=p.main_heating["clean_description"],
|
mainheat_description=p.main_heating["clean_description"],
|
||||||
heating_control_description=p.main_heating_controls["clean_description"],
|
heating_control_description=p.main_heating_controls["clean_description"],
|
||||||
|
|
@ -510,10 +521,10 @@ for p in tqdm(input_properties):
|
||||||
|
|
||||||
# Add best practice measures (ventilation/trickle vents)
|
# Add best practice measures (ventilation/trickle vents)
|
||||||
selected = optimiser_functions.add_best_practice_measures(p.id, solution, recommendations, selected)
|
selected = optimiser_functions.add_best_practice_measures(p.id, solution, recommendations, selected)
|
||||||
# Final flattening - Don't do this!
|
# Final flattening
|
||||||
# recommendations[p.id] = optimiser_functions.flatten_recommendations_with_defaults(
|
recommendations[p.id] = optimiser_functions.flatten_recommendations_with_defaults(
|
||||||
# p.id, recommendations, selected
|
p.id, recommendations, selected
|
||||||
# )
|
)
|
||||||
|
|
||||||
# TODO: functionise
|
# TODO: functionise
|
||||||
for measure in funded_measures:
|
for measure in funded_measures:
|
||||||
|
|
@ -529,3 +540,231 @@ for p in tqdm(input_properties):
|
||||||
partial_project_score=partial_project_score,
|
partial_project_score=partial_project_score,
|
||||||
uplift_project_score=uplift_project_score
|
uplift_project_score=uplift_project_score
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# for p in tqdm(input_properties):
|
||||||
|
# if not recommendations.get(p.id):
|
||||||
|
# continue
|
||||||
|
#
|
||||||
|
# # we need to double unlist because we have a list of lists
|
||||||
|
# property_measure_types = {rec["type"] for recs in recommendations[p.id] for rec in recs}
|
||||||
|
# property_required_measures = [m for m in recommendations[p.id] if m[0]["type"] in body.required_measures]
|
||||||
|
# measures_to_optimise = [m for m in recommendations[p.id] if m[0]["type"] not in body.required_measures]
|
||||||
|
#
|
||||||
|
# # If a measure requiring ventilation is selected, and the property does not have ventilation, we enfore
|
||||||
|
# # its inclusion
|
||||||
|
# needs_ventilation = any(
|
||||||
|
# x in property_measure_types for x in assumptions.measures_needing_ventilation
|
||||||
|
# ) and not p.has_ventilation
|
||||||
|
#
|
||||||
|
# if not measures_to_optimise:
|
||||||
|
# # Nothing to do, we just reshape the recommendations
|
||||||
|
# recommendations[p.id] = optimiser_functions.flatten_recommendations_with_defaults(
|
||||||
|
# p.id, recommendations, set()
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
#
|
||||||
|
# fixed_gain = optimiser_functions.calculate_fixed_gain(
|
||||||
|
# property_required_measures, recommendations, p, needs_ventilation
|
||||||
|
# )
|
||||||
|
# gain = optimiser_functions.calculate_gain(body=body, p=p, fixed_gain=fixed_gain)
|
||||||
|
#
|
||||||
|
# funding = Funding(
|
||||||
|
# tenure="Social",
|
||||||
|
# project_scores_matrix=project_scores_matrix,
|
||||||
|
# partial_project_scores_matrix=partial_project_scores_matrix,
|
||||||
|
# whlg_eligible_postcodes=whlg_eligible_postcodes,
|
||||||
|
# eco4_social_cavity_abs_rate=12.5,
|
||||||
|
# eco4_social_solid_abs_rate=17,
|
||||||
|
# eco4_private_cavity_abs_rate=12.5,
|
||||||
|
# eco4_private_solid_abs_rate=17,
|
||||||
|
# gbis_social_cavity_abs_rate=21,
|
||||||
|
# gbis_social_solid_abs_rate=25,
|
||||||
|
# gbis_private_cavity_abs_rate=21,
|
||||||
|
# gbis_private_solid_abs_rate=28,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# li_thickness = convert_thickness_to_numeric(
|
||||||
|
# p.roof["insulation_thickness"], p.roof["is_pitched"], p.roof["is_flat"]
|
||||||
|
# )
|
||||||
|
# current_wall_u_value = p.walls["thermal_transmittance"]
|
||||||
|
# if current_wall_u_value is None:
|
||||||
|
# current_wall_u_value = get_wall_u_value(
|
||||||
|
# clean_description=p.walls["clean_description"],
|
||||||
|
# age_band=p.age_band,
|
||||||
|
# is_granite_or_whinstone=p.walls["is_granite_or_whinstone"],
|
||||||
|
# is_sandstone_or_limestone=p.walls["is_sandstone_or_limestone"],
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # We insert the innovation uplift
|
||||||
|
# measures_to_optimise_with_uplift = deepcopy(measures_to_optimise)
|
||||||
|
#
|
||||||
|
# # TODO: Turn this into a function and store the innovaiton uplift
|
||||||
|
# for group in measures_to_optimise_with_uplift:
|
||||||
|
# for r in group:
|
||||||
|
#
|
||||||
|
# if r["type"] in ["mechanical_ventilation", "low_energy_lighting", "secondary_heating",
|
||||||
|
# "extension_cavity_wall_insulation", "draught_proofing", "sealing_open_fireplace"]:
|
||||||
|
# (
|
||||||
|
# r["partial_project_score"],
|
||||||
|
# r["partial_project_funding"],
|
||||||
|
# r["innovation_uplift"],
|
||||||
|
# r["uplift_project_score"],
|
||||||
|
# ) = (
|
||||||
|
# 0, 0, 0, 0
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
#
|
||||||
|
# (
|
||||||
|
# r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"],
|
||||||
|
# r["uplift_project_score"]
|
||||||
|
# ) = funding.get_innovation_uplift(
|
||||||
|
# measure=r,
|
||||||
|
# starting_sap=p.data["current-energy-efficiency"],
|
||||||
|
# floor_area=p.floor_area,
|
||||||
|
# is_cavity=p.walls["is_cavity_wall"],
|
||||||
|
# current_wall_uvalue=current_wall_u_value,
|
||||||
|
# is_partial="partial" in p.walls["clean_description"].lower(),
|
||||||
|
# existing_li_thickness=li_thickness,
|
||||||
|
# mainheating=p.main_heating,
|
||||||
|
# main_fuel=p.main_fuel,
|
||||||
|
# mainheat_energy_eff=p.data["mainheat-energy-eff"],
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# input_measures = optimiser_functions.prepare_input_measures(
|
||||||
|
# measures_to_optimise_with_uplift, body.goal, needs_ventilation, funding=True
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # When the goal is Increasing EPC, we can run the funding optimiser
|
||||||
|
# if body.goal == "Increasing EPC":
|
||||||
|
#
|
||||||
|
# solutions = optimise_with_funding_paths(
|
||||||
|
# p=p,
|
||||||
|
# input_measures=input_measures,
|
||||||
|
# housing_type=body.housing_type,
|
||||||
|
# budget=body.budget,
|
||||||
|
# target_gain=gain,
|
||||||
|
# funding=funding
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Given the solutions we select the optimal one
|
||||||
|
# solutions["cost_less_full_project_funding"] = np.where(
|
||||||
|
# solutions["scheme"] == "eco4",
|
||||||
|
# solutions["total_cost"] - solutions["full_project_funding"] - solutions["total_uplift"],
|
||||||
|
# solutions["total_cost"] - solutions["partial_project_funding"] - solutions["total_uplift"]
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# solutions["cost_less_full_project_funding"] = (
|
||||||
|
# solutions["total_cost"] - solutions["full_project_funding"] - solutions["total_uplift"]
|
||||||
|
# )
|
||||||
|
# solutions = solutions.sort_values("cost_less_full_project_funding", ascending=True)
|
||||||
|
#
|
||||||
|
# if solutions["meets_upgrade_target"].any():
|
||||||
|
# # If we have a solution that meets the upgrade target, we select that one
|
||||||
|
# optimal_solution = solutions[solutions["meets_upgrade_target"]].iloc[0]
|
||||||
|
# else:
|
||||||
|
# # Pick the cheapest
|
||||||
|
# optimal_solution = solutions.iloc[0]
|
||||||
|
#
|
||||||
|
# # This is the list of measures that we will recommend
|
||||||
|
# scheme = optimal_solution["scheme"]
|
||||||
|
# funded_measures = optimal_solution["items"] if scheme != "none" else []
|
||||||
|
# solution = optimal_solution["items"] + optimal_solution["unfunded_items"]
|
||||||
|
# # This is the total amount of funding that the project will produce (including uplifts) (£)
|
||||||
|
# project_funding = optimal_solution["full_project_funding"] if scheme == "eco4" else \
|
||||||
|
# optimal_solution["partial_project_funding"]
|
||||||
|
# # This is the total amount of funding associated to the uplift (£)
|
||||||
|
# total_uplift = optimal_solution["total_uplift"]
|
||||||
|
# # This is the funding scheme selected
|
||||||
|
# # This is the full project ABS
|
||||||
|
# full_project_score = optimal_solution["project_score"]
|
||||||
|
# # This is the partial project ABS
|
||||||
|
# partial_project_score = optimal_solution["partial_project_score"]
|
||||||
|
# # This is the uplift score ABS
|
||||||
|
# uplift_project_score = optimal_solution["total_uplift_score"]
|
||||||
|
# else:
|
||||||
|
# # We optimise and then we determine eligibility for funding, based on the measures selected
|
||||||
|
# optimiser = (
|
||||||
|
# GainOptimiser(
|
||||||
|
# input_measures, max_cost=body.budget, max_gain=gain, allow_slack=False
|
||||||
|
# ) if body.budget else CostOptimiser(input_measures, min_gain=gain)
|
||||||
|
# )
|
||||||
|
# optimiser.setup()
|
||||||
|
# optimiser.solve()
|
||||||
|
# solution = optimiser.solution
|
||||||
|
#
|
||||||
|
# recommendation_types = []
|
||||||
|
# for measures in input_measures:
|
||||||
|
# for measure in measures:
|
||||||
|
# recommendation_types.append(measure["type"])
|
||||||
|
# recommendation_types = set(recommendation_types)
|
||||||
|
#
|
||||||
|
# has_wall_insulation_recommendation = any(
|
||||||
|
# (m in recommendation_types or "+".join([m, "mechanical_ventilation"])) for m in
|
||||||
|
# WALL_INSULATION_MEASURES
|
||||||
|
# )
|
||||||
|
# has_roof_insulation_recommendation = any(
|
||||||
|
# (m in recommendation_types or "+".join([m, "mechanical_ventilation"])) for m in
|
||||||
|
# ROOF_INSULATION_MEASURES
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# funding.check_funding(
|
||||||
|
# measures=solution,
|
||||||
|
# starting_sap=p.data["current-energy-efficiency"],
|
||||||
|
# ending_sap=p.data["current-energy-efficiency"] + sum([x["gain"] for x in solution]),
|
||||||
|
# floor_area=p.floor_area,
|
||||||
|
# mainheat_description=p.main_heating["clean_description"],
|
||||||
|
# heating_control_description=p.main_heating_controls["clean_description"],
|
||||||
|
# is_cavity=p.walls["is_cavity_wall"],
|
||||||
|
# current_wall_uvalue=current_wall_u_value,
|
||||||
|
# is_partial="partial" in p.walls["clean_description"].lower(),
|
||||||
|
# existing_li_thickness=li_thickness,
|
||||||
|
# mainheating=p.main_heating,
|
||||||
|
# main_fuel=p.main_fuel,
|
||||||
|
# mainheat_energy_eff=p.data["mainheat-energy-eff"],
|
||||||
|
# has_wall_insulation_recommendation=has_wall_insulation_recommendation,
|
||||||
|
# has_roof_insulation_recommendation=has_roof_insulation_recommendation,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Determine the scheme
|
||||||
|
# scheme = "none"
|
||||||
|
# if funding.eco4_eligible:
|
||||||
|
# scheme = "eco4"
|
||||||
|
# if scheme == "none" and funding.gbis_eligible:
|
||||||
|
# scheme = "gbis"
|
||||||
|
#
|
||||||
|
# funded_measures = solution if scheme in ["gbis", "eco4"] else []
|
||||||
|
# project_funding = 0 if funding.full_project_abs is not None else funding.full_project_abs
|
||||||
|
# total_uplift = funding.eco4_uplift
|
||||||
|
# full_project_score = 0 if funding.full_project_abs is not None else funding.full_project_abs
|
||||||
|
# partial_project_score = funding.partial_project_abs
|
||||||
|
# uplift_project_score = funding.eco4_uplift if scheme == "eco4" else funding.gbis_uplift
|
||||||
|
#
|
||||||
|
# selected = {r["id"] for r in solution}
|
||||||
|
#
|
||||||
|
# if property_required_measures:
|
||||||
|
# solution = optimiser_functions.add_required_measures(
|
||||||
|
# property_id=p.id, property_required_measures=property_required_measures,
|
||||||
|
# recommendations=recommendations, selected=selected,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Add best practice measures (ventilation/trickle vents)
|
||||||
|
# selected = optimiser_functions.add_best_practice_measures(p.id, solution, recommendations, selected)
|
||||||
|
# # Final flattening - Don't do this!
|
||||||
|
# # recommendations[p.id] = optimiser_functions.flatten_recommendations_with_defaults(
|
||||||
|
# # p.id, recommendations, selected
|
||||||
|
# # )
|
||||||
|
#
|
||||||
|
# # TODO: functionise
|
||||||
|
# for measure in funded_measures:
|
||||||
|
# if "+mechanical_ventilation" in measure["type"]:
|
||||||
|
# measure["type"] = measure["type"].split("+mechanical_ventilation")[0]
|
||||||
|
#
|
||||||
|
# p.insert_funding(
|
||||||
|
# scheme=scheme,
|
||||||
|
# funded_measures=funded_measures,
|
||||||
|
# project_funding=project_funding,
|
||||||
|
# total_uplift=total_uplift,
|
||||||
|
# full_project_score=full_project_score,
|
||||||
|
# partial_project_score=partial_project_score,
|
||||||
|
# uplift_project_score=uplift_project_score
|
||||||
|
# )
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue