def main_case_count_calculations(self):
     """This method calculates the entire Case Count KPIs set."""
     if not (self.filtered_scif.empty or self.matches.empty):
         try:
             if not self.filtered_mdis.empty:
                 self._prepare_data_for_calculation()
                 self._generate_adj_graphs()
                 facings_res = self._calculate_display_size_facings()
                 sku_cases_res = self._count_number_of_cases()
                 unshoppable_cases_res = self._non_shoppable_case_kpi()
                 implied_cases_res = self._implied_shoppable_cases_kpi()
             else:
                 facings_res = self._calculate_display_size_facings()
                 sku_cases_res = self._count_number_of_cases()
                 unshoppable_cases_res = []
                 implied_cases_res = []
             sku_cases_res = self._remove_nonshoppable_cases_from_shoppable_cases(
                 sku_cases_res, unshoppable_cases_res)
             total_res = self._calculate_total_cases(sku_cases_res +
                                                     implied_cases_res +
                                                     unshoppable_cases_res)
             placeholder_res = self._generate_placeholder_results(
                 facings_res, sku_cases_res, unshoppable_cases_res,
                 implied_cases_res)
             self._save_results_to_db(facings_res + sku_cases_res +
                                      unshoppable_cases_res +
                                      implied_cases_res + total_res +
                                      placeholder_res)
             self._calculate_total_score_level_res(total_res)
         except Exception as err:
             Log.error(
                 "DiageoUS Case Count calculation failed due to the following error: {}"
                 .format(err))
 def commit_results(self, queries):
     """
     This function commits the results into the DB in batches.
     query_num is the number of queires that were executed in the current batch
     After batch_size is reached, the function re-connects the DB and cursor.
     """
     self.rds_conn.connect_rds()
     cursor = self.rds_conn.db.cursor()
     batch_size = 1000
     query_num = 0
     failed_queries = []
     for query in queries:
         try:
             cursor.execute(query)
             # print query
         except Exception as e:
             Log.warning(
                 'Committing to DB failed to due to: {}. Query: {}'.format(
                     e, query))
             self.rds_conn.db.commit()
             failed_queries.append(query)
             self.rds_conn.connect_rds()
             cursor = self.rds_conn.db.cursor()
             continue
         if query_num > batch_size:
             self.rds_conn.db.commit()
             self.rds_conn.connect_rds()
             cursor = self.rds_conn.db.cursor()
             query_num = 0
         query_num += 1
     self.rds_conn.db.commit()
 def main_case_count_calculations(self):
     """This method calculates the entire Case Count KPIs set."""
     if self.filtered_mdis.empty or self.filtered_scif.empty:
         return
     try:
         self._prepare_data_for_calculation()
         total_facings_per_brand_res = self._calculate_total_bottler_and_carton_facings(
         )
         self._save_results_to_db(total_facings_per_brand_res,
                                  Ccc.TOTAL_FACINGS_KPI)
         cases_per_brand_res = self._count_number_of_cases()
         self._save_results_to_db(cases_per_brand_res, Ccc.CASE_COUNT_KPI)
         unshoppable_brands_lst = self._non_shoppable_case_kpi()
         self._save_results_to_db(unshoppable_brands_lst,
                                  Ccc.NON_SHOPPABLE_CASES_KPI)
         implied_shoppable_cases_kpi_res = self._implied_shoppable_cases_kpi(
         )
         self._save_results_to_db(implied_shoppable_cases_kpi_res,
                                  Ccc.IMPLIED_SHOPPABLE_CASES_KPI)
         total_cases_res = self._calculate_and_total_cases(
             cases_per_brand_res + implied_shoppable_cases_kpi_res)
         self._save_results_to_db(total_cases_res,
                                  Ccc.TOTAL_CASES_KPI,
                                  should_enter=False)
     except Exception as err:
         Log.error(
             "DiageoUS Case Count calculation failed due to the following error: {}"
             .format(err))
    def p1_assortment_validator(self):
        """
        This function validates the store assortment template.
        It compares the OUTLET_ID (= store_number_1) and the products ean_code to the stores and products from the DB
        :return: False in case of an error and True in case of a valid template
        """
        raw_data = self.parse_assortment_template()
        legal_template = True
        invalid_inputs = {INVALID_STORES: [], INVALID_PRODUCTS: []}
        valid_stores = self.store_data.loc[
            self.store_data['store_number'].isin(raw_data[OUTLET_ID])]
        if len(valid_stores) != len(raw_data[OUTLET_ID].unique()):
            invalid_inputs[INVALID_STORES] = list(
                set(raw_data[OUTLET_ID].unique()) -
                set(valid_stores['store_number']))
            Log.debug("The following stores don't exist in the DB: {}".format(
                invalid_inputs[INVALID_STORES]))
            legal_template = False

        valid_product = self.all_products.loc[self.all_products[EAN_CODE].isin(
            raw_data[EAN_CODE])]
        if len(valid_product) != len(raw_data[EAN_CODE].unique()):
            invalid_inputs[INVALID_PRODUCTS] = list(
                set(raw_data[EAN_CODE].unique()) -
                set(valid_product[EAN_CODE]))
            Log.debug(
                "The following products don't exist in the DB: {}".format(
                    invalid_inputs[INVALID_PRODUCTS]))
            legal_template = False
        return legal_template, invalid_inputs
