diff --git a/asset_list/AssetList.py b/asset_list/AssetList.py index d4288114..25a40f99 100644 --- a/asset_list/AssetList.py +++ b/asset_list/AssetList.py @@ -376,6 +376,7 @@ class AssetList: } self.variable_mappings = {} + self.hubspot_data = None self.rename_map = {} self.keep_variables = [] @@ -1526,3 +1527,221 @@ class AssetList: flat_data = pd.DataFrame(flat_data) self.flat_data = flat_data + + def prepare_for_crm(self, contact_details, company_domain, crm_pipeline_name, first_dealstage, assigned_surveyors): + """ + This function prepares the data for upload into Hubspot + :return: + """ + # This is a placeholder for now + + # This maps the opportunities as we reference them, to the product data as stored in Hubspot + product_lookup_table = { + "Non-Intrusive Data Showed Cavity Extraction": { + "name": "Extract & Fill - ECO4", "id": 100307905778, "unit_price": 500 + }, + "Non-Intrusive Data Showed Empty Cavity": { + "name": "Empty Cavity & Loft - ECO4", "id": 82733738177, "unit_price": 1000 + }, + "Non-Intrusive Data Showed Empty Cavity but all SAP scores allowed": { + "name": "Empty Cavity & Loft - ECO4", "id": 82733738177, "unit_price": 1000 + }, + "Non-Intrusive Data Showed Cavity Extraction but all SAP scores allowed": { + "name": "Extract & Fill - ECO4", "id": 100307905778, "unit_price": 500 + }, + "EPC Data Showed Empty Cavity": { + "name": "Empty Cavity & Loft - ECO4", "id": 82733738177, "unit_price": 1000 + }, + "Solid Floor, Insulated, No Solar": { + "name": "Solar PV - ECO4", "id": 82623589564, "unit_price": 1608 + }, + "Solid Floor, Insulated, Needs Loft": { + "name": "Solar PV - ECO4", "id": 82623589564, "unit_price": 1608 + }, + "Other Floor, Insulated, No Solar": { + "name": "Solar PV - ECO4", "id": 82623589564, "unit_price": 1608 + }, + "Other Floor, Insulated, Needs Loft": { + "name": "Solar PV - ECO4", "id": 82623589564, "unit_price": 1608 + } + } + # We check if all products are covered in the lookup table + cavity_products = self.standardised_asset_list["cavity_reason"].unique() + solar_products = self.standardised_asset_list["solar_reason"].unique() + # Check if there any options not in out lookup table + if ( + any(x for x in cavity_products if x not in product_lookup_table) or + any(x for x in solar_products if x not in product_lookup_table) + ): + raise ValueError("We have products not referenced in the lookup table - check this") + + programme_data = self.standardised_asset_list.copy() + + # Exclusions - these are properties we won't treat for the moment + product_exclusions = [ + "Other Floor, Insulated, No Solar", + "Other Floor, Insulated, Needs Loft" + ] + if product_exclusions: + logger.warning("Excluding products: %s", product_exclusions) + + programme_data = programme_data[programme_data["solar_reason"].isin(product_exclusions) == False] + + # Merge on the contact details + programme_data = programme_data.merge( + contact_details, + how="left", + left_on=self.STANDARD_LANDLORD_PROPERTY_ID, + right_on=self.landlord_property_id, + ) + + programme_data["Company Domain Name "] = company_domain + # Append the product data onto the programme data + programme_data["cavity_product"] = programme_data["cavity_reason"].map( + lambda x: product_lookup_table.get(x, {"name": None})["name"] + ) + programme_data["solar_product"] = programme_data["solar_reason"].map( + lambda x: product_lookup_table.get(x, {"name": None})["name"] + ) + + programme_data["domna_product"] = programme_data["solar_reason"].copy() + programme_data["domna_product"] = np.where( + pd.isnull(programme_data["domna_product"]), + programme_data["solar_product"], + programme_data["domna_product"] + ) + # We filter just on rows where we have a product + programme_data = programme_data[ + ~pd.isnull(programme_data["domna_product"]) + ] + programme_data = programme_data.drop(columns=["solar_product", "cavity_product"]) + + product_df = ( + pd.DataFrame(product_lookup_table).T[["name", "id", "unit_price"]] + .reset_index() + .rename( + columns={ + "name": "Name ", + "id": 'Product ID ', + "unit_price": 'Unit price ', + "index": "domna_product" + } + ) + ) + + product_df['Quantity '] = 1 + + # Append on the product data + programme_data = programme_data.merge( + product_df, + how="left", + on="domna_product", + ) + + # Add in deal and pipeline information + programme_data["dealname"] = programme_data[self.STANDARD_FULL_ADDRESS] + " : " + programme_data[ + "domna_product"] + programme_data['Pipeline '] = crm_pipeline_name + programme_data['Deal Stage '] = first_dealstage + programme_data['Associations: Listing'] = "Property Owner" + + programme_data = programme_data.merge( + assigned_surveyors.rename( + columns={self.landlord_property_id: self.STANDARD_LANDLORD_PROPERTY_ID} + ), how="left", on=self.STANDARD_LANDLORD_PROPERTY_ID + ) + + # This maps the hubspot schema to the template. Anything that is not covered in this will be flagged + schema_mappings = { + 'Name ': self.DOMNA_PROPERTY_ID, # TODO: Maybe change this? + 'Company Domain Name ': 'Company Domain Name ', + 'Email ': 'email', # TODO: Review + 'First Name ': 'first name', # TODO: Review + 'Last Name ': 'last name', # TODO: Review + 'Phone ': 'phone', # TODO: Review + 'Full Address ': self.STANDARD_FULL_ADDRESS, + 'Address 1 ': self.STANDARD_ADDRESS_1, + 'Address 2 ': None, # TODO: Don't have this for the moment + 'Postcode ': self.STANDARD_POSTCODE, + 'Property Type ': self.STANDARD_PROPERTY_TYPE, + 'Property Sub Type ': None, # TODO: Don't have this for the moment + 'Bedroom(s) ': None, # TODO: Don't have this for the moment + 'Domna Property ID ': self.DOMNA_PROPERTY_ID, + 'National UPRN ': ( + self.STANDARD_UPRN if self.STANDARD_UPRN is not None else self.EPC_API_DATA_NAMES["uprn"] + ), + 'Owner Property ID ': self.STANDARD_LANDLORD_PROPERTY_ID, + 'Wall Construction ': self.STANDARD_WALL_CONSTRUCTION, + 'Heating System ': self.STANDARD_HEATING_SYSTEM, + 'Year Built ': self.STANDARD_YEAR_BUILT, + 'Boiler Make ': None, # TODO: Don't have this for the moment + 'Boiler Model ': None, # TODO: Don't have this for the moment + 'Non-Intrusives: Date Checked ': None, + # TODO: Don't have this for the moment + 'Non-Intrusives: Wall Type ': ( + "non-intrusives: Construction" if self.non_intrusives_present else None + ), + 'Non-intrusives: Insulation ': ( + "non-intrusives: Insulated" if self.non_intrusives_present else None + ), + 'Non-intrusives: Insulation Material ': ( + "non-intrusives: Material" if self.non_intrusives_present else None + ), + 'Non-Intrusives: CIGA Check Required ': ( + 'non-intrusives: CIGA Check Required' if self.non_intrusives_present else None + ), + 'Non-Intrusives: PV Access Issues ': ( + 'non-intrusives: PV, ACCESS ISSUE, SEE NOTES' if self.non_intrusives_present else None + ), + 'Non-Intrusives: Roof Orientation ': ( + 'non-intrusives: OFF GAS - ROOF ORIENTATION' if self.non_intrusives_present else None + ), + 'Non-Intrusives: Surveyor Notes ': ( + 'non-intrusives: Any further surveyor notes' if self.non_intrusives_present else None + ), + 'Non-Intrusives: Surveyor Name ': ( + 'non-intrusives: Surveyors Name' if self.non_intrusives_present else None + ), + 'CIGA: Date Requested ': None, # TODO: Don't have this for the moment + 'CIGA: Cavity Guarantee Found ': None, + 'Last EPC: Is Estimated ': self.EPC_API_DATA_NAMES["estimated"], + 'Last EPC: EPC Rating ': self.EPC_API_DATA_NAMES["current-energy-rating"], + 'Last EPC: SAP Rating ': self.EPC_API_DATA_NAMES["current-energy-efficiency"], + 'Last EPC: Main Heating Description ': self.EPC_API_DATA_NAMES[ + "mainheat-description"], + 'Last EPC: Heating Controls ': self.EPC_API_DATA_NAMES[ + "mainheatcont-description"], + 'Last EPC: Lodgement Date ': self.EPC_API_DATA_NAMES["inspection-date"], + 'Last EPC: Floor Area ': self.EPC_API_DATA_NAMES["total-floor-area"], + 'Last EPC: Wall ': self.EPC_API_DATA_NAMES["walls-description"], + 'Last EPC: Roof ': self.EPC_API_DATA_NAMES["roof-description"], + 'Last EPC: Floor ': self.EPC_API_DATA_NAMES["floor-description"], + 'Last EPC: Room Height ': self.EPC_API_DATA_NAMES["floor-height"], + 'Last EPC: Age Band ': self.EPC_API_DATA_NAMES["construction-age-band"], + 'Deal Stage ': 'Deal Stage ', + 'Pipeline ': 'Pipeline ', + 'Expected Commencement Date ': None, # TODO: Need to set this, + 'Deal Name ': "dealname", # Need to create this, + 'Product ID ': 'Product ID ', + 'Name ': 'Name ', + 'Unit price ': 'Unit price ', + 'Quantity ': 'Quantity ', + 'Deal Owner': 'surveyor_email', + 'Amount ': 'Unit price ', + } + + # We now create the finalised dataset to be uploaded into Hubspot + variables_required = list(schema_mappings.values()) + variables_required = [v for v in variables_required if v is not None] + # We now flag anything that has a none value, which is information we haven't got right now + none_variables = [k for k, v in schema_mappings.items() if v is None] + # We'll add placeholder columns for the None variables + programme_data = programme_data[variables_required] + for col in none_variables: + programme_data[col] = None + + programme_data = programme_data.rename( + columns={v: k for k, v in schema_mappings.items() if v is not None} + ) + + self.hubspot_data = programme_data diff --git a/asset_list/app.py b/asset_list/app.py index 45839157..475bd7b3 100644 --- a/asset_list/app.py +++ b/asset_list/app.py @@ -262,7 +262,25 @@ def app(): landlord_wall_construction = None landlord_heating_system = None landlord_existing_pv = None - landlord_property_id = "Property Ref" + landlord_property_id = "Property ref" + + # For Westward + # data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Westward" + # data_filename = "WESTWARD - completed list..xlsx" + # sheet_name = "Sheet1" + # postcode_column = "WFT EDIT Postcode" + # fulladdress_column = "Address" + # address1_column = None + # address1_method = "house_number_extraction" + # address_cols_to_concat = [] + # missing_postcodes_method = None + # landlord_year_built = "Build date" + # landlord_os_uprn = "UPRN" + # landlord_property_type = "Location type" + # landlord_wall_construction = "Wall Construction (EPC)" + # landlord_heating_system = "Heat Source" + # landlord_existing_pv = "PV (Y/N)" + # landlord_property_id = "Place ref" # Maps addresses to uprn in problematic cases manual_uprn_map = {} @@ -454,6 +472,47 @@ def app(): asset_list.flat_analysis() + # Convert to a format suitable for CRM + contact_details = pd.DataFrame( + [ + { + asset_list.landlord_property_id: "EXETEMORH0100010", + "first name": "Khalim", + "last name": "Conn-Kowlessar", + "email": "kconnkowlessar@gmail.com", + "phone": "075399248" + } + ] + ) + + assigned_surveyors = pd.DataFrame( + [ + { + asset_list.landlord_property_id: "EXETEMORH0100010", + "surveyor_name": "Khalim Conn-Kowlessar", + "surveyor_email": "khalim@domna.homes", + } + ] + ) + + # TODO: Sort the output by postcode + + company_domain = "ealing.gov.uk" + crm_pipeline_name = "Survey Management" + first_dealstage = "READY TO BEGIN SCHEDULING" + # TODO - temp, upload to either SharePoint or AWS + hubspot_template = pd.read_csv("~/Downloads/Hubspot Upload Template - Demo V2(Template).csv") + hubspot_schema = hubspot_template.columns.tolist() + + asset_list.prepare_for_crm( + contact_details=contact_details, + assigned_surveyors=assigned_surveyors, + company_domain=company_domain, + crm_pipeline_name=crm_pipeline_name, + first_dealstage=first_dealstage + ) + hubspt_data = asset_list.hubspot_data + # Store as an excel filename = os.path.join(data_folder, ".".join(data_filename.split(".")[:-1])) + " - Standardised.xlsx" # Store the data in two tabs. One for the asset list with the EPC data and the second with the flat data