示例#5
0
    def menu_count(self):
        kpi_fk = self.get_kpi_fk_by_kpi_type(Consts.MENU_KPI_CHILD)
        parent_kpi = self.get_kpi_fk_by_kpi_type(Consts.TOTAL_MENU_KPI_SCORE)
        # we need to save a second set of KPIs with heirarchy for the mobile report
        kpi_fk_mr = self.get_kpi_fk_by_kpi_type(Consts.MENU_KPI_CHILD_MR)
        parent_kpi_mr = self.get_kpi_fk_by_kpi_type(Consts.TOTAL_MENU_KPI_SCORE_MR)

        if self.targets.empty:
            return
        try:
            menu_product_fks = [t for t in self.targets.product_fk.unique().tolist() if pd.notna(t)]
        except AttributeError:
            Log.warning('Menu Count targets are corrupt for this store')
            return

        filtered_scif = self.scif[self.scif['template_group'].str.contains('Menu')]
        present_menu_scif_sub_brands = filtered_scif.sub_brand.unique().tolist()
        passed_products = 0

        for product_fk in menu_product_fks:
            result = 0
            sub_brand = self.all_products['sub_brand'][self.all_products['product_fk'] == product_fk].iloc[0]

            custom_entity_df = self.custom_entity['pk'][self.custom_entity['name'] == sub_brand]
            if custom_entity_df.empty:
                custom_entity_pk = -1
            else:
                custom_entity_pk = custom_entity_df.iloc[0]

            if sub_brand in present_menu_scif_sub_brands:
                result = 1
                passed_products += 1

            self.write_to_db(fk=kpi_fk_mr, numerator_id=product_fk, numerator_result=0, denominator_result=0,
                             denominator_id=custom_entity_pk,
                             result=result, score=0, identifier_parent=parent_kpi_mr, identifier_result=kpi_fk_mr,
                             should_enter=True)

            self.write_to_db(fk=kpi_fk, numerator_id=product_fk, numerator_result=0, denominator_result=0,
                             denominator_id=custom_entity_pk, result=result, score=0)

        target_products = len(menu_product_fks)
        self.write_to_db(fk=parent_kpi_mr, numerator_id=self.manufacturer_fk, numerator_result=0, denominator_result=0,
                         denominator_id=self.store_id,
                         result=passed_products, score=0, target=target_products, identifier_result=parent_kpi_mr)

        self.write_to_db(fk=parent_kpi, numerator_id=self.manufacturer_fk, numerator_result=0, denominator_result=0,
                         denominator_id=self.store_id,
                         result=passed_products, score=0, target=target_products)
 def get_closest_point(origin_point, other_points_df):
     """This method gets a point (x & y coordinates) and checks what is the closet point the could be
     found in the DataFrame that is being received.
     @param: origin_point (tuple): coordinates of x and y
     @param: other_points_df (DataFrame): A DF that has rect_x' and 'rect_y' columns
     @return: A DataFrame with the closest points and the rest of the data
     """
     other_points = other_points_df[['rect_x', 'rect_y']].values
     # Euclidean geometry magic
     distances = np.sum((other_points - origin_point) ** 2, axis=1)
     # get the shortest hypotenuse
     try:
         closest_point = other_points[np.argmin(distances)]
     except ValueError:
         Log.error('Unable to find a matching opposite point for supplied anchor!')
         return other_points_df
     return other_points_df[
         (other_points_df['rect_x'] == closest_point[0]) & (other_points_df['rect_y'] == closest_point[1])]
 def set_end_date_for_irrelevant_assortments(self, stores_list):
     """
     This function sets an end_date to all of the irrelevant stores in the assortment.
     :param stores_list: List of the stores from the assortment template
     """
     Log.debug("Closing assortment for stores out of template")
     irrelevant_stores = self.store_data.loc[
         ~self.store_data['store_number'].
         isin(stores_list)]['store_fk'].unique().tolist()
     current_assortment_stores = self.current_top_skus['store_fk'].unique(
     ).tolist()
     stores_to_remove = list(
         set(irrelevant_stores).intersection(
             set(current_assortment_stores)))
     for store in stores_to_remove:
         query = [
             self.get_store_deactivation_query(store, self.deactivate_date)
         ]
         self.commit_results(query)
     Log.debug("Assortment is closed for ({}) stores".format(
         len(stores_to_remove)))
    def upload_store_assortment_file(self):
        raw_data = self.parse_assortment_template()
        data = []
        list_of_stores = raw_data[OUTLET_ID].unique().tolist()

        if not self.partial_update:
            self.set_end_date_for_irrelevant_assortments(list_of_stores)

        Log.debug("Preparing assortment data for update")
        store_counter = 0
        for store in list_of_stores:
            store_data = {}
            store_products = raw_data.loc[raw_data[OUTLET_ID] ==
                                          store][EAN_CODE].tolist()
            store_data[store] = store_products
            data.append(store_data)

            store_counter += 1
            if store_counter % 1000 == 0 or store_counter == len(
                    list_of_stores):
                Log.debug("Assortment is prepared for {}/{} stores".format(
                    store_counter, len(list_of_stores)))

        Log.debug("Updating assortment data in DB")
        store_counter = 0
        for store_data in data:

            self.update_db_from_json(store_data)

            if self.all_queries:
                queries = self.merge_insert_queries(self.all_queries)
                self.commit_results(queries)
                self.all_queries = []

            store_counter += 1
            if store_counter % 1000 == 0 or store_counter == len(data):
                Log.debug(
                    "Assortment is updated in DB for {}/{} stores".format(
                        store_counter, len(data)))
    def upload_assortment(self):
        """
        This is the main function of the assortment.
        It does the validation and then upload the assortment.
        :return:
        """
        Log.debug("Parsing and validating the assortment template")
        is_valid, invalid_inputs = self.p1_assortment_validator()

        Log.info("Assortment upload is started")
        self.upload_store_assortment_file()
        if not is_valid:
            Log.warning("Errors were found during the template validation")
            if invalid_inputs[INVALID_STORES]:
                Log.warning("The following stores don't exist in the DB: {}"
                            "".format(invalid_inputs[INVALID_STORES]))
            if invalid_inputs[INVALID_PRODUCTS]:
                Log.warning("The following products don't exist in the DB: {}"
                            "".format(invalid_inputs[INVALID_PRODUCTS]))
        Log.info("Assortment upload is finished")
    def update_db_from_json(self, data):
        update_products = set()
        missing_products = set()

        store_number = data.keys()[0]
        if store_number is None:
            Log.debug("'{}' column or value is missing".format(STORE_NUMBER))
            return

        store_fk = self.get_store_fk(store_number)
        if store_fk is None:
            Log.debug(
                'Store Number {} does not exist in DB'.format(store_number))
            return

        for key in data[store_number]:
            validation = False
            if isinstance(key, (float, int)):
                validation = True
            elif isinstance(key, (str, unicode)):
                validation = True
            if validation:
                product_ean_code = str(key).split(',')[-1]
                product_fk = self.get_product_fk(product_ean_code)
                if product_fk is None:
                    missing_products.add(product_ean_code)
                else:
                    update_products.add(product_fk)

        if missing_products:
            Log.debug(
                'The following EAN Codes for Store Number {} do not exist in DB: {}.'
                ''.format(store_number, list(missing_products)))
        queries = []
        current_products = self.current_top_skus[
            self.current_top_skus['store_fk'] ==
            store_fk]['product_fk'].tolist()

        products_to_deactivate = tuple(
            set(current_products).difference(update_products))
        products_to_activate = tuple(
            set(update_products).difference(current_products))

        if products_to_deactivate:
            if len(products_to_deactivate) == 1:
                queries.append(
                    self.get_deactivation_query(
                        store_fk, "(" + str(products_to_deactivate[0]) + ")",
                        self.deactivate_date))
            else:
                queries.append(
                    self.get_deactivation_query(store_fk,
                                                tuple(products_to_deactivate),
                                                self.deactivate_date))

        for product_fk in products_to_activate:
            queries.append(
                self.get_activation_query(store_fk, product_fk,
                                          self.activate_date))

        self.all_queries.extend(queries)
        Log.debug(
            'Store Number {} - Products to update {}: Deactivated {}, Activated {}'
            ''.format(store_number, len(update_products),
                      len(products_to_deactivate), len(products_to_activate)))