Пример #1
0
class HEINZCRToolBox:
    LVL3_HEADERS = ['assortment_group_fk', 'assortment_fk', 'target', 'product_fk',
                    'in_store', 'kpi_fk_lvl1', 'kpi_fk_lvl2', 'kpi_fk_lvl3', 'group_target_date',
                    'assortment_super_group_fk']
    LVL2_HEADERS = ['assortment_group_fk', 'assortment_fk', 'target', 'passes', 'total',
                    'kpi_fk_lvl1', 'kpi_fk_lvl2', 'group_target_date']
    LVL1_HEADERS = ['assortment_group_fk', 'target', 'passes', 'total', 'kpi_fk_lvl1']
    ASSORTMENT_FK = 'assortment_fk'
    ASSORTMENT_GROUP_FK = 'assortment_group_fk'
    ASSORTMENT_SUPER_GROUP_FK = 'assortment_super_group_fk'
    BRAND_VARIENT = 'brand_varient'
    NUMERATOR = 'numerator'
    DENOMINATOR = 'denominator'
    DISTRIBUTION_KPI = 'Distribution - SKU'
    OOS_SKU_KPI = 'OOS - SKU'
    OOS_KPI = 'OOS'

    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.common = CommonV2  # remove later
        self.common_v2 = CommonV2(self.data_provider)
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.rds_conn = PSProjectConnector(self.project_name, DbUsers.CalculationEng)
        self.kpi_results_queries = []
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.survey = Survey(self.data_provider, output=self.output, ps_data_provider=self.ps_data_provider,
                             common=self.common_v2)
        self.store_sos_policies = self.ps_data_provider.get_store_policies()
        self.labels = self.ps_data_provider.get_labels()
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.store_info = self.ps_data_provider.get_ps_store_info(self.store_info)
        self.country = self.store_info['country'].iloc[0]
        self.current_date = datetime.now()
        self.extra_spaces_template = pd.read_excel(Const.EXTRA_SPACES_RELEVANT_SUB_CATEGORIES_PATH)
        self.store_targets = pd.read_excel(Const.STORE_TARGETS_PATH)
        self.sub_category_weight = pd.read_excel(Const.SUB_CATEGORY_TARGET_PATH, sheetname='category_score')
        self.kpi_weights = pd.read_excel(Const.SUB_CATEGORY_TARGET_PATH, sheetname='max_weight')
        self.targets = self.ps_data_provider.get_kpi_external_targets()
        self.store_assortment = PSAssortmentDataProvider(
            self.data_provider).execute(policy_name=None)
        self.supervisor_target = self.get_supervisor_target()
        try:
            self.sub_category_assortment = pd.merge(self.store_assortment,
                                                    self.all_products.loc[:, ['product_fk', 'sub_category',
                                                                              'sub_category_fk']],
                                                    how='left', on='product_fk')
            self.sub_category_assortment = \
                self.sub_category_assortment[~self.sub_category_assortment['assortment_name'].str.contains(
                    'ASSORTMENT')]
            self.sub_category_assortment = pd.merge(self.sub_category_assortment, self.sub_category_weight, how='left',
                                                    left_on='sub_category',
                                                    right_on='Category')


        except KeyError:
            self.sub_category_assortment = pd.DataFrame()
        self.update_score_sub_category_weights()
        try:
            self.store_assortment_without_powerskus = \
                self.store_assortment[self.store_assortment['assortment_name'].str.contains('ASSORTMENT')]
        except KeyError:
            self.store_assortment_without_powerskus = pd.DataFrame()

        self.adherence_results = pd.DataFrame(columns=['product_fk', 'trax_average',
                                                       'suggested_price', 'into_interval', 'min_target', 'max_target',
                                                       'percent_range'])
        self.extra_spaces_results = pd.DataFrame(
            columns=['sub_category_fk', 'template_fk', 'count'])

        self.powersku_scores = {}
        self.powersku_empty = {}

        self.powersku_bonus = {}
        self.powersku_price = {}
        self.powersku_sos = {}

    def main_calculation(self, *args, **kwargs):
        """
        This function calculates the KPI results.
        """
        if self.scif.empty:
            return
        # these function must run first
        #  self.adherence_results = self.heinz_global_price_adherence(pd.read_excel(Const.PRICE_ADHERENCE_TEMPLATE_PATH,
        #                                                                          sheetname="Price Adherence"))
        self.adherence_results = self.heinz_global_price_adherence(self.targets)
        self.extra_spaces_results = self.heinz_global_extra_spaces()
        self.set_relevant_sub_categories()

        # this isn't relevant to the 'Perfect Score' calculation
        self.heinz_global_distribution_per_category()
        self.calculate_assortment()

        self.calculate_powersku_assortment()
        self.main_sos_calculation()
        self.calculate_powersku_price_adherence()
        self.calculate_perfect_store_extra_spaces()
        self.check_bonus_question()

        self.calculate_perfect_sub_category()

    def calculate_assortment(self):
        if self.store_assortment_without_powerskus.empty:
            return

        products_in_store = self.scif[self.scif['facings'] > 0]['product_fk'].unique().tolist()
        pass_count = 0

        total_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Distribution')
        identifier_dict = self.common_v2.get_dictionary(kpi_fk=total_kpi_fk)

        oos_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('OOS')
        oos_identifier_dict = self.common_v2.get_dictionary(kpi_fk=oos_kpi_fk)

        for row in self.store_assortment_without_powerskus.itertuples():
            result = 0
            if row.product_fk in products_in_store:
                result = 1
                pass_count += 1

            sku_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Distribution - SKU')
            self.common_v2.write_to_db_result(sku_kpi_fk, numerator_id=row.product_fk, denominator_id=row.assortment_fk,
                                              result=result, identifier_parent=identifier_dict, should_enter=True)

            oos_result = 0 if result else 1
            oos_sku_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('OOS - SKU')
            self.common_v2.write_to_db_result(oos_sku_kpi_fk, numerator_id=row.product_fk,
                                              denominator_id=row.assortment_fk,
                                              result=oos_result, identifier_parent=oos_identifier_dict,
                                              should_enter=True)

        number_of_products_in_assortment = len(self.store_assortment_without_powerskus)
        if number_of_products_in_assortment:
            total_result = (pass_count / float(number_of_products_in_assortment)) * 100
            oos_products = number_of_products_in_assortment - pass_count
            oos_result = (oos_products / float(number_of_products_in_assortment)) * 100
        else:
            total_result = 0
            oos_products = number_of_products_in_assortment
            oos_result = number_of_products_in_assortment
        self.common_v2.write_to_db_result(total_kpi_fk, numerator_id=Const.OWN_MANUFACTURER_FK,
                                          denominator_id=self.store_id,
                                          numerator_result=pass_count,
                                          denominator_result=number_of_products_in_assortment,
                                          result=total_result, identifier_result=identifier_dict)
        self.common_v2.write_to_db_result(oos_kpi_fk, numerator_id=Const.OWN_MANUFACTURER_FK,
                                          denominator_id=self.store_id,
                                          numerator_result=oos_products,
                                          denominator_result=number_of_products_in_assortment,
                                          result=oos_result, identifier_result=oos_identifier_dict)

    def calculate_powersku_assortment(self):
        if self.sub_category_assortment.empty:
            return 0

        sub_category_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.POWER_SKU_SUB_CATEGORY)
        sku_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.POWER_SKU)
        target_kpi_weight = float(
            self.kpi_weights['Score'][self.kpi_weights['KPIs'] == Const.KPI_WEIGHTS['POWERSKU']].iloc[
                0])

        kpi_weight = self.get_kpi_weight('POWERSKU')

        products_in_session = self.scif[self.scif['facings'] > 0]['product_fk'].unique().tolist()
        self.sub_category_assortment['in_session'] = \
            self.sub_category_assortment.loc[:, 'product_fk'].isin(products_in_session)

        # save PowerSKU results at SKU level
        for sku in self.sub_category_assortment[
            ['product_fk', 'sub_category_fk', 'in_session', 'sub_category']].itertuples():
            parent_dict = self.common_v2.get_dictionary(
                kpi_fk=sub_category_kpi_fk, sub_category_fk=sku.sub_category_fk)
            relevant_sub_category_df = self.sub_category_assortment[
                self.sub_category_assortment['sub_category'] == sku.sub_category]
            if relevant_sub_category_df.empty:
                sub_category_count = 0
            else:
                sub_category_count = len(relevant_sub_category_df)

            result = 1 if sku.in_session else 0

            score = result * (target_kpi_weight / float(sub_category_count))
            self.common_v2.write_to_db_result(sku_kpi_fk, numerator_id=sku.product_fk,
                                              denominator_id=sku.sub_category_fk, score=score,
                                              result=result, identifier_parent=parent_dict, should_enter=True)
        # save PowerSKU results at sub_category level

        aggregated_results = self.sub_category_assortment.groupby('sub_category_fk').agg(
            {'in_session': 'sum', 'product_fk': 'count'}).reset_index().rename(
            columns={'product_fk': 'product_count'})
        aggregated_results['percent_complete'] = \
            aggregated_results.loc[:, 'in_session'] / aggregated_results.loc[:, 'product_count']
        aggregated_results['result'] = aggregated_results['percent_complete']
        for sub_category in aggregated_results.itertuples():
            identifier_dict = self.common_v2.get_dictionary(kpi_fk=sub_category_kpi_fk,
                                                            sub_category_fk=sub_category.sub_category_fk)

            result = sub_category.result
            score = result * kpi_weight

            self.powersku_scores[sub_category.sub_category_fk] = score
            self.common_v2.write_to_db_result(sub_category_kpi_fk, numerator_id=sub_category.sub_category_fk,
                                              denominator_id=self.store_id,
                                              identifier_parent=sub_category.sub_category_fk,
                                              identifier_result=identifier_dict, result=result * 100, score=score,
                                              weight=target_kpi_weight, target=target_kpi_weight,
                                              should_enter=True)

    def heinz_global_distribution_per_category(self):
        relevant_stores = pd.DataFrame(columns=self.store_sos_policies.columns)
        for row in self.store_sos_policies.itertuples():
            policies = json.loads(row.store_policy)
            df = self.store_info
            for key, value in policies.items():
                try:
                    df_1 = df[df[key].isin(value)]
                except KeyError:
                    continue
            if not df_1.empty:
                stores = self.store_sos_policies[(self.store_sos_policies['store_policy'] == row.store_policy.encode('utf-8'))
                                                 & (
                                                         self.store_sos_policies[
                                                             'target_validity_start_date'] <= datetime.date(
                                                     self.current_date))]
                if stores.empty:
                    relevant_stores = stores
                else:
                    relevant_stores = relevant_stores.append(stores, ignore_index=True)
        relevant_stores = relevant_stores.drop_duplicates(subset=['kpi', 'sku_name', 'target', 'sos_policy'],
                                                          keep='last')
        for row in relevant_stores.itertuples():
            sos_policy = json.loads(row.sos_policy)
            numerator_key = sos_policy[self.NUMERATOR].keys()[0]
            denominator_key = sos_policy[self.DENOMINATOR].keys()[0]
            numerator_val = sos_policy[self.NUMERATOR][numerator_key]
            denominator_val = sos_policy[self.DENOMINATOR][denominator_key]
            target = row.target * 100
            if numerator_key == 'manufacturer':
                numerator_key = numerator_key + '_name'

            if denominator_key == 'sub_category' \
                    and denominator_val.lower() != 'all' \
                    and json.loads(row.store_policy).get('store_type') \
                    and len(json.loads(row.store_policy).get('store_type')) == 1:
                try:
                    denominator_id = self.all_products[self.all_products[denominator_key] == denominator_val][
                        denominator_key + '_fk'].values[0]
                    numerator_id = self.all_products[self.all_products[numerator_key] == numerator_val][
                        numerator_key.split('_')[0] + '_fk'].values[0]

                    # self.common.write_to_db_result_new_tables(fk=12, numerator_id=numerator_id,
                    #                                           numerator_result=None,
                    #                                           denominator_id=denominator_id,
                    #                                           denominator_result=None,
                    #                                           result=target)
                    self.common_v2.write_to_db_result(fk=12, numerator_id=numerator_id, numerator_result=None,
                                                      denominator_id=denominator_id, denominator_result=None,
                                                      result=target)
                except Exception as e:
                    Log.warning(denominator_key + ' - - ' + denominator_val)

    def calculate_perfect_store(self):
        pass

    def calculate_perfect_sub_category(self):
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.PERFECT_STORE_SUB_CATEGORY)
        parent_kpi = self.common_v2.get_kpi_fk_by_kpi_type(Const.PERFECT_STORE)

        total_score = 0
        sub_category_fk_list = []
        kpi_type_dict_scores = [self.powersku_scores, self.powersku_empty, self.powersku_price,
                                self.powersku_sos]

        for kpi_dict in kpi_type_dict_scores:
            sub_category_fk_list.extend(kpi_dict.keys())

        kpi_weight_perfect_store = 0
        if self.country in self.sub_category_weight.columns.to_list():
            kpi_weight_perfect_store = self.sub_category_weight[self.country][
                self.sub_category_weight['Category'] == Const.PERFECT_STORE_KPI_WEIGHT]

            if not kpi_weight_perfect_store.empty:
                kpi_weight_perfect_store = kpi_weight_perfect_store.iloc[0]

        unique_sub_cat_fks = list(dict.fromkeys(sub_category_fk_list))

        sub_category_fks = self.sub_category_weight.sub_category_fk.unique().tolist()
        relevant_sub_cat_list = [x for x in sub_category_fks if str(x) != 'nan']

        # relevant_sub_cat_list = self.sub_category_assortment['sub_category_fk'][
        #     self.sub_category_assortment['Category'] != pd.np.nan].unique().tolist()
        for sub_cat_fk in unique_sub_cat_fks:
            if sub_cat_fk in relevant_sub_cat_list:
                bonus_score = 0
                try:
                    bonus_score = self.powersku_bonus[sub_cat_fk]
                except:
                    pass

                sub_cat_weight = self.get_weight(sub_cat_fk)
                sub_cat_score = self.calculate_sub_category_sum(kpi_type_dict_scores, sub_cat_fk)

                result = sub_cat_score

                score = (result * sub_cat_weight) + bonus_score
                total_score += score

                self.common_v2.write_to_db_result(kpi_fk, numerator_id=sub_cat_fk,
                                                  denominator_id=self.store_id,
                                                  result=result, score=score,
                                                  identifier_parent=parent_kpi,
                                                  identifier_result=sub_cat_fk,
                                                  weight=sub_cat_weight * 100,
                                                  should_enter=True)

        self.common_v2.write_to_db_result(parent_kpi, numerator_id=Const.OWN_MANUFACTURER_FK,
                                          denominator_id=self.store_id,
                                          result=total_score, score=total_score,
                                          identifier_result=parent_kpi,
                                          target=kpi_weight_perfect_store,
                                          should_enter=True)

    def main_sos_calculation(self):
        relevant_stores = pd.DataFrame(columns=self.store_sos_policies.columns)
        for row in self.store_sos_policies.itertuples():
            policies = json.loads(row.store_policy)
            df = self.store_info
            for key, value in policies.items():
                try:
                    if key != 'additional_attribute_3':
                        df1 = df[df[key].isin(value)]
                except KeyError:
                    continue
            if not df1.empty:
                stores = \
                    self.store_sos_policies[(self.store_sos_policies['store_policy'].str.encode(
                                                'utf-8') == row.store_policy.encode('utf-8')) &
                                            (self.store_sos_policies['target_validity_start_date'] <= datetime.date(
                                                self.current_date))]
                if stores.empty:
                    relevant_stores = stores
                else:
                    relevant_stores = relevant_stores.append(stores, ignore_index=True)

        relevant_stores = relevant_stores.drop_duplicates(subset=['kpi', 'sku_name', 'target', 'sos_policy'],
                                                          keep='last')

        results_df = pd.DataFrame(columns=['sub_category', 'sub_category_fk', 'score'])

        sos_sub_category_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.SOS_SUB_CATEGORY)

        for row in relevant_stores.itertuples():
            sos_policy = json.loads(row.sos_policy)
            numerator_key = sos_policy[self.NUMERATOR].keys()[0]
            denominator_key = sos_policy[self.DENOMINATOR].keys()[0]
            numerator_val = sos_policy[self.NUMERATOR][numerator_key]
            denominator_val = sos_policy[self.DENOMINATOR][denominator_key]
            json_policy = json.loads(row.store_policy)
            kpi_fk = row.kpi

            # This is to assign the KPI to SOS_manufacturer_category_GLOBAL
            if json_policy.get('store_type') and len(json_policy.get('store_type')) > 1:
                kpi_fk = 8

            if numerator_key == 'manufacturer':
                numerator_key = numerator_key + '_name'
                # we need to include 'Philadelphia' as a manufacturer for all countries EXCEPT Chile
                if self.country == 'Chile':
                    numerator_values = [numerator_val]
                else:
                    numerator_values = [numerator_val, 'Philadelphia']
            else:
                # if the numerator isn't 'manufacturer', we just need to convert the value to a list
                numerator_values = [numerator_val]

            if denominator_key == 'sub_category':
                include_stacking_list = ['Nuts', 'DRY CHEESE', 'IWSN', 'Shredded', 'SNACK']
                if denominator_val in include_stacking_list:
                    facings_field = 'facings'
                else:
                    facings_field = 'facings_ign_stack'
            else:
                facings_field = 'facings_ign_stack'

            if denominator_key == 'sub_category' and denominator_val.lower() == 'all':
                # Here we are talkin on a KPI when the target have no denominator,
                # the calculation should be done on Numerator only
                numerator = self.scif[(self.scif[numerator_key] == numerator_val) &
                                      (self.scif['location_type'] == 'Primary Shelf')
                                      ][facings_field].sum()
                kpi_fk = 9
                denominator = None
                denominator_id = None
            else:
                numerator = self.scif[(self.scif[numerator_key].isin(numerator_values)) &
                                      (self.scif[denominator_key] == denominator_val) &
                                      (self.scif['location_type'] == 'Primary Shelf')][facings_field].sum()
                denominator = self.scif[(self.scif[denominator_key] == denominator_val) &
                                        (self.scif['location_type'] == 'Primary Shelf')][facings_field].sum()

            try:
                if denominator is not None:
                    denominator_id = self.all_products[self.all_products[denominator_key] == denominator_val][
                        denominator_key + '_fk'].values[0]
                if numerator is not None:
                    numerator_id = self.all_products[self.all_products[numerator_key] == numerator_val][
                        numerator_key.split('_')[0] + '_fk'].values[0]

                sos = 0
                if numerator and denominator:
                    sos = np.divide(float(numerator), float(denominator)) * 100
                score = 0
                target = row.target * 100
                if sos >= target:
                    score = 100

                identifier_parent = None
                should_enter = False
                if denominator_key == 'sub_category' and kpi_fk == row.kpi:
                    # if this a sub_category result, save it to the results_df for 'Perfect Store' store
                    results_df.loc[len(results_df)] = [denominator_val, denominator_id, score / 100]
                    identifier_parent = self.common_v2.get_dictionary(kpi_fk=sos_sub_category_kpi_fk,
                                                                      sub_category_fk=denominator_id)
                    should_enter = True

                manufacturer = None
                self.common_v2.write_to_db_result(kpi_fk, numerator_id=numerator_id, numerator_result=numerator,
                                                  denominator_id=denominator_id, denominator_result=denominator,
                                                  result=target, score=sos, target=target,
                                                  score_after_actions=manufacturer, identifier_parent=identifier_parent,
                                                  should_enter=should_enter)
            except Exception as e:
                Log.warning(denominator_key + ' - - ' + denominator_val)

        # if there are no sub_category sos results, there's no perfect store information to be saved
        if len(results_df) == 0:
            return 0

        # save aggregated results for each sub category
        kpi_weight = self.get_kpi_weight('SOS')
        for row in results_df.itertuples():
            identifier_result = \
                self.common_v2.get_dictionary(kpi_fk=sos_sub_category_kpi_fk,
                                              sub_category_fk=row.sub_category_fk)

            # sub_cat_weight = self.get_weight(row.sub_category_fk)
            result = row.score
            score = result * kpi_weight

            self.powersku_sos[row.sub_category_fk] = score
            # limit results so that aggregated results can only add up to 3
            self.common_v2.write_to_db_result(sos_sub_category_kpi_fk,
                                              numerator_id=row.sub_category_fk,
                                              denominator_id=self.store_id,
                                              result=row.score, score=score,
                                              identifier_parent=row.sub_category_fk,
                                              identifier_result=identifier_result,
                                              weight=kpi_weight,
                                              target=kpi_weight,
                                              should_enter=True)

    def calculate_powersku_price_adherence(self):
        adherence_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.POWER_SKU_PRICE_ADHERENCE)
        adherence_sub_category_kpi_fk = \
            self.common_v2.get_kpi_fk_by_kpi_type(Const.POWER_SKU_PRICE_ADHERENCE_SUB_CATEGORY)

        if self.sub_category_assortment.empty:
            return False

        results = pd.merge(self.sub_category_assortment,
                           self.adherence_results, how='left', on='product_fk')
        results['into_interval'].fillna(0, inplace=True)

        for row in results.itertuples():
            parent_dict = self.common_v2.get_dictionary(kpi_fk=adherence_sub_category_kpi_fk,
                                                        sub_category_fk=row.sub_category_fk)

            score_value = 'Not Present'
            in_session = row.in_session
            if in_session:
                if not pd.isna(row.trax_average) and row.suggested_price:
                    price_in_interval = 1 if row.into_interval == 1 else 0
                    if price_in_interval == 1:
                        score_value = 'Pass'
                    else:
                        score_value = 'Fail'
                else:
                    score_value = 'No Price'

            score = Const.PRESENCE_PRICE_VALUES[score_value]
            self.common_v2.write_to_db_result(adherence_kpi_fk, numerator_id=row.product_fk,
                                              denominator_id=row.sub_category_fk, result=row.trax_average,
                                              score=score, target=row.suggested_price, numerator_result=row.min_target,
                                              denominator_result=row.max_target,
                                              weight=row.percent_range,
                                              identifier_parent=parent_dict, should_enter=True)

        aggregated_results = results.groupby('sub_category_fk').agg(
            {'into_interval': 'sum', 'product_fk': 'count'}).reset_index().rename(
            columns={'product_fk': 'product_count'})
        aggregated_results['percent_complete'] = \
            aggregated_results.loc[:, 'into_interval'] / aggregated_results.loc[:, 'product_count']

        for row in aggregated_results.itertuples():
            identifier_result = self.common_v2.get_dictionary(kpi_fk=adherence_sub_category_kpi_fk,
                                                              sub_category_fk=row.sub_category_fk)
            kpi_weight = self.get_kpi_weight('PRICE')
            result = row.percent_complete
            score = result * kpi_weight

            self.powersku_price[row.sub_category_fk] = score

            self.common_v2.write_to_db_result(adherence_sub_category_kpi_fk, numerator_id=row.sub_category_fk,
                                              denominator_id=self.store_id, result=result, score=score,
                                              numerator_result=row.into_interval, denominator_result=row.product_count,
                                              identifier_parent=row.sub_category_fk,
                                              identifier_result=identifier_result,
                                              weight=kpi_weight, target=kpi_weight,
                                              should_enter=True)

    def heinz_global_price_adherence(self, config_df):
        config_df = config_df.sort_values(by=["received_time"], ascending=False).drop_duplicates(
            subset=['start_date', 'end_date', 'ean_code', 'store_type'], keep="first")

        if config_df.empty:
            Log.warning("No external_targets data found - Price Adherence will not be calculated")
            return self.adherence_results

        self.match_product_in_scene.loc[self.match_product_in_scene['price'].isna(), 'price'] = \
            self.match_product_in_scene.loc[self.match_product_in_scene['price'].isna(), 'promotion_price']
        # =============== remove after updating logic to support promotional pricing ===============
        results_df = self.adherence_results
        my_config_df = \
            config_df[config_df['store_type'].str.encode('utf-8') == self.store_info.store_type[0].encode('utf-8')]

        products_in_session = self.scif['product_ean_code'].unique().tolist()
        products_in_session = [ean for ean in products_in_session if ean is not pd.np.nan and ean is not None]

        my_config_df = my_config_df[my_config_df['ean_code'].isin(products_in_session)]

        for row in my_config_df.itertuples():
            product_pk = \
                self.all_products[self.all_products['product_ean_code']
                                  == row.ean_code]['product_fk'].iloc[0]

            mpisc_df_price = \
                self.match_product_in_scene[(self.match_product_in_scene['product_fk'] == product_pk) |
                                            (self.match_product_in_scene[
                                                 'substitution_product_fk'] == product_pk)]['price']
            try:
                suggested_price = float(row.suggested_price)
            except Exception as e:
                Log.error("Product with ean_code {} is not in the configuration file for customer type {}"
                          .format(row.ean_code, self.store_info.store_type[0].encode('utf-8')))
                break
            percentage_weight = int(row.percentage_weight)
            upper_percentage = (100 + percentage_weight) / float(100)
            lower_percentage = (100 - percentage_weight) / float(100)
            min_price = suggested_price * lower_percentage
            max_price = suggested_price * upper_percentage
            percentage_sku = percentage_weight
            into_interval = 0
            prices_sum = 0
            count = 0
            trax_average = None
            for price in mpisc_df_price:
                if price and pd.notna(price):
                    prices_sum += price
                    count += 1

            if prices_sum > 0:
                trax_average = prices_sum / count
                into_interval = 0

            if not np.isnan(suggested_price):
                if min_price <= trax_average <= max_price:
                    into_interval = 100

            results_df.loc[len(results_df)] = [product_pk, trax_average,
                                               suggested_price, into_interval / 100, min_price, max_price,
                                               percentage_sku]

            self.common_v2.write_to_db_result(10, numerator_id=product_pk,
                                              numerator_result=suggested_price,
                                              denominator_id=product_pk,
                                              denominator_result=trax_average,
                                              result=row.percentage_weight,
                                              score=into_interval)
            if trax_average:
                mark_up = (np.divide(np.divide(float(trax_average), float(1.13)),
                                     float(suggested_price)) - 1) * 100

                self.common_v2.write_to_db_result(11, numerator_id=product_pk,
                                                  numerator_result=suggested_price,
                                                  denominator_id=product_pk,
                                                  denominator_result=trax_average,
                                                  score=mark_up,
                                                  result=mark_up)

        return results_df

    def calculate_perfect_store_extra_spaces(self):
        extra_spaces_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(
            Const.PERFECT_STORE_EXTRA_SPACES_SUB_CATEGORY)

        sub_cats_for_store = self.relevant_sub_categories

        if self.extra_spaces_results.empty:
            pass

        try:
            relevant_sub_categories = [x.strip() for x in self.extra_spaces_template[
                self.extra_spaces_template['country'].str.encode('utf-8') == self.country.encode('utf-8')][
                'sub_category'].iloc[0].split(',')]
        except IndexError:
            Log.warning(
                'No relevant sub_categories for the Extra Spaces KPI found for the following country: {}'.format(
                    self.country))

        self.extra_spaces_results = pd.merge(self.extra_spaces_results,
                                             self.all_products.loc[:, [
                                                                          'sub_category_fk',
                                                                          'sub_category']].dropna().drop_duplicates(),
                                             how='left', on='sub_category_fk')

        relevant_extra_spaces = \
            self.extra_spaces_results[self.extra_spaces_results['sub_category'].isin(
                relevant_sub_categories)]
        kpi_weight = self.get_kpi_weight('EXTRA')
        for row in relevant_extra_spaces.itertuples():
            self.powersku_empty[row.sub_category_fk] = 1 * kpi_weight
            score = result = 1

            if row.sub_category_fk in sub_cats_for_store:
                sub_cats_for_store.remove(row.sub_category_fk)

            self.common_v2.write_to_db_result(extra_spaces_kpi_fk, numerator_id=row.sub_category_fk,
                                              denominator_id=row.template_fk, result=result, score=score,
                                              identifier_parent=row.sub_category_fk,
                                              target=1, should_enter=True)

        for sub_cat_fk in sub_cats_for_store:
            result = score = 0
            self.powersku_empty[sub_cat_fk] = 0
            self.common_v2.write_to_db_result(extra_spaces_kpi_fk, numerator_id=sub_cat_fk,
                                              denominator_id=0, result=result, score=score,
                                              identifier_parent=sub_cat_fk,
                                              target=1, should_enter=True)

    def heinz_global_extra_spaces(self):
        try:
            supervisor = self.store_info['additional_attribute_3'][0]
            store_target = -1
            # for row in self.store_sos_policies.itertuples():
            #     policies = json.loads(row.store_policy)
            #     for key, value in policies.items():
            #         try:
            #             if key == 'additional_attribute_3' and value[0] == supervisor:
            #                 store_target = row.target
            #                 break
            #         except KeyError:
            #             continue

            for row in self.supervisor_target.itertuples():
                try:
                    if row.supervisor == supervisor:
                        store_target = row.target
                        break
                except:
                    continue
        except Exception as e:
            Log.error("Supervisor target is not configured for the extra spaces report ")
            raise e

        results_df = self.extra_spaces_results

        # limit to only secondary scenes
        relevant_scif = self.scif[(self.scif['location_type_fk'] == float(2)) &
                                  (self.scif['facings'] > 0)]
        if relevant_scif.empty:
            return results_df
        # aggregate facings for every scene/sub_category combination in the visit
        relevant_scif = \
            relevant_scif.groupby(['scene_fk', 'template_fk', 'sub_category_fk'], as_index=False)['facings'].sum()
        # sort sub_categories by number of facings, largest first
        relevant_scif = relevant_scif.sort_values(['facings'], ascending=False)
        # drop all but the sub_category with the largest number of facings for each scene
        relevant_scif = relevant_scif.drop_duplicates(subset=['scene_fk'], keep='first')

        for row in relevant_scif.itertuples():
            results_df.loc[len(results_df)] = [row.sub_category_fk, row.template_fk, row.facings]
            self.common_v2.write_to_db_result(13, numerator_id=row.template_fk,
                                              numerator_result=row.facings,
                                              denominator_id=row.sub_category_fk,
                                              denominator_result=row.facings,
                                              context_id=row.scene_fk,
                                              result=store_target)

        return results_df

    def check_bonus_question(self):
        bonus_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.BONUS_QUESTION_SUB_CATEGORY)
        bonus_weight = self.kpi_weights['Score'][self.kpi_weights['KPIs'] == Const.KPI_WEIGHTS['Bonus']].iloc[0]

        sub_category_fks = self.sub_category_weight.sub_category_fk.unique().tolist()
        sub_category_fks = [x for x in sub_category_fks if str(x) != 'nan']
        if self.survey.check_survey_answer(('question_fk', Const.BONUS_QUESTION_FK), 'Yes,yes,si,Si'):
            result = 1
        else:
            result = 0

        for sub_cat_fk in sub_category_fks:
            sub_cat_weight = self.get_weight(sub_cat_fk)

            score = result * sub_cat_weight
            target_weight = bonus_weight * sub_cat_weight
            self.powersku_bonus[sub_cat_fk] = score

            self.common_v2.write_to_db_result(bonus_kpi_fk, numerator_id=sub_cat_fk,
                                              denominator_id=self.store_id,
                                              result=result, score=score, identifier_parent=sub_cat_fk,
                                              weight=target_weight, target=target_weight,
                                              should_enter=True)

    def commit_results_data(self):
        self.common_v2.commit_results_data()

    def update_score_sub_category_weights(self):
        all_sub_category_fks = self.all_products[['sub_category', 'sub_category_fk']].drop_duplicates()
        self.sub_category_weight = pd.merge(self.sub_category_weight, all_sub_category_fks, left_on='Category',
                                            right_on='sub_category',
                                            how='left')

    def get_weight(self, sub_category_fk):
        weight_value = 0

        if self.country in self.sub_category_weight.columns.to_list():
            weight_df = self.sub_category_weight[self.country][
                (self.sub_category_weight.sub_category_fk == sub_category_fk)]
            if weight_df.empty:
                return 0

            weight_value = weight_df.iloc[0]

            if pd.isna(weight_value):
                weight_value = 0

        weight = weight_value * 0.01
        return weight

    def get_kpi_weight(self, kpi_name):
        weight = self.kpi_weights['Score'][self.kpi_weights['KPIs'] == Const.KPI_WEIGHTS[kpi_name]].iloc[0]
        return weight

    def get_supervisor_target(self):
        supervisor_target = self.targets[self.targets['kpi_type'] == 'Extra Spaces']
        return supervisor_target

    def calculate_sub_category_sum(self, dict_list, sub_cat_fk):
        total_score = 0
        for item in dict_list:
            try:
                total_score += item[sub_cat_fk]
            except:
                pass

        return total_score

    def set_relevant_sub_categories(self):
        if self.country in self.sub_category_weight.columns.to_list():

            df = self.sub_category_weight[['Category', 'sub_category_fk', self.country]].dropna()
            self.relevant_sub_categories = df.sub_category_fk.to_list()
        else:
            self.relevant_sub_categories = []
Пример #2
0
class CCBOTTLERSUSREDToolBox:
    def __init__(self, data_provider, output, calculation_type):
        self.output = output
        self.data_provider = data_provider
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.scif = self.scif[self.scif['product_type'] != "Irrelevant"]
        self.united_scenes = self.get_united_scenes(
        )  # we don't need to check scenes without United products
        self.survey = Survey(self.data_provider, self.output)
        self.templates = {}
        self.calculation_type = calculation_type
        if self.calculation_type == Const.SOVI:
            self.TEMPLATE_PATH = TEMPLATE_PATH
            self.RED_SCORE = Const.RED_SCORE
            self.RED_SCORE_INTEG = Const.RED_SCORE_INTEG
            for sheet in Const.SHEETS:
                self.templates[sheet] = pd.read_excel(
                    self.TEMPLATE_PATH, sheetname=sheet).fillna('')
            self.converters = self.templates[Const.CONVERTERS]
        else:
            self.TEMPLATE_PATH = SURVEY_TEMPLATE_PATH
            self.RED_SCORE = Const.MANUAL_RED_SCORE
            self.RED_SCORE_INTEG = Const.MANUAL_RED_SCORE_INTEG
            for sheet in Const.SHEETS_MANUAL:
                self.templates[sheet] = pd.read_excel(
                    self.TEMPLATE_PATH, sheetname=sheet).fillna('')
        self.common_db_integ = Common(self.data_provider, self.RED_SCORE_INTEG)
        self.kpi_static_data_integ = self.common_db_integ.get_kpi_static_data()
        self.common_db = Common(self.data_provider, self.RED_SCORE)
        self.region = self.store_info['region_name'].iloc[0]
        self.store_type = self.store_info['store_type'].iloc[0]
        if self.store_type in STORE_TYPES:  #####
            self.store_type = STORE_TYPES[self.store_type]  ####
        self.store_attr = self.store_info['additional_attribute_15'].iloc[0]
        self.kpi_static_data = self.common_db.get_kpi_static_data()
        main_template = self.templates[Const.KPIS]
        self.templates[Const.KPIS] = main_template[
            (main_template[Const.REGION] == self.region)
            & (main_template[Const.STORE_TYPE] == self.store_type)]
        self.scene_calculator = CCBOTTLERSUSSceneRedToolBox(
            data_provider, output, self.templates, self)
        self.scenes_results = pd.DataFrame(columns=Const.COLUMNS_OF_SCENE)
        self.session_results = pd.DataFrame(columns=Const.COLUMNS_OF_SESSION)
        self.all_results = pd.DataFrame(columns=Const.COLUMNS_OF_SCENE)
        self.used_scenes = []
        self.red_score = 0
        self.weight_factor = self.get_weight_factor()

    # main functions:

    def main_calculation(self, *args, **kwargs):
        """
            This function gets all the scene results from the SceneKPI, after that calculates every session's KPI,
            and in the end it calls "filter results" to choose every KPI and scene and write the results in DB.
        """
        main_template = self.templates[Const.KPIS]
        if self.calculation_type == Const.SOVI:
            self.scenes_results = self.scene_calculator.main_calculation()
            session_template = main_template[main_template[Const.SESSION_LEVEL]
                                             == Const.V]
            for i, main_line in session_template.iterrows():
                self.calculate_main_kpi(main_line)
        else:
            for i, main_line in main_template.iterrows():
                self.calculate_manual_kpi(main_line)
        self.choose_and_write_results()

    def calculate_main_kpi(self, main_line):
        """
        This function gets a line from the main_sheet, transfers it to the match function, and checks all of the
        KPIs in the same name in the match sheet.
        :param main_line: series from the template of the main_sheet.
        """
        kpi_name = main_line[Const.KPI_NAME]
        kpi_type = main_line[Const.SHEET]
        relevant_scif = self.scif[
            (self.scif['scene_id'].isin(self.united_scenes))
            & (self.scif['product_type'] != 'Empty')]
        scene_types = self.does_exist(main_line, Const.SCENE_TYPE)
        if scene_types:
            relevant_scif = relevant_scif[relevant_scif['template_name'].isin(
                scene_types)]
        scene_groups = self.does_exist(main_line, Const.SCENE_TYPE_GROUP)
        if scene_groups:
            relevant_scif = relevant_scif[relevant_scif['template_group'].isin(
                scene_groups)]
        if kpi_type == Const.SCENE_AVAILABILITY:
            result = False if relevant_scif.empty else True
        else:
            isnt_dp = True if self.store_attr != Const.DP and main_line[
                Const.STORE_ATTRIBUTE] == Const.DP else False
            relevant_template = self.templates[kpi_type]
            relevant_template = relevant_template[relevant_template[
                Const.KPI_NAME] == kpi_name]
            target = len(relevant_template) if main_line[Const.GROUP_TARGET] == Const.ALL \
                else main_line[Const.GROUP_TARGET]
            if main_line[Const.SAME_PACK] == Const.V:
                result = self.calculate_availability_with_same_pack(
                    relevant_template, relevant_scif, isnt_dp, target)
            else:
                function = self.get_kpi_function(kpi_type)
                passed_counter = 0
                for i, kpi_line in relevant_template.iterrows():
                    answer = function(kpi_line, relevant_scif, isnt_dp)
                    if answer:
                        passed_counter += 1
                result = passed_counter >= target
        self.write_to_session_level(kpi_name=kpi_name, result=result)

    def calculate_manual_kpi(self, main_line):
        """
        This function gets a line from the main_sheet, transfers it to the match function, and checks all of the
        KPIs in the same name in the match sheet.
        :param main_line: series from the template of the main_sheet.
        """
        kpi_name = main_line[Const.KPI_NAME]
        relevant_template = self.templates[Const.SURVEY]
        relevant_template = relevant_template[relevant_template[Const.KPI_NAME]
                                              == kpi_name]
        target = len(relevant_template) if main_line[Const.GROUP_TARGET] == Const.ALL \
            else main_line[Const.GROUP_TARGET]
        passed_counter = 0
        for i, kpi_line in relevant_template.iterrows():
            answer = self.calculate_survey_specific(kpi_line)
            if answer:
                passed_counter += 1
        result = passed_counter >= target
        self.write_to_session_level(kpi_name=kpi_name, result=result)

    # write in DF:

    def write_to_session_level(self, kpi_name, result=0):
        """
        Writes a result in the DF
        :param kpi_name: string
        :param result: boolean
        """
        result_dict = {Const.KPI_NAME: kpi_name, Const.RESULT: result * 1}
        self.session_results = self.session_results.append(result_dict,
                                                           ignore_index=True)

    def write_to_all_levels(self,
                            kpi_name,
                            result,
                            display_text,
                            weight,
                            scene_fk=None,
                            reuse_scene=False):
        """
        Writes the final result in the "all" DF, add the score to the red score and writes the KPI in the DB
        :param kpi_name: str
        :param result: int
        :param display_text: str
        :param weight: int/float
        :param scene_fk: for the scene's kpi
        :param reuse_scene: this kpi can use scenes that were used
        """
        score = self.get_score(weight) * (result > 0)
        self.red_score += score
        result_dict = {
            Const.KPI_NAME: kpi_name,
            Const.RESULT: result,
            Const.SCORE: score
        }
        if scene_fk:
            result_dict[Const.SCENE_FK] = scene_fk
            if not reuse_scene:
                self.used_scenes.append(scene_fk)
        self.all_results = self.all_results.append(result_dict,
                                                   ignore_index=True)
        self.write_to_db(kpi_name, score, display_text=display_text)

    # survey:

    def calculate_survey_specific(self,
                                  kpi_line,
                                  relevant_scif=None,
                                  isnt_dp=None):
        """
        returns a survey line if True or False
        :param kpi_line: line from the survey sheet
        :param relevant_scif:
        :param isnt_dp:
        :return: True or False - if the question gets the needed answer
        """
        question = kpi_line[Const.Q_TEXT]
        if not question:
            question_id = kpi_line[Const.Q_ID]
            if question_id == "":
                Log.warning(
                    "The template has a survey question without ID or text")
                return False
            question = ('question_fk', int(question_id))
        answers = kpi_line[Const.ACCEPTED_ANSWER].split(',')
        min_answer = None if kpi_line[Const.REQUIRED_ANSWER] == '' else True
        for answer in answers:
            if self.survey.check_survey_answer(survey_text=question,
                                               target_answer=answer,
                                               min_required_answer=min_answer):
                return True
        return False

    # availability:

    def calculate_availability_with_same_pack(self, relevant_template,
                                              relevant_scif, isnt_dp, target):
        """
        checks if all the lines in the availability sheet passes the KPI, AND if all of these filtered scif has
        at least one common product that has the same size and number of sub_packages.
        :param relevant_template: all the match lines from the availability sheet.
        :param relevant_scif: filtered scif
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we shouldn't calculate
        DP lines
        :param target: how many lines should pass
        :return: boolean
        """
        relevant_scif = relevant_scif.fillna("NAN")
        # only items categorized as SSD should be evaluated in this calculation; see PROS-6342
        relevant_scif = relevant_scif[relevant_scif['att4'] == 'SSD']
        if relevant_scif.empty:
            return False
        sizes = relevant_scif['size'].tolist()
        sub_packages_nums = relevant_scif['number_of_sub_packages'].tolist()
        packages = set(zip(sizes, sub_packages_nums))
        for package in packages:
            passed_counter = 0
            filtered_scif = relevant_scif[
                (relevant_scif['size'] == package[0])
                & (relevant_scif['number_of_sub_packages'] == package[1])]
            for i, kpi_line in relevant_template.iterrows():
                answer = self.calculate_availability(kpi_line, filtered_scif,
                                                     isnt_dp)
                if answer:
                    passed_counter += 1
            if passed_counter < target:
                return False
        return True

    def calculate_availability(self, kpi_line, relevant_scif, isnt_dp):
        """
        checks if all the lines in the availability sheet passes the KPI (there is at least one product
        in this relevant scif that has the attributes).
        :param relevant_scif: filtered scif
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we shouldn't calculate
        DP lines
        :param kpi_line: line from the availability sheet
        :return: boolean
        """
        if isnt_dp and kpi_line[Const.MANUFACTURER] in Const.DP_MANU:
            return True
        filtered_scif = self.filter_scif_availability(kpi_line, relevant_scif)
        target = kpi_line[Const.TARGET]
        return filtered_scif[
            filtered_scif['facings'] > 0]['facings'].count() >= target

    def filter_scif_specific(self, relevant_scif, kpi_line, name_in_template,
                             name_in_scif):
        """
        takes scif and filters it from the template
        :param relevant_scif: the current filtered scif
        :param kpi_line: line from one sheet (availability for example)
        :param name_in_template: the column name in the template
        :param name_in_scif: the column name in SCIF
        :return:
        """
        values = self.does_exist(kpi_line, name_in_template)
        if values:
            if name_in_scif in Const.NUMERIC_VALUES_TYPES:
                values = [float(x) for x in values]
            return relevant_scif[relevant_scif[name_in_scif].isin(values)]
        return relevant_scif

    def filter_scif_availability(self, kpi_line, relevant_scif):
        """
        calls filter_scif_specific for every column in the template of availability
        :param kpi_line:
        :param relevant_scif:
        :return:
        """
        names_of_columns = {
            Const.MANUFACTURER: "manufacturer_name",
            Const.BRAND: "brand_name",
            Const.TRADEMARK: "att2",
            Const.SIZE: "size",
            Const.NUM_SUB_PACKAGES: "number_of_sub_packages",
            Const.PREMIUM_SSD: "Premium SSD",
            Const.INNOVATION_BRAND: "Innovation Brand",
        }
        for name in names_of_columns:
            relevant_scif = self.filter_scif_specific(relevant_scif, kpi_line,
                                                      name,
                                                      names_of_columns[name])
        return relevant_scif

    # SOS:

    def calculate_sos(self, kpi_line, relevant_scif, isnt_dp):
        """
        calculates SOS line in the relevant scif.
        :param kpi_line: line from SOS sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out of the numerator.
        :return: boolean
        """
        kpi_name = kpi_line[Const.KPI_NAME]
        if kpi_line[Const.EXCLUSION_SHEET] == Const.V:
            exclusion_sheet = self.templates[Const.SKU_EXCLUSION]
            relevant_exclusions = exclusion_sheet[exclusion_sheet[
                Const.KPI_NAME] == kpi_name]
            for i, exc_line in relevant_exclusions.iterrows():
                relevant_scif = self.exclude_scif(exc_line, relevant_scif)
        relevant_scif = relevant_scif[relevant_scif['product_type'] != "Empty"]
        den_type = kpi_line[Const.DEN_TYPES_1]
        den_value = kpi_line[Const.DEN_VALUES_1]
        relevant_scif = self.filter_by_type_value(relevant_scif, den_type,
                                                  den_value)
        if kpi_line[Const.SSD_STILL] != "":
            relevant_scif = self.filter_by_type_value(
                relevant_scif, Const.SSD_STILL, kpi_line[Const.SSD_STILL])
        num_type = kpi_line[Const.NUM_TYPES_1]
        num_value = kpi_line[Const.NUM_VALUES_1]
        num_scif = self.filter_by_type_value(relevant_scif, num_type,
                                             num_value)
        if isnt_dp:
            num_scif = num_scif[~(
                num_scif['manufacturer_name'].isin(Const.DP_MANU))]
        target = float(kpi_line[Const.TARGET]) / 100
        percentage = num_scif['facings'].sum() / relevant_scif['facings'].sum() if relevant_scif['facings'].sum() > 0 \
            else 0
        return percentage >= target

    # SOS majority:

    def calculate_sos_maj(self, kpi_line, relevant_scif, isnt_dp):
        """
        calculates SOS majority line in the relevant scif. Filters the denominator and sends the line to the
        match function (majority or dominant)
        :param kpi_line: line from SOS majority sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out of the numerator (and the denominator of the dominant part).
        :return: boolean
        """
        kpi_name = kpi_line[Const.KPI_NAME]
        if kpi_line[Const.EXCLUSION_SHEET] == Const.V:
            exclusion_sheet = self.templates[Const.SKU_EXCLUSION]
            relevant_exclusions = exclusion_sheet[exclusion_sheet[
                Const.KPI_NAME] == kpi_name]
            for i, exc_line in relevant_exclusions.iterrows():
                relevant_scif = self.exclude_scif(exc_line, relevant_scif)
        relevant_scif = relevant_scif[relevant_scif['product_type'] != "Empty"]
        den_type = kpi_line[Const.DEN_TYPES_1]
        den_value = kpi_line[Const.DEN_VALUES_1]
        relevant_scif = self.filter_by_type_value(relevant_scif, den_type,
                                                  den_value)
        den_type = kpi_line[Const.DEN_TYPES_2]
        den_value = kpi_line[Const.DEN_VALUES_2]
        relevant_scif = self.filter_by_type_value(relevant_scif, den_type,
                                                  den_value)
        if kpi_line[Const.MAJ_DOM] == Const.MAJOR:
            answer = self.calculate_majority_part(kpi_line, relevant_scif,
                                                  isnt_dp)
        elif kpi_line[Const.MAJ_DOM] == Const.DOMINANT:
            answer = self.calculate_dominant_part(kpi_line, relevant_scif,
                                                  isnt_dp)
        else:
            Log.warning("SOS majority does not know '{}' part".format(
                kpi_line[Const.MAJ_DOM]))
            answer = False
        return answer

    def calculate_majority_part(self, kpi_line, relevant_scif, isnt_dp):
        """
        filters the numerator and checks if the SOS is bigger than 50%.
        :param kpi_line: line from SOS majority sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out of the numerator.
        :return: boolean
        """
        num_type = kpi_line[Const.NUM_TYPES_1]
        num_value = kpi_line[Const.NUM_VALUES_1]
        num_scif = self.filter_by_type_value(relevant_scif, num_type,
                                             num_value)
        num_type = kpi_line[Const.NUM_TYPES_2]
        num_value = kpi_line[Const.NUM_VALUES_2]
        num_scif = self.filter_by_type_value(num_scif, num_type, num_value)
        if num_scif.empty:
            return None
        if isnt_dp:
            num_scif = num_scif[~(
                num_scif['manufacturer_name'].isin(Const.DP_MANU))]
        target = Const.MAJORITY_TARGET
        return num_scif['facings'].sum() / relevant_scif['facings'].sum(
        ) >= target

    def calculate_dominant_part(self, kpi_line, relevant_scif, isnt_dp):
        """
        filters the numerator and checks if the given value in the given type is the one with the most facings.
        :param kpi_line: line from SOS majority sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out.
        :return: boolean
        """
        type_name = self.get_column_name(kpi_line[Const.NUM_TYPES_1],
                                         relevant_scif)
        values = str(kpi_line[Const.NUM_VALUES_1]).split(', ')
        if isnt_dp:
            relevant_scif = relevant_scif[~(
                relevant_scif['manufacturer_name'].isin(Const.DP_MANU))]
            if kpi_line[Const.ADD_IF_NOT_DP] != "":
                values_to_add = str(kpi_line[Const.ADD_IF_NOT_DP]).split(', ')
                values = values + values_to_add
        if type_name in Const.NUMERIC_VALUES_TYPES:
            values = [float(x) for x in values]
        max_facings, needed_one = 0, 0
        values_type = relevant_scif[type_name].unique().tolist()
        if None in values_type:
            values_type.remove(None)
            current_sum = relevant_scif[
                relevant_scif[type_name].isnull()]['facings'].sum()
            if current_sum > max_facings:
                max_facings = current_sum
        for value in values_type:
            current_sum = relevant_scif[relevant_scif[type_name] ==
                                        value]['facings'].sum()
            if current_sum > max_facings:
                max_facings = current_sum
            if value in values:
                needed_one += current_sum
        return needed_one >= max_facings

    # helpers:

    def get_column_name(self, field_name, df):
        """
        checks what the real field name in DttFrame is (if it exists in the DF or exists in the "converter" sheet).
        :param field_name: str
        :param df: scif/products
        :return: real column name (if exists)
        """
        if field_name in df.columns:
            return field_name
        if field_name.upper() in self.converters[
                Const.NAME_IN_TEMP].str.upper().tolist():
            field_name = self.converters[self.converters[
                Const.NAME_IN_TEMP].str.upper() == field_name.upper()][
                    Const.NAME_IN_DB].iloc[0]
            return field_name
        return None

    def filter_by_type_value(self, relevant_scif, type_name, value):
        """
        filters scif with the type and value
        :param relevant_scif: current filtered scif
        :param type_name: str (from the template)
        :param value: str
        :return: new scif
        """
        if type_name == "":
            return relevant_scif
        values = value.split(', ')
        new_type_name = self.get_column_name(type_name, relevant_scif)
        if not new_type_name:
            print "There is no field '{}'".format(type_name)
            return relevant_scif
        if new_type_name in Const.NUMERIC_VALUES_TYPES:
            values = [float(x) for x in values]
        return relevant_scif[relevant_scif[new_type_name].isin(values)]

    @staticmethod
    def exclude_scif(exclude_line, relevant_scif):
        """
        filters products out of the scif
        :param exclude_line: line from the exclusion sheet
        :param relevant_scif: current filtered scif
        :return: new scif
        """
        if exclude_line[Const.PRODUCT_EAN] != "":
            exclude_products = exclude_line[Const.PRODUCT_EAN].split(', ')
            relevant_scif = relevant_scif[~(
                relevant_scif['product_ean_code'].isin(exclude_products))]
        if exclude_line[Const.BRAND] != "":
            exclude_brands = exclude_line[Const.BRAND].split(', ')
            relevant_scif = relevant_scif[~(
                relevant_scif['brand_name'].isin(exclude_brands))]
        return relevant_scif

    @staticmethod
    def does_exist(kpi_line, column_name):
        """
        checks if kpi_line has values in this column, and if it does - returns a list of these values
        :param kpi_line: line from template
        :param column_name: str
        :return: list of values if there are, otherwise None
        """
        if column_name in kpi_line.keys() and kpi_line[column_name] != "":
            cell = kpi_line[column_name]
            if type(cell) in [int, float]:
                return [cell]
            elif type(cell) in [unicode, str]:
                return cell.split(", ")
        return None

    def get_kpi_function(self, kpi_type):
        """
        transfers every kpi to its own function
        :param kpi_type: value from "sheet" column in the main sheet
        :return: function
        """
        if kpi_type == Const.SURVEY:
            return self.calculate_survey_specific
        elif kpi_type == Const.AVAILABILITY:
            return self.calculate_availability
        elif kpi_type == Const.SOS:
            return self.calculate_sos
        elif kpi_type == Const.SOS_MAJOR:
            return self.calculate_sos_maj
        else:
            Log.warning(
                "The value '{}' in column sheet in the template is not recognized"
                .format(kpi_type))
            return None

    def choose_and_write_results(self):
        """
        writes all the KPI in the DB: first the session's ones, second the scene's ones and in the end the ones
        that depends on the previous ones. After all it writes the red score
        """
        # self.scenes_results.to_csv('results/{}/scene {}.csv'.format(self.calculation_type, self.session_uid))####
        # self.session_results.to_csv('results/{}/session {}.csv'.format(self.calculation_type, self.session_uid))####
        main_template = self.templates[Const.KPIS]
        self.write_session_kpis(main_template)
        if self.calculation_type == Const.SOVI:
            self.write_scene_kpis(main_template)
        self.write_condition_kpis(main_template)
        self.write_missings(main_template)
        self.write_to_db(self.RED_SCORE, self.red_score)
        # result_dict = {Const.KPI_NAME: 'RED SCORE', Const.SCORE: self.red_score}####
        # self.all_results = self.all_results.append(result_dict, ignore_index=True)####
        # self.all_results.to_csv('results/{}/{}.csv'.format(self.calculation_type, self.session_uid))####

    def write_missings(self, main_template):
        """
        write 0 in all the KPIs that didn't get score
        :param main_template:
        """
        for i, main_line in main_template.iterrows():
            kpi_name = main_line[Const.KPI_NAME]
            if not self.all_results[self.all_results[Const.KPI_NAME] ==
                                    kpi_name].empty:
                continue
            result = 0
            display_text = main_line[Const.DISPLAY_TEXT]
            weight = main_line[Const.WEIGHT]
            self.write_to_all_levels(kpi_name, result, display_text, weight)

    def write_session_kpis(self, main_template):
        """
        iterates all the session's KPIs and saves them
        :param main_template: main_sheet.
        """
        session_template = main_template[main_template[Const.CONDITION] == ""]
        if self.calculation_type == Const.SOVI:
            session_template = session_template[session_template[
                Const.SESSION_LEVEL] == Const.V]
        for i, main_line in session_template.iterrows():
            kpi_name = main_line[Const.KPI_NAME]
            result = self.session_results[self.session_results[Const.KPI_NAME]
                                          == kpi_name]
            if result.empty:
                continue
            result = result.iloc[0][Const.RESULT]
            display_text = main_line[Const.DISPLAY_TEXT]
            weight = main_line[Const.WEIGHT]
            self.write_to_all_levels(kpi_name, result, display_text, weight)

    def write_incremental_kpis(self, scene_template):
        """
        lets the incremental KPIs choose their scenes (if they passed).
        if KPI passed some scenes, we will choose the scene that the children passed
        :param scene_template: filtered main_sheet
        :return: the new template (without the KPI written already)
        """
        incremental_template = scene_template[
            scene_template[Const.INCREMENTAL] != ""]
        while not incremental_template.empty:
            for i, main_line in incremental_template.iterrows():
                kpi_name = main_line[Const.KPI_NAME]
                reuse_scene = main_line[Const.REUSE_SCENE] == Const.V
                kpi_results = self.scenes_results[self.scenes_results[
                    Const.KPI_NAME] == kpi_name]
                if not reuse_scene:
                    kpi_results = kpi_results[~(
                        kpi_results[Const.SCENE_FK].isin(self.used_scenes))]
                true_results = kpi_results[kpi_results[Const.RESULT] > 0]
                increments = main_line[Const.INCREMENTAL]
                if ', ' in increments:
                    first_kpi = increments.split(', ')[0]
                    others = increments.replace(', '.format(first_kpi), '')
                    scene_template.loc[scene_template[Const.KPI_NAME] ==
                                       first_kpi, Const.INCREMENTAL] = others
                if true_results.empty:
                    scene_template.loc[scene_template[Const.KPI_NAME] ==
                                       kpi_name, Const.INCREMENTAL] = ""
                else:
                    true_results = true_results.sort_values(by=Const.RESULT,
                                                            ascending=False)
                    display_text = main_line[Const.DISPLAY_TEXT]
                    weight = main_line[Const.WEIGHT]
                    scene_fk = true_results.iloc[0][Const.SCENE_FK]
                    self.write_to_all_levels(
                        kpi_name,
                        true_results.iloc[0][Const.RESULT],
                        display_text,
                        weight,
                        scene_fk=scene_fk,
                        reuse_scene=reuse_scene)
                    scene_template = scene_template[~(
                        scene_template[Const.KPI_NAME] == kpi_name)]
            incremental_template = scene_template[
                scene_template[Const.INCREMENTAL] != ""]
        return scene_template

    def write_regular_scene_kpis(self, scene_template):
        """
        lets the regular KPIs choose their scenes (if they passed).
        Like in the incremental part - if KPI passed some scenes, we will choose the scene that the children passed
        :param scene_template: filtered main_sheet (only scene KPIs, and without the passed incremental)
        :return: the new template (without the KPI written already)
        """
        for i, main_line in scene_template.iterrows():
            kpi_name = main_line[Const.KPI_NAME]
            reuse_scene = main_line[Const.REUSE_SCENE] == Const.V
            kpi_results = self.scenes_results[self.scenes_results[
                Const.KPI_NAME] == kpi_name]
            if not reuse_scene:
                kpi_results = kpi_results[~(
                    kpi_results[Const.SCENE_FK].isin(self.used_scenes))]
            true_results = kpi_results[kpi_results[Const.RESULT] > 0]
            display_text = main_line[Const.DISPLAY_TEXT]
            weight = main_line[Const.WEIGHT]
            if true_results.empty:
                continue
            true_results = true_results.sort_values(by=Const.RESULT,
                                                    ascending=False)
            scene_fk = true_results.iloc[0][Const.SCENE_FK]
            self.write_to_all_levels(kpi_name,
                                     true_results.iloc[0][Const.RESULT],
                                     display_text,
                                     weight,
                                     scene_fk=scene_fk,
                                     reuse_scene=reuse_scene)
            scene_template = scene_template[~(
                scene_template[Const.KPI_NAME] == kpi_name)]
        return scene_template

    def write_not_passed_scene_kpis(self, scene_template):
        """
        lets the KPIs not passed choose their scenes.
        :param scene_template: filtered main_sheet (only scene KPIs, and without the passed KPIs)
        """
        for i, main_line in scene_template.iterrows():
            kpi_name = main_line[Const.KPI_NAME]
            reuse_scene = main_line[Const.REUSE_SCENE] == Const.V
            kpi_results = self.scenes_results[self.scenes_results[
                Const.KPI_NAME] == kpi_name]
            if not reuse_scene:
                kpi_results = kpi_results[~(
                    kpi_results[Const.SCENE_FK].isin(self.used_scenes))]
            display_text = main_line[Const.DISPLAY_TEXT]
            weight = main_line[Const.WEIGHT]
            if kpi_results.empty:
                continue
            scene_fk = kpi_results.iloc[0][Const.SCENE_FK]
            self.write_to_all_levels(kpi_name,
                                     0,
                                     display_text,
                                     weight,
                                     scene_fk=scene_fk,
                                     reuse_scene=reuse_scene)

    def write_scene_kpis(self, main_template):
        """
        iterates every scene_kpi that does not depend on others, and choose the scene they will take:
        1. the incrementals take their scene (if they passed).
        2. the regular KPIs that passed choose their scenes.
        3. the ones that didn't pass choose their random scenes.
        :param main_template: main_sheet.
        """
        scene_template = main_template[
            (main_template[Const.SESSION_LEVEL] != Const.V)
            & (main_template[Const.CONDITION] == "")]
        scene_template = self.write_incremental_kpis(scene_template)
        scene_template = self.write_regular_scene_kpis(scene_template)
        self.write_not_passed_scene_kpis(scene_template)

    def write_condition_kpis(self, main_template):
        """
        writes all the KPI that depend on other KPIs by checking if the parent KPI has passed and in which scene.
        :param main_template: main_sheet
        """
        condition_template = main_template[
            main_template[Const.CONDITION] != '']
        for i, main_line in condition_template.iterrows():
            condition = main_line[Const.CONDITION]
            kpi_name = main_line[Const.KPI_NAME]
            if self.calculation_type == Const.MANUAL or main_line[
                    Const.SESSION_LEVEL] == Const.V:
                kpi_results = self.session_results[self.session_results[
                    Const.KPI_NAME] == kpi_name]
            else:
                kpi_results = self.scenes_results[self.scenes_results[
                    Const.KPI_NAME] == kpi_name]
            condition_result = self.all_results[
                (self.all_results[Const.KPI_NAME] == condition)
                & (self.all_results[Const.RESULT] > 0)]
            if condition_result.empty:
                continue
            condition_result = condition_result.iloc[0]
            condition_scene = condition_result[Const.SCENE_FK]
            if condition_scene and Const.SCENE_FK in kpi_results:
                results = kpi_results[kpi_results[Const.SCENE_FK] ==
                                      condition_scene]
            else:
                results = kpi_results
            if results.empty:
                continue
            result = results.iloc[0][Const.RESULT]
            display_text = main_line[Const.DISPLAY_TEXT]
            weight = main_line[Const.WEIGHT]
            scene_fk = results.iloc[0][
                Const.SCENE_FK] if Const.SCENE_FK in kpi_results else None
            self.write_to_all_levels(kpi_name,
                                     result,
                                     display_text,
                                     weight,
                                     scene_fk=scene_fk)

    def get_united_scenes(self):
        return self.scif[self.scif['United Deliver'] ==
                         'Y']['scene_id'].unique().tolist()

    def get_weight_factor(self):
        sum_weights = self.templates[Const.KPIS][Const.WEIGHT].sum()
        return sum_weights / 100.0

    def get_score(self, weight):
        return weight / self.weight_factor

    def write_to_db(self, kpi_name, score, display_text=''):
        """
        writes result in the DB
        :param kpi_name: str
        :param score: float
        :param display_text: str
        """
        if kpi_name == self.RED_SCORE:
            self.write_to_db_result(self.common_db.get_kpi_fk_by_kpi_name(
                self.RED_SCORE, 1),
                                    score=score,
                                    level=1)
            if self.common_db_integ:
                self.write_to_db_result(
                    self.common_db_integ.get_kpi_fk_by_kpi_name(
                        self.RED_SCORE_INTEG, 1),
                    score=score,
                    level=1,
                    set_type=Const.MANUAL)
        else:
            self.write_to_db_result(self.common_db.get_kpi_fk_by_kpi_name(
                kpi_name, 2),
                                    score=score,
                                    level=2)
            self.write_to_db_result(self.common_db.get_kpi_fk_by_kpi_name(
                kpi_name, 3),
                                    score=score,
                                    level=3,
                                    display_text=display_text)
            if self.common_db_integ:
                self.write_to_db_result(
                    self.common_db_integ.get_kpi_fk_by_kpi_name(kpi_name, 3),
                    score=score,
                    level=3,
                    display_text=kpi_name,
                    set_type=Const.MANUAL)

    def write_to_db_result(self,
                           fk,
                           level,
                           score,
                           set_type=Const.SOVI,
                           **kwargs):
        """
        This function creates the result data frame of every KPI (atomic KPI/KPI/KPI set),
        and appends the insert SQL query into the queries' list, later to be written to the DB.
        """
        if kwargs:
            kwargs['score'] = score
            attributes = self.create_attributes_dict(fk=fk,
                                                     level=level,
                                                     set_type=set_type,
                                                     **kwargs)
        else:
            attributes = self.create_attributes_dict(fk=fk,
                                                     score=score,
                                                     set_type=set_type,
                                                     level=level)
        if level == self.common_db.LEVEL1:
            table = self.common_db.KPS_RESULT
        elif level == self.common_db.LEVEL2:
            table = self.common_db.KPK_RESULT
        elif level == self.common_db.LEVEL3:
            table = self.common_db.KPI_RESULT
        else:
            return
        query = insert(attributes, table)
        if set_type == Const.SOVI:
            self.common_db.kpi_results_queries.append(query)
        else:
            self.common_db_integ.kpi_results_queries.append(query)

    def create_attributes_dict(self,
                               score,
                               fk=None,
                               level=None,
                               display_text=None,
                               set_type=Const.SOVI,
                               **kwargs):
        """
        This function creates a data frame with all attributes needed for saving in KPI results tables.
        or
        you can send dict with all values in kwargs
        """
        kpi_static_data = self.kpi_static_data if set_type == Const.SOVI else self.kpi_static_data_integ
        if level == self.common_db.LEVEL1:
            if kwargs:
                kwargs['score'] = score
                values = [val for val in kwargs.values()]
                col = [col for col in kwargs.keys()]
                attributes = pd.DataFrame(values, columns=col)
            else:
                kpi_set_name = kpi_static_data[kpi_static_data['kpi_set_fk'] ==
                                               fk]['kpi_set_name'].values[0]
                attributes = pd.DataFrame(
                    [(kpi_set_name, self.session_uid, self.store_id,
                      self.visit_date.isoformat(), format(score, '.2f'), fk)],
                    columns=[
                        'kps_name', 'session_uid', 'store_fk', 'visit_date',
                        'score_1', 'kpi_set_fk'
                    ])
        elif level == self.common_db.LEVEL2:
            if kwargs:
                kwargs['score'] = score
                values = [val for val in kwargs.values()]
                col = [col for col in kwargs.keys()]
                attributes = pd.DataFrame(values, columns=col)
            else:
                kpi_name = kpi_static_data[kpi_static_data['kpi_fk'] ==
                                           fk]['kpi_name'].values[0].replace(
                                               "'", "\\'")
                attributes = pd.DataFrame(
                    [(self.session_uid, self.store_id,
                      self.visit_date.isoformat(), fk, kpi_name, score)],
                    columns=[
                        'session_uid', 'store_fk', 'visit_date', 'kpi_fk',
                        'kpk_name', 'score'
                    ])
        elif level == self.common_db.LEVEL3:
            if kwargs:
                kwargs['score'] = score
                values = tuple([val for val in kwargs.values()])
                col = [col for col in kwargs.keys()]
                attributes = pd.DataFrame([values], columns=col)
            else:
                data = kpi_static_data[kpi_static_data['atomic_kpi_fk'] == fk]
                kpi_fk = data['kpi_fk'].values[0]
                kpi_set_name = kpi_static_data[kpi_static_data['atomic_kpi_fk']
                                               == fk]['kpi_set_name'].values[0]
                attributes = pd.DataFrame(
                    [(display_text, self.session_uid, kpi_set_name,
                      self.store_id, self.visit_date.isoformat(),
                      datetime.utcnow().isoformat(), score, kpi_fk, fk)],
                    columns=[
                        'display_text', 'session_uid', 'kps_name', 'store_fk',
                        'visit_date', 'calculation_time', 'score', 'kpi_fk',
                        'atomic_kpi_fk'
                    ])
        else:
            attributes = pd.DataFrame()
        return attributes.to_dict()

    def commit_results(self):
        """
        committing the results in both sets
        """
        self.common_db.delete_results_data_by_kpi_set()
        self.common_db.commit_results_data_without_delete()
        if self.common_db_integ:
            self.common_db_integ.delete_results_data_by_kpi_set()
            self.common_db_integ.commit_results_data_without_delete()
Пример #3
0
class CMAToolBox:
    EXCLUDE_FILTER = 0
    INCLUDE_FILTER = 1
    CONTAIN_FILTER = 2

    def __init__(self, data_provider, output, common_db2):
        self.output = output
        self.data_provider = data_provider
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.manufacturer_fk = 1
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.united_scenes = self.get_united_scenes(
        )  # we don't need to check scenes without United products
        self.survey = Survey(self.data_provider, self.output)
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.sos = SOS(self.data_provider, self.output)
        self.templates = {}
        self.common_db = Common(self.data_provider, SUB_PROJECT)
        self.common_db2 = common_db2
        self.result_values = self.ps_data_provider.get_result_values()
        self.region = self.store_info['region_name'].iloc[0]
        self.store_type = self.store_info['store_type'].iloc[0]
        self.program = self.store_info['additional_attribute_14'].iloc[0]
        self.sales_center = self.store_info['additional_attribute_5'].iloc[0]
        if self.store_type in STORE_TYPES:
            self.store_type = STORE_TYPES[self.store_type]
        self.store_attr = self.store_info['additional_attribute_15'].iloc[0]
        self.kpi_static_data = self.common_db.get_kpi_static_data()
        self.ignore_stacking = False
        self.facings_field = 'facings' if not self.ignore_stacking else 'facings_ign_stack'
        self.total_score = 0
        self.total_count = 0
        for sheet in Const.SHEETS_CMA:
            self.templates[sheet] = pd.read_excel(TEMPLATE_PATH,
                                                  sheetname=sheet).fillna('')
        self.tools = Shared(self.data_provider, self.output)

    # main functions:

    def main_calculation(self, *args, **kwargs):
        """
            This function gets all the scene results from the SceneKPI, after that calculates every session's KPI,
            and in the end it calls "filter results" to choose every KPI and scene and write the results in DB.
        """
        main_template = self.templates[Const.KPIS]
        if self.region in Const.REGIONS:
            for i, main_line in main_template.iterrows():
                store_type = self.does_exist(main_line, Const.STORE_TYPE)
                if store_type is None or self.store_type in self.does_exist(
                        main_line, Const.STORE_TYPE):
                    self.calculate_main_kpi(main_line)
            kpi_fk = self.common_db2.get_kpi_fk_by_kpi_name(SUB_PROJECT)

            result = 0
            if self.total_count:
                result = self.total_score * 100.0 / self.total_count
            self.common_db2.write_to_db_result(
                fk=kpi_fk,
                result=result,
                numerator_result=self.total_score,
                numerator_id=self.manufacturer_fk,
                denominator_result=self.total_count,
                denominator_id=self.store_id,
                identifier_result=self.common_db2.get_dictionary(
                    parent_name=SUB_PROJECT))
            self.write_to_db_result(self.common_db.get_kpi_fk_by_kpi_name(
                SUB_PROJECT, 1),
                                    score=self.total_score,
                                    level=1)

    def calculate_main_kpi(self, main_line):
        """
        This function gets a line from the main_sheet, transfers it to the match function, and checks all of the
        KPIs in the same name in the match sheet.
        :param main_line: series from the template of the main_sheet.
        """
        kpi_name = main_line[Const.KPI_NAME]
        kpi_type = main_line[Const.TYPE]
        relevant_scif = self.scif[self.scif['scene_id'].isin(
            self.united_scenes)]
        scene_types = self.does_exist(main_line, Const.SCENE_TYPE)
        result = score = target = None
        general_filters = {}
        if scene_types:
            relevant_scif = relevant_scif[relevant_scif['template_name'].isin(
                scene_types)]
            general_filters['template_name'] = scene_types
        scene_groups = self.does_exist(main_line, Const.TEMPLATE_GROUP)
        if scene_groups:
            relevant_scif = relevant_scif[relevant_scif['template_group'].isin(
                scene_groups)]
            general_filters['template_group'] = scene_groups
        if kpi_type == Const.SOS:
            isnt_dp = True if self.store_attr != Const.DP and main_line[
                Const.STORE_ATTRIBUTE] == Const.DP else False
            relevant_template = self.templates[kpi_type]
            relevant_template = relevant_template[relevant_template[
                Const.KPI_NAME] == kpi_name]
            kpi_function = self.get_kpi_function(kpi_type)
            for i, kpi_line in relevant_template.iterrows():
                result, score, target = kpi_function(kpi_line, relevant_scif,
                                                     isnt_dp, general_filters)
        else:
            pass
        self.total_count += 1
        if score > 0:
            self.total_score += 1
        if isinstance(result, tuple):
            self.write_to_all_levels(kpi_name=kpi_name,
                                     result=result[0],
                                     score=score,
                                     target=target,
                                     num=result[1],
                                     den=result[2])
        else:
            self.write_to_all_levels(kpi_name=kpi_name,
                                     result=result,
                                     score=score,
                                     target=target)

    # write in DF:
    def write_to_all_levels(self,
                            kpi_name,
                            result,
                            score,
                            target=None,
                            num=None,
                            den=None):
        """
        Writes the final result in the "all" DF, add the score to the red score and writes the KPI in the DB
        :param kpi_name: str
        :param result: int
        :param display_text: str
        :param weight: int/float
        :param scene_fk: for the scene's kpi
        :param reuse_scene: this kpi can use scenes that were used
        """
        # result_dict = {Const.KPI_NAME: kpi_name, Const.RESULT: result, Const.SCORE: score, Const.THRESHOLD: target}
        # self.all_results = self.all_results.append(result_dict, ignore_index=True)
        self.write_to_db(kpi_name,
                         score,
                         result=result,
                         target=target,
                         num=num,
                         den=den)

    # survey:

    def calculate_survey_specific(self,
                                  kpi_line,
                                  relevant_scif=None,
                                  isnt_dp=None):
        """
        returns a survey line if True or False
        :param kpi_line: line from the survey sheet
        :param relevant_scif:
        :param isnt_dp:
        :return: True or False - if the question gets the needed answer
        """
        question = kpi_line[Const.Q_TEXT]
        if not question:
            question_id = kpi_line[Const.Q_ID]
            if question_id == "":
                Log.warning(
                    "The template has a survey question without ID or text")
                return False
            question = ('question_fk', int(question_id))
        answers = kpi_line[Const.ACCEPTED_ANSWER].split(',')
        min_answer = None if kpi_line[Const.REQUIRED_ANSWER] == '' else True
        for answer in answers:
            if self.survey.check_survey_answer(survey_text=question,
                                               target_answer=answer,
                                               min_required_answer=min_answer):
                return True
        return False

    # availability:

    def calculate_availability_with_same_pack(self, relevant_template,
                                              relevant_scif, isnt_dp):
        """
        checks if all the lines in the availability sheet passes the KPI, AND if all of these filtered scif has
        at least one common product that has the same size and number of sub_packages.
        :param relevant_template: all the match lines from the availability sheet.
        :param relevant_scif: filtered scif
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we shouldn't calculate
        DP lines
        :return: boolean
        """
        packages = None
        for i, kpi_line in relevant_template.iterrows():
            if isnt_dp and kpi_line[Const.MANUFACTURER] in Const.DP_MANU:
                continue
            filtered_scif = self.filter_scif_availability(
                kpi_line, relevant_scif)
            filtered_scif = filtered_scif.fillna("NAN")
            target = kpi_line[Const.TARGET]
            sizes = filtered_scif['size'].tolist()
            sub_packages_nums = filtered_scif['number_of_sub_packages'].tolist(
            )
            cur_packages = set(zip(sizes, sub_packages_nums))
            if packages is None:
                packages = cur_packages
            else:
                packages = cur_packages & packages
                if len(packages) == 0:
                    return False
            if filtered_scif[
                    filtered_scif['facings'] > 0]['facings'].count() < target:
                return False
        if len(packages) > 1:
            return False
        return True

    def calculate_availability(self, kpi_line, relevant_scif, isnt_dp):
        """
        checks if all the lines in the availability sheet passes the KPI (there is at least one product
        in this relevant scif that has the attributes).
        :param relevant_scif: filtered scif
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we shouldn't calculate
        DP lines
        :param kpi_line: line from the availability sheet
        :return: boolean
        """
        if isnt_dp and kpi_line[Const.MANUFACTURER] in Const.DP_MANU:
            return True
        filtered_scif = self.filter_scif_availability(kpi_line, relevant_scif)
        target = kpi_line[Const.TARGET]
        return filtered_scif[
            filtered_scif['facings'] > 0]['facings'].count() >= target

    def filter_scif_specific(self, relevant_scif, kpi_line, name_in_template,
                             name_in_scif):
        """
        takes scif and filters it from the template
        :param relevant_scif: the current filtered scif
        :param kpi_line: line from one sheet (availability for example)
        :param name_in_template: the column name in the template
        :param name_in_scif: the column name in SCIF
        :return:
        """
        values = self.does_exist(kpi_line, name_in_template)
        if values:
            if name_in_scif in Const.NUMERIC_VALUES_TYPES:
                values = [float(x) for x in values]
            return relevant_scif[relevant_scif[name_in_scif].isin(values)]
        return relevant_scif

    def filter_scif_availability(self, kpi_line, relevant_scif):
        """
        calls filter_scif_specific for every column in the template of availability
        :param kpi_line:
        :param relevant_scif:
        :return:
        """
        names_of_columns = {
            Const.MANUFACTURER: "manufacturer_name",
            Const.BRAND: "brand_name",
            Const.TRADEMARK: "att2",
            Const.SIZE: "size",
            Const.NUM_SUB_PACKAGES: "number_of_sub_packages",
        }
        for name in names_of_columns:
            relevant_scif = self.filter_scif_specific(relevant_scif, kpi_line,
                                                      name,
                                                      names_of_columns[name])
        return relevant_scif

    # SOS:

    def calculate_sos(self, kpi_line, relevant_scif, isnt_dp, general_filters):
        """
        calculates SOS line in the relevant scif.
        :param kpi_line: line from SOS sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out of the numerator.
        :return: boolean
        """
        kpi_name = kpi_line[Const.KPI_NAME]
        den_type = kpi_line[Const.DEN_TYPES_1]
        den_value = kpi_line[Const.DEN_VALUES_1].split(',')
        num_type = kpi_line[Const.NUM_TYPES_1]
        num_value = kpi_line[Const.NUM_VALUES_1].split(',')
        target = self.get_sos_targets(kpi_name)
        general_filters[den_type] = den_value
        if kpi_line[Const.DEN_TYPES_2]:
            den_type_2 = kpi_line[Const.DEN_TYPES_2]
            den_value_2 = kpi_line[Const.DEN_VALUES_2].split(',')
            general_filters[den_type_2] = den_value_2
        sos_filters = {num_type: num_value}
        if isnt_dp:
            sos_filters['manufacturer_name'] = (Const.DP_MANU, 0)
        if kpi_line[Const.NUM_TYPES_2]:
            num_type_2 = kpi_line[Const.NUM_TYPES_2]
            num_value_2 = kpi_line[Const.NUM_VALUES_2].split(',')
            sos_filters[num_type_2] = num_value_2

        num_scif = relevant_scif[self.get_filter_condition(
            relevant_scif, **sos_filters)]
        den_scif = relevant_scif[self.get_filter_condition(
            relevant_scif, **general_filters)]
        sos_value, num, den = self.tools.sos_with_num_and_dem(
            kpi_line, num_scif, den_scif, self.facings_field)
        # sos_value = self.sos.calculate_share_of_shelf(sos_filters, **general_filters)
        # sos_value *= 100
        # sos_value = round(sos_value, 2)

        if target:
            target *= 100
            score = 1 if sos_value >= target else 0
        else:
            score = 0
            target = 0
        return (sos_value, num, den), score, target

    # SOS majority:

    def get_sos_targets(self, kpi_name):
        targets_template = self.templates[Const.TARGETS]
        store_targets = targets_template.loc[
            (targets_template['program'] == self.program)
            & (targets_template['channel'] == self.store_type)]
        filtered_targets_to_kpi = store_targets.loc[
            targets_template['KPI name'] == kpi_name]
        if not filtered_targets_to_kpi.empty:
            target = filtered_targets_to_kpi[Const.TARGET].values[0]
        else:
            target = None
        return target
        # return False

    def calculate_sos_maj(self, kpi_line, relevant_scif, isnt_dp):
        """
        calculates SOS majority line in the relevant scif. Filters the denominator and sends the line to the
        match function (majority or dominant)
        :param kpi_line: line from SOS majority sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out of the numerator (and the denominator of the dominant part).
        :return: boolean
        """
        kpi_name = kpi_line[Const.KPI_NAME]
        if kpi_line[Const.EXCLUSION_SHEET] == Const.V:
            exclusion_sheet = self.templates[Const.SKU_EXCLUSION]
            relevant_exclusions = exclusion_sheet[exclusion_sheet[
                Const.KPI_NAME] == kpi_name]
            for i, exc_line in relevant_exclusions.iterrows():
                relevant_scif = self.exclude_scif(exc_line, relevant_scif)
        relevant_scif = relevant_scif[relevant_scif['product_type'] != "Empty"]
        den_type = kpi_line[Const.DEN_TYPES_1]
        den_value = kpi_line[Const.DEN_VALUES_1]
        relevant_scif = self.filter_by_type_value(relevant_scif, den_type,
                                                  den_value)
        den_type = kpi_line[Const.DEN_TYPES_2]
        den_value = kpi_line[Const.DEN_VALUES_2]
        relevant_scif = self.filter_by_type_value(relevant_scif, den_type,
                                                  den_value)
        if kpi_line[Const.MAJ_DOM] == Const.MAJOR:
            answer = self.calculate_majority_part(kpi_line, relevant_scif,
                                                  isnt_dp)
        elif kpi_line[Const.MAJ_DOM] == Const.DOMINANT:
            answer = self.calculate_dominant_part(kpi_line, relevant_scif,
                                                  isnt_dp)
        else:
            Log.warning("SOS majority does not know '{}' part".format(
                kpi_line[Const.MAJ_DOM]))
            answer = False
        return answer

    def calculate_majority_part(self, kpi_line, relevant_scif, isnt_dp):
        """
        filters the numerator and checks if the SOS is bigger than 50%.
        :param kpi_line: line from SOS majority sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out of the numerator.
        :return: boolean
        """
        num_type = kpi_line[Const.NUM_TYPES_1]
        num_value = kpi_line[Const.NUM_VALUES_1]
        num_scif = self.filter_by_type_value(relevant_scif, num_type,
                                             num_value)
        num_type = kpi_line[Const.NUM_TYPES_2]
        num_value = kpi_line[Const.NUM_VALUES_2]
        num_scif = self.filter_by_type_value(num_scif, num_type, num_value)
        if num_scif.empty:
            return None
        if isnt_dp:
            num_scif = num_scif[~(
                num_scif['manufacturer_name'].isin(Const.DP_MANU))]
        target = Const.MAJORITY_TARGET
        return num_scif['facings'].sum() / relevant_scif['facings'].sum(
        ) >= target

    def calculate_dominant_part(self, kpi_line, relevant_scif, isnt_dp):
        """
        filters the numerator and checks if the given value in the given type is the one with the most facings.
        :param kpi_line: line from SOS majority sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out.
        :return: boolean
        """
        if isnt_dp:
            relevant_scif = relevant_scif[~(
                relevant_scif['manufacturer_name'].isin(Const.DP_MANU))]
        type_name = self.get_column_name(kpi_line[Const.NUM_TYPES_1],
                                         relevant_scif)
        values = str(kpi_line[Const.NUM_VALUES_1]).split(', ')
        if type_name in Const.NUMERIC_VALUES_TYPES:
            values = [float(x) for x in values]
        max_facings, needed_one = 0, 0
        values_type = relevant_scif[type_name].unique().tolist()
        if None in values_type:
            values_type.remove(None)
            current_sum = relevant_scif[
                relevant_scif[type_name].isnull()]['facings'].sum()
            if current_sum > max_facings:
                max_facings = current_sum
        for value in values_type:
            current_sum = relevant_scif[relevant_scif[type_name] ==
                                        value]['facings'].sum()
            if current_sum > max_facings:
                max_facings = current_sum
            if value in values:
                needed_one += current_sum
        return needed_one >= max_facings

    def get_filter_condition(self, df, **filters):
        """
        :param df: The data frame to be filters.
        :param filters: These are the parameters which the data frame is filtered by.
                       Every parameter would be a tuple of the value and an include/exclude flag.
                       INPUT EXAMPLE (1):   manufacturer_name = ('Diageo', DIAGEOAUPNGAMERICAGENERALToolBox.INCLUDE_FILTER)
                       INPUT EXAMPLE (2):   manufacturer_name = 'Diageo'
        :return: a filtered Scene Item Facts data frame.
        """
        if not filters:
            return df['pk'].apply(bool)
        if self.facings_field in df.keys():
            filter_condition = (df[self.facings_field] > 0)
        else:
            filter_condition = None
        for field in filters.keys():
            if field in df.keys():
                if isinstance(filters[field], tuple):
                    value, exclude_or_include = filters[field]
                else:
                    value, exclude_or_include = filters[
                        field], self.INCLUDE_FILTER
                if not value:
                    continue
                if not isinstance(value, list):
                    value = [value]
                if exclude_or_include == self.INCLUDE_FILTER:
                    condition = (df[field].isin(value))
                elif exclude_or_include == self.EXCLUDE_FILTER:
                    condition = (~df[field].isin(value))
                elif exclude_or_include == self.CONTAIN_FILTER:
                    condition = (df[field].str.contains(value[0], regex=False))
                    for v in value[1:]:
                        condition |= df[field].str.contains(v, regex=False)
                else:
                    continue
                if filter_condition is None:
                    filter_condition = condition
                else:
                    filter_condition &= condition
            else:
                Log.warning('field {} is not in the Data Frame'.format(field))

        return filter_condition

    # helpers:
    @staticmethod
    def get_column_name(field_name, df):
        """
        checks what the real field name in DttFrame is (if it exists in the DF or exists in the "converter" sheet).
        :param field_name: str
        :param df: scif/products
        :return: real column name (if exists)
        """
        if field_name in df.columns:
            return field_name
        return None

    def filter_by_type_value(self, relevant_scif, type_name, value):
        """
        filters scif with the type and value
        :param relevant_scif: current filtered scif
        :param type_name: str (from the template)
        :param value: str
        :return: new scif
        """
        if type_name == "":
            return relevant_scif
        values = value.split(', ')
        new_type_name = self.get_column_name(type_name, relevant_scif)
        if not new_type_name:
            print "There is no field '{}'".format(type_name)
            return relevant_scif
        if new_type_name in Const.NUMERIC_VALUES_TYPES:
            values = [float(x) for x in values]
        return relevant_scif[relevant_scif[new_type_name].isin(values)]

    @staticmethod
    def exclude_scif(exclude_line, relevant_scif):
        """
        filters products out of the scif
        :param exclude_line: line from the exclusion sheet
        :param relevant_scif: current filtered scif
        :return: new scif
        """
        exclude_products = exclude_line[Const.PRODUCT_EAN].split(', ')
        return relevant_scif[~(
            relevant_scif['product_ean_code'].isin(exclude_products))]

    @staticmethod
    def does_exist(kpi_line, column_name):
        """
        checks if kpi_line has values in this column, and if it does - returns a list of these values
        :param kpi_line: line from template
        :param column_name: str
        :return: list of values if there are, otherwise None
        """
        if column_name in kpi_line.keys() and kpi_line[column_name] != "":
            cell = kpi_line[column_name]
            if type(cell) in [int, float]:
                return [cell]
            elif type(cell) in [unicode, str]:
                if ", " in cell:
                    return cell.split(", ")
                else:
                    return cell.split(',')
        return None

    def get_kpi_function(self, kpi_type):
        """
        transfers every kpi to its own function
        :param kpi_type: value from "sheet" column in the main sheet
        :return: function
        """
        if kpi_type == Const.SURVEY:
            return self.calculate_survey_specific
        elif kpi_type == Const.AVAILABILITY:
            return self.calculate_availability
        elif kpi_type == Const.SOS:
            return self.calculate_sos
        elif kpi_type == Const.SOS_MAJOR:
            return self.calculate_sos_maj
        else:
            Log.warning(
                "The value '{}' in column sheet in the template is not recognized"
                .format(kpi_type))
            return None

    def get_united_scenes(self):
        return self.scif[self.scif['United Deliver'] ==
                         'Y']['scene_id'].unique().tolist()

    def get_pks_of_result(self, result):
        """
        converts string result to its pk (in static.kpi_result_value)
        :param result: str
        :return: int
        """
        pk = self.result_values[self.result_values['value'] ==
                                result]['pk'].iloc[0]
        return pk

    def write_to_db(self,
                    kpi_name,
                    score,
                    result=None,
                    target=None,
                    num=None,
                    den=None):
        """
        writes result in the DB
        :param kpi_name: str
        :param score: float
        :param result: str
        :param target: int
        """
        if target and score == 0:
            delta = den * (target / 100) - num
        else:
            delta = 0
        score_value = Const.PASS if score == 1 else Const.FAIL
        score = self.get_pks_of_result(score_value)
        kpi_fk = self.common_db2.get_kpi_fk_by_kpi_type(SUB_PROJECT + " " +
                                                        kpi_name)
        self.common_db2.write_to_db_result(
            fk=kpi_fk,
            result=result,
            score=score,
            should_enter=True,
            target=target,
            numerator_result=num,
            denominator_result=den,
            weight=delta,
            numerator_id=Const.MANUFACTURER_FK,
            denominator_id=self.store_id,
            identifier_parent=self.common_db2.get_dictionary(
                parent_name=SUB_PROJECT))
        self.write_to_db_result(self.common_db.get_kpi_fk_by_kpi_name(
            kpi_name, 2),
                                score=score,
                                level=2)
        self.write_to_db_result(self.common_db.get_kpi_fk_by_kpi_name(
            kpi_name, 3),
                                score=score,
                                level=3,
                                threshold=target,
                                result=result)

    def write_to_db_result(self,
                           fk,
                           level,
                           score,
                           set_type=Const.SOVI,
                           **kwargs):
        """
        This function creates the result data frame of every KPI (atomic KPI/KPI/KPI set),
        and appends the insert SQL query into the queries' list, later to be written to the DB.
        """
        if kwargs:
            kwargs['score'] = score
            attributes = self.create_attributes_dict(fk=fk,
                                                     level=level,
                                                     **kwargs)
        else:
            attributes = self.create_attributes_dict(fk=fk,
                                                     score=score,
                                                     level=level)
        if level == self.common_db.LEVEL1:
            table = self.common_db.KPS_RESULT
        elif level == self.common_db.LEVEL2:
            table = self.common_db.KPK_RESULT
        elif level == self.common_db.LEVEL3:
            table = self.common_db.KPI_RESULT
        else:
            return
        query = insert(attributes, table)
        self.common_db.kpi_results_queries.append(query)

    def create_attributes_dict(self,
                               score,
                               fk=None,
                               level=None,
                               display_text=None,
                               set_type=Const.SOVI,
                               **kwargs):
        """
        This function creates a data frame with all attributes needed for saving in KPI results tables.
        or
        you can send dict with all values in kwargs
        """
        kpi_static_data = self.kpi_static_data if set_type == Const.SOVI else self.kpi_static_data_integ
        if level == self.common_db.LEVEL1:
            if kwargs:
                kwargs['score'] = score
                values = [val for val in kwargs.values()]
                col = [col for col in kwargs.keys()]
                attributes = pd.DataFrame(values, columns=col)
            else:
                kpi_set_name = kpi_static_data[kpi_static_data['kpi_set_fk'] ==
                                               fk]['kpi_set_name'].values[0]
                attributes = pd.DataFrame(
                    [(kpi_set_name, self.session_uid, self.store_id,
                      self.visit_date.isoformat(), format(score, '.2f'), fk)],
                    columns=[
                        'kps_name', 'session_uid', 'store_fk', 'visit_date',
                        'score_1', 'kpi_set_fk'
                    ])
        elif level == self.common_db.LEVEL2:
            if kwargs:
                kwargs['score'] = score
                values = [val for val in kwargs.values()]
                col = [col for col in kwargs.keys()]
                attributes = pd.DataFrame(values, columns=col)
            else:
                kpi_name = kpi_static_data[kpi_static_data['kpi_fk'] ==
                                           fk]['kpi_name'].values[0].replace(
                                               "'", "\\'")
                attributes = pd.DataFrame(
                    [(self.session_uid, self.store_id,
                      self.visit_date.isoformat(), fk, kpi_name, score)],
                    columns=[
                        'session_uid', 'store_fk', 'visit_date', 'kpi_fk',
                        'kpk_name', 'score'
                    ])
        elif level == self.common_db.LEVEL3:
            data = kpi_static_data[kpi_static_data['atomic_kpi_fk'] == fk]
            kpi_fk = data['kpi_fk'].values[0]
            kpi_set_name = kpi_static_data[kpi_static_data['atomic_kpi_fk'] ==
                                           fk]['kpi_set_name'].values[0]
            display_text = data['kpi_name'].values[0]
            if kwargs:
                kwargs = self.add_additional_data_to_attributes(
                    kwargs, score, kpi_set_name, kpi_fk, fk,
                    datetime.utcnow().isoformat(), display_text)

                values = tuple([val for val in kwargs.values()])
                col = [col for col in kwargs.keys()]
                attributes = pd.DataFrame([values], columns=col)
            else:
                attributes = pd.DataFrame(
                    [(display_text, self.session_uid, kpi_set_name,
                      self.store_id, self.visit_date.isoformat(),
                      datetime.utcnow().isoformat(), score, kpi_fk, fk)],
                    columns=[
                        'display_text', 'session_uid', 'kps_name', 'store_fk',
                        'visit_date', 'calculation_time', 'score', 'kpi_fk',
                        'atomic_kpi_fk'
                    ])
        else:
            attributes = pd.DataFrame()
        return attributes.to_dict()

    def add_additional_data_to_attributes(self, kwargs_dict, score,
                                          kpi_set_name, kpi_fk, fk, calc_time,
                                          display_text):
        kwargs_dict['score'] = score
        kwargs_dict['kps_name'] = kpi_set_name
        kwargs_dict['kpi_fk'] = kpi_fk
        kwargs_dict['atomic_kpi_fk'] = fk
        kwargs_dict['calculation_time'] = calc_time
        kwargs_dict['session_uid'] = self.session_uid
        kwargs_dict['store_fk'] = self.store_id
        kwargs_dict['visit_date'] = self.visit_date.isoformat()
        kwargs_dict['display_text'] = display_text
        return kwargs_dict

    def commit_results(self):
        """
        committing the results in both sets
        """
        self.common_db.delete_results_data_by_kpi_set()
        self.common_db.commit_results_data_without_delete()
Пример #4
0
class LIBERTYToolBox:

    def __init__(self, data_provider, output, common_db):
        self.output = output
        self.data_provider = data_provider
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.store_info = self.ps_data_provider.get_ps_store_info(
            self.data_provider[Data.STORE_INFO])
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.scif = self.scif[self.scif['product_type'] != "Irrelevant"]
        self.result_values = self.ps_data_provider.get_result_values()
        self.templates = self.read_templates()
        self.common_db = common_db
        self.survey = Survey(self.data_provider, output=self.output, ps_data_provider=self.ps_data_provider,
                             common=self.common_db)
        self.manufacturer_fk = Const.MANUFACTURER_FK
        self.region = self.store_info['region_name'].iloc[0]
        self.store_type = self.store_info['store_type'].iloc[0]
        self.retailer = self.store_info['retailer_name'].iloc[0]
        self.branch = self.store_info['branch_name'].iloc[0]
        self.additional_attribute_4 = self.store_info['additional_attribute_4'].iloc[0]
        self.additional_attribute_7 = self.store_info['additional_attribute_7'].iloc[0]
        self.body_armor_delivered = self.get_body_armor_delivery_status()
        self.convert_base_size_and_multi_pack()

    def read_templates(self):
        templates = {}
        for sheet in Const.SHEETS:
            converters = None
            if sheet == Const.MINIMUM_FACINGS:
                converters = {Const.BASE_SIZE_MIN: self.convert_base_size_values,
                              Const.BASE_SIZE_MAX: self.convert_base_size_values}
            templates[sheet] = \
                pd.read_excel(Const.TEMPLATE_PATH, sheet_name=sheet,
                              converters=converters).fillna('')
        return templates

    # main functions:

    def main_calculation(self, *args, **kwargs):
        """
            This function gets all the scene results from the SceneKPI, after that calculates every session's KPI,
            and in the end it calls "filter results" to choose every KPI and scene and write the results in DB.
        """
        if self.region != 'Liberty':
            return
        red_score = 0
        main_template = self.templates[Const.KPIS]
        for i, main_line in main_template.iterrows():
            relevant_store_types = self.does_exist(main_line, Const.ADDITIONAL_ATTRIBUTE_7)
            if relevant_store_types and self.additional_attribute_7 not in relevant_store_types:
                continue
            result = self.calculate_main_kpi(main_line)
            if result:
                red_score += main_line[Const.WEIGHT] * result

        if len(self.common_db.kpi_results) > 0:
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(Const.RED_SCORE_PARENT)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=1, denominator_id=self.store_id, result=red_score,
                                              identifier_result=Const.RED_SCORE_PARENT, should_enter=True)
        return

    def calculate_main_kpi(self, main_line):
        """
        This function gets a line from the main_sheet, transfers it to the match function, and checks all of the
        KPIs in the same name in the match sheet.
        :param main_line: series from the template of the main_sheet.
        """
        relevant_scif = self.scif
        scene_types = self.does_exist(main_line, Const.SCENE_TYPE)
        if scene_types:
            relevant_scif = relevant_scif[relevant_scif['template_name'].isin(scene_types)]
        excluded_scene_types = self.does_exist(main_line, Const.EXCLUDED_SCENE_TYPE)
        if excluded_scene_types:
            relevant_scif = relevant_scif[~relevant_scif['template_name'].isin(
                excluded_scene_types)]
        template_groups = self.does_exist(main_line, Const.TEMPLATE_GROUP)
        if template_groups:
            relevant_scif = relevant_scif[relevant_scif['template_group'].isin(template_groups)]

        result = self.calculate_kpi_by_type(main_line, relevant_scif)

        return result

    def calculate_kpi_by_type(self, main_line, relevant_scif):
        """
        the function calculates all the kpis
        :param main_line: one kpi line from the main template
        :param relevant_scif:
        :return: boolean, but it can be None if we want not to write it in DB
        """
        kpi_type = main_line[Const.KPI_TYPE]
        relevant_template = self.templates[kpi_type]
        kpi_line = relevant_template[relevant_template[Const.KPI_NAME]
                                     == main_line[Const.KPI_NAME]].iloc[0]
        kpi_function = self.get_kpi_function(kpi_type)
        weight = main_line[Const.WEIGHT]

        if relevant_scif.empty:
            result = 0
        else:
            result = kpi_function(kpi_line, relevant_scif, weight)

        result_type_fk = self.ps_data_provider.get_pks_of_result(
            Const.PASS) if result > 0 else self.ps_data_provider.get_pks_of_result(Const.FAIL)

        if self.does_exist(main_line, Const.PARENT_KPI_NAME):
            # if this is a child KPI, we do not need to return a value to the Total Score KPI
            return 0
        else:  # normal behavior for when this isn't a child KPI
            kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(kpi_name)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                              denominator_id=self.store_id, denominator_result=0, weight=weight,
                                              result=result_type_fk, identifier_parent=Const.RED_SCORE_PARENT,
                                              identifier_result=kpi_name, should_enter=True)
            return result

    # SOS functions

    def calculate_sos(self, kpi_line, relevant_scif, weight):
        market_share_required = self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET)
        if market_share_required:
            market_share_target = self.get_market_share_target()
        else:
            market_share_target = 0

        if not market_share_target:
            market_share_target = 0

        denominator_facings = relevant_scif['facings'].sum()

        filtered_scif = relevant_scif.copy()

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        liberty_truck = self.does_exist(kpi_line, Const.LIBERTY_KEY_MANUFACTURER)
        if liberty_truck:
            liberty_truck_scif = relevant_scif[relevant_scif[Const.LIBERTY_KEY_MANUFACTURER].isin(
                liberty_truck)]
            filtered_scif = filtered_scif.append(liberty_truck_scif, sort=False).drop_duplicates()

        if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
            body_armor_scif = relevant_scif[relevant_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
            filtered_scif = filtered_scif.append(body_armor_scif, sort=False)

        numerator_facings = filtered_scif['facings'].sum()
        sos_value = numerator_facings / float(denominator_facings)
        result = 1 if sos_value > market_share_target else 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=numerator_facings,
                                          denominator_id=self.store_id, denominator_result=denominator_facings,
                                          weight=weight, score=result * weight,
                                          result=sos_value * 100, target=market_share_target * 100,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    # Availability functions
    def calculate_availability(self, kpi_line, relevant_scif, weight):
        survey_question_skus_required = self.does_exist(
            kpi_line, Const.SURVEY_QUESTION_SKUS_REQUIRED)
        if survey_question_skus_required:
            survey_question_skus, secondary_survey_question_skus = \
                self.get_relevant_product_assortment_by_kpi_name(kpi_line[Const.KPI_NAME])
            unique_skus = \
                relevant_scif[relevant_scif['product_fk'].isin(
                    survey_question_skus)]['product_fk'].unique().tolist()
            if secondary_survey_question_skus:
                secondary_unique_skus = \
                    relevant_scif[relevant_scif['product_fk'].isin(secondary_survey_question_skus)][
                        'product_fk'].unique().tolist()
            else:
                secondary_unique_skus = None

        else:
            secondary_unique_skus = None
            manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
            if manufacturer:
                relevant_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]
            brand = self.does_exist(kpi_line, Const.BRAND)
            if brand:
                relevant_scif = relevant_scif[relevant_scif['brand_name'].isin(brand)]
            category = self.does_exist(kpi_line, Const.CATEGORY)
            if category:
                relevant_scif = relevant_scif[relevant_scif['category'].isin(category)]
            excluded_brand = self.does_exist(kpi_line, Const.EXCLUDED_BRAND)
            if excluded_brand:
                relevant_scif = relevant_scif[~relevant_scif['brand_name'].isin(excluded_brand)]
            excluded_sku = self.does_exist(kpi_line, Const.EXCLUDED_SKU)
            if excluded_sku:
                relevant_scif = relevant_scif[~relevant_scif['product_name'].isin(excluded_sku)]
            unique_skus = relevant_scif['product_fk'].unique().tolist()

        length_of_unique_skus = len(unique_skus)
        minimum_number_of_skus = kpi_line[Const.MINIMUM_NUMBER_OF_SKUS]

        if length_of_unique_skus >= minimum_number_of_skus:
            if secondary_unique_skus:
                length_of_unique_skus = len(secondary_unique_skus)
                minimum_number_of_skus = kpi_line[Const.SECONDARY_MINIMUM_NUMBER_OF_SKUS]
                result = 1 if length_of_unique_skus > minimum_number_of_skus else 0
            else:
                result = 1
        else:
            result = 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                          denominator_id=self.store_id, denominator_result=0, weight=weight,
                                          result=length_of_unique_skus, target=minimum_number_of_skus,
                                          score=result * weight,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    def get_relevant_product_assortment_by_kpi_name(self, kpi_name):
        template = self.templates[Const.SURVEY_QUESTION_SKUS]
        relevant_template = template[template[Const.KPI_NAME] == kpi_name]
        # we need this to fix dumb template
        relevant_template[Const.EAN_CODE] = \
            relevant_template[Const.EAN_CODE].apply(lambda x: str(int(x)) if x != '' else None)
        primary_ean_codes = \
            relevant_template[relevant_template[Const.SECONDARY_GROUP]
                              != 'Y'][Const.EAN_CODE].unique().tolist()
        primary_ean_codes = [code for code in primary_ean_codes if code is not None]
        primary_products = self.all_products[self.all_products['product_ean_code'].isin(
            primary_ean_codes)]
        primary_product_pks = primary_products['product_fk'].unique().tolist()
        secondary_ean_codes = \
            relevant_template[relevant_template[Const.SECONDARY_GROUP]
                              == 'Y'][Const.EAN_CODE].unique().tolist()
        if secondary_ean_codes:
            secondary_products = self.all_products[self.all_products['product_ean_code'].isin(
                secondary_ean_codes)]
            secondary_product_pks = secondary_products['product_fk'].unique().tolist()
        else:
            secondary_product_pks = None
        return primary_product_pks, secondary_product_pks

    # Count of Display functions
    def calculate_count_of_display(self, kpi_line, relevant_scif, weight):
        filtered_scif = relevant_scif.copy()

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        liberty_truck = self.does_exist(kpi_line, Const.LIBERTY_KEY_MANUFACTURER)
        if liberty_truck:
            liberty_truck_scif = relevant_scif[relevant_scif[Const.LIBERTY_KEY_MANUFACTURER].isin(
                liberty_truck)]
            filtered_scif = filtered_scif.append(liberty_truck_scif, sort=False).drop_duplicates()

        brand = self.does_exist(kpi_line, Const.BRAND)
        if brand:
            filtered_scif = filtered_scif[filtered_scif['brand_name'].isin(brand)]

        category = self.does_exist(kpi_line, Const.CATEGORY)
        if category:
            filtered_scif = filtered_scif[filtered_scif['category'].isin(category)]

        excluded_brand = self.does_exist(kpi_line, Const.EXCLUDED_BRAND)
        if excluded_brand:
            filtered_scif = filtered_scif[~filtered_scif['brand_name'].isin(excluded_brand)]

        excluded_category = self.does_exist(kpi_line, Const.EXCLUDED_CATEGORY)
        if excluded_category:
            filtered_scif = filtered_scif[~filtered_scif['category'].isin(excluded_category)]

        ssd_still = self.does_exist(kpi_line, Const.ATT4)
        if ssd_still:
            filtered_scif = filtered_scif[filtered_scif['att4'].isin(ssd_still)]

        if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
            body_armor_scif = relevant_scif[relevant_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
            filtered_scif = filtered_scif.append(body_armor_scif, sort=False)

        size_subpackages = self.does_exist(kpi_line, Const.SIZE_SUBPACKAGES_NUM)
        if size_subpackages:
            # convert all pairings of size and number of subpackages to tuples
            # size_subpackages_tuples = [tuple([float(i) for i in x.split(';')]) for x in size_subpackages]
            size_subpackages_tuples = [tuple([self.convert_base_size_values(i) for i in x.split(';')]) for x in
                                       size_subpackages]
            filtered_scif = filtered_scif[pd.Series(list(zip(filtered_scif['Base Size'],
                                                             filtered_scif['Multi-Pack Size'])),
                                                    index=filtered_scif.index).isin(size_subpackages_tuples)]

        excluded_size_subpackages = self.does_exist(kpi_line, Const.EXCLUDED_SIZE_SUBPACKAGES_NUM)
        if excluded_size_subpackages:
            # convert all pairings of size and number of subpackages to tuples
            # size_subpackages_tuples = [tuple([float(i) for i in x.split(';')]) for x in size_subpackages]
            size_subpackages_tuples = [tuple([self.convert_base_size_values(i) for i in x.split(';')]) for x in
                                       excluded_size_subpackages]
            filtered_scif = filtered_scif[~pd.Series(list(zip(filtered_scif['Base Size'],
                                                              filtered_scif['Multi-Pack Size'])),
                                                     index=filtered_scif.index).isin(size_subpackages_tuples)]

        sub_packages = self.does_exist(kpi_line, Const.SUBPACKAGES_NUM)
        if sub_packages:
            if sub_packages == [Const.NOT_NULL]:
                filtered_scif = filtered_scif[~filtered_scif['Multi-Pack Size'].isnull()]
            elif sub_packages == [Const.GREATER_THAN_ONE]:
                filtered_scif = filtered_scif[filtered_scif['Multi-Pack Size'] > 1]
            else:
                filtered_scif = filtered_scif[filtered_scif['Multi-Pack Size'].isin(
                    [int(i) for i in sub_packages])]

        if self.does_exist(kpi_line, Const.MINIMUM_FACINGS_REQUIRED):
            number_of_passing_displays, _ = self.get_number_of_passing_displays(filtered_scif)

            if self.does_exist(kpi_line, Const.PARENT_KPI_NAME):
                parent_kpi_name = kpi_line[Const.PARENT_KPI_NAME] + Const.LIBERTY + Const.DRILLDOWN
                kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(
                    kpi_line[Const.KPI_NAME] + Const.LIBERTY)
                self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                                  denominator_id=self.store_id, denominator_result=0, weight=weight,
                                                  result=number_of_passing_displays,
                                                  score=number_of_passing_displays,
                                                  identifier_parent=parent_kpi_name, should_enter=True)
                return 0
            else:
                parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
                identifier_result = parent_kpi_name + Const.DRILLDOWN
                kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
                self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                                  denominator_id=self.store_id, denominator_result=0, weight=weight,
                                                  result=number_of_passing_displays,
                                                  score=number_of_passing_displays * weight,
                                                  identifier_parent=parent_kpi_name,
                                                  identifier_result=identifier_result, should_enter=True)
                return number_of_passing_displays
        else:
            return 0

    # Share of Display functions
    def calculate_share_of_display(self, kpi_line, relevant_scif, weight):
        base_scif = relevant_scif.copy()

        ssd_still = self.does_exist(kpi_line, Const.ATT4)
        if ssd_still:
            ssd_still_scif = base_scif[base_scif['att4'].isin(ssd_still)]
        else:
            ssd_still_scif = base_scif

        denominator_passing_displays, _ = \
            self.get_number_of_passing_displays(ssd_still_scif)

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = ssd_still_scif[ssd_still_scif['manufacturer_name'].isin(manufacturer)]
        else:
            filtered_scif = ssd_still_scif

        liberty_truck = self.does_exist(kpi_line, Const.LIBERTY_KEY_MANUFACTURER)
        if liberty_truck:
            liberty_truck_scif = ssd_still_scif[ssd_still_scif[Const.LIBERTY_KEY_MANUFACTURER].isin(
                liberty_truck)]
            filtered_scif = filtered_scif.append(liberty_truck_scif, sort=False).drop_duplicates()

        if self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET):
            market_share_target = self.get_market_share_target(ssd_still=ssd_still)
        else:
            market_share_target = 0

        if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
            body_armor_scif = relevant_scif[relevant_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
            filtered_scif = filtered_scif.append(body_armor_scif, sort=False)

        if self.does_exist(kpi_line, Const.MINIMUM_FACINGS_REQUIRED):
            numerator_passing_displays, _ = \
                self.get_number_of_passing_displays(filtered_scif)

            if denominator_passing_displays != 0:
                share_of_displays = \
                    numerator_passing_displays / float(denominator_passing_displays)
            else:
                share_of_displays = 0

            result = 1 if share_of_displays > market_share_target else 0

            parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk,
                                              numerator_result=numerator_passing_displays,
                                              denominator_id=self.store_id,
                                              denominator_result=denominator_passing_displays, weight=weight,
                                              result=share_of_displays * 100, target=market_share_target * 100,
                                              score=result * weight,
                                              identifier_parent=parent_kpi_name, should_enter=True)

            return result
        else:
            return 0

    def get_number_of_passing_displays(self, filtered_scif):
        if filtered_scif.empty:
            return 0, 0

        filtered_scif = \
            filtered_scif.groupby(['Base Size', 'Multi-Pack Size', 'scene_id'],
                                  as_index=False)['facings'].sum()

        filtered_scif['passed_displays'] = \
            filtered_scif.apply(lambda row: self._calculate_pass_status_of_display(row), axis=1)

        number_of_displays = filtered_scif['passed_displays'].sum()
        facings_of_displays = filtered_scif[filtered_scif['passed_displays'] == 1]['facings'].sum()

        return number_of_displays, facings_of_displays

    def _calculate_pass_status_of_display(self, row):  # need to move to external KPI targets
        template = self.templates[Const.MINIMUM_FACINGS]
        relevant_template = template[(template[Const.BASE_SIZE_MIN] <= row['Base Size']) &
                                     (template[Const.BASE_SIZE_MAX] >= row['Base Size']) &
                                     (template[Const.MULTI_PACK_SIZE] == row['Multi-Pack Size'])]
        if relevant_template.empty:
            return 0
        minimum_facings = relevant_template[Const.MINIMUM_FACINGS_REQUIRED_FOR_DISPLAY].min()
        return 1 if row['facings'] >= minimum_facings else 0

    # Share of Cooler functions
    def calculate_share_of_coolers(self, kpi_line, relevant_scif, weight):
        scene_ids = relevant_scif['scene_id'].unique().tolist()

        total_coolers = len(scene_ids)
        if total_coolers == 0:
            return 0

        passing_coolers = 0

        if self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET):
            market_share_target = self.get_market_share_target()
        else:
            market_share_target = 0

        for scene_id in scene_ids:
            cooler_scif = relevant_scif[relevant_scif['scene_id'] == scene_id]

            filtered_scif = cooler_scif.copy()

            manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
            if manufacturer:
                filtered_scif = cooler_scif[cooler_scif['manufacturer_name'].isin(manufacturer)]

            liberty_truck = self.does_exist(kpi_line, Const.LIBERTY_KEY_MANUFACTURER)
            if liberty_truck:
                liberty_truck_scif = cooler_scif[cooler_scif[Const.LIBERTY_KEY_MANUFACTURER].isin(
                    liberty_truck)]
                filtered_scif = filtered_scif.append(
                    liberty_truck_scif, sort=False).drop_duplicates()

            if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
                body_armor_scif = cooler_scif[cooler_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
                filtered_scif = filtered_scif.append(body_armor_scif, sort=False).drop_duplicates()

            coke_facings_threshold = self.does_exist(kpi_line, Const.COKE_FACINGS_THRESHOLD)
            cooler_sos = filtered_scif['facings'].sum() / cooler_scif['facings'].sum()
            cooler_result = 1 if cooler_sos >= coke_facings_threshold else 0

            passing_coolers += cooler_result

        coke_market_share = passing_coolers / float(total_coolers)
        result = 1 if coke_market_share > market_share_target else 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk,
                                          numerator_result=passing_coolers,
                                          denominator_id=self.store_id,
                                          denominator_result=total_coolers, weight=weight,
                                          result=coke_market_share * 100, target=market_share_target * 100,
                                          score=result * weight,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    # Survey functions
    def calculate_survey(self, kpi_line, relevant_scif, weight):
        return 1 if self.survey.check_survey_answer(kpi_line[Const.QUESTION_TEXT], 'Yes') else 0

    # helper functions
    def convert_base_size_and_multi_pack(self):
        self.scif.loc[:, 'Base Size'] = self.scif['Base Size'].apply(self.convert_base_size_values)
        self.scif.loc[:, 'Multi-Pack Size'] = \
            self.scif['Multi-Pack Size'].apply(lambda x: int(x) if x is not None else None)

    @staticmethod
    def convert_base_size_values(value):
        try:
            new_value = float(value.split()[0]) if value not in [None, ''] else None
        except IndexError:
            Log.error('Could not convert base size value for {}'.format(value))
            new_value = None
        return new_value

    def get_market_share_target(self, ssd_still=None):  # need to move to external KPI targets
        template = self.templates[Const.MARKET_SHARE]
        relevant_template = template[(template[Const.ADDITIONAL_ATTRIBUTE_4] == self.additional_attribute_4) &
                                     (template[Const.RETAILER] == self.retailer) &
                                     (template[Const.BRANCH] == self.branch)]

        if relevant_template.empty:
            if ssd_still:
                if ssd_still[0].lower() == Const.SSD.lower():
                    return 49
                elif ssd_still[0].lower() == Const.STILL.lower():
                    return 16
                else:
                    return 0
            else:
                return 26

        if ssd_still:
            if ssd_still[0].lower() == Const.SSD.lower():
                return relevant_template[Const.SSD].iloc[0]
            elif ssd_still[0].lower() == Const.STILL.lower():
                return relevant_template[Const.STILL].iloc[0]

        # total 26, ssd only 49, still only 16
        return relevant_template[Const.SSD_AND_STILL].iloc[0]

    def get_body_armor_delivery_status(self):
        if self.store_info['additional_attribute_8'].iloc[0] == 'Y':
            return True
        else:
            return False

    def get_kpi_function(self, kpi_type):
        """
        transfers every kpi to its own function
        :param kpi_type: value from "sheet" column in the main sheet
        :return: function
        """
        if kpi_type == Const.SOS:
            return self.calculate_sos
        elif kpi_type == Const.AVAILABILITY:
            return self.calculate_availability
        elif kpi_type == Const.COUNT_OF_DISPLAY:
            return self.calculate_count_of_display
        elif kpi_type == Const.SHARE_OF_DISPLAY:
            return self.calculate_share_of_display
        elif kpi_type == Const.SHARE_OF_COOLERS:
            return self.calculate_share_of_coolers
        elif kpi_type == Const.SURVEY:
            return self.calculate_survey
        else:
            Log.warning(
                "The value '{}' in column sheet in the template is not recognized".format(kpi_type))
            return None

    @staticmethod
    def does_exist(kpi_line, column_name):
        """
        checks if kpi_line has values in this column, and if it does - returns a list of these values
        :param kpi_line: line from template
        :param column_name: str
        :return: list of values if there are, otherwise None
        """
        if column_name in kpi_line.keys() and kpi_line[column_name] != "":
            cell = kpi_line[column_name]
            if type(cell) in [int, float, np.float64]:
                return [cell]
            elif type(cell) in [unicode, str]:
                return [x.strip() for x in cell.split(",")]
        return None
Пример #5
0
class LIBERTYToolBox:

    def __init__(self, data_provider, output, common_db):
        self.output = output
        self.data_provider = data_provider
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.store_info = self.ps_data_provider.get_ps_store_info(self.data_provider[Data.STORE_INFO])
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.scif = self.scif[self.scif['product_type'] != "Irrelevant"]
        self.templates = {}
        self.result_values = self.ps_data_provider.get_result_values()
        for sheet in Const.SHEETS:
            self.templates[sheet] = pd.read_excel(Const.TEMPLATE_PATH, sheetname=sheet).fillna('')
        self.common_db = common_db
        self.survey = Survey(self.data_provider, output=self.output, ps_data_provider=self.ps_data_provider,
                             common=self.common_db)
        self.manufacturer_fk = Const.MANUFACTURER_FK
        self.region = self.store_info['region_name'].iloc[0]
        self.store_type = self.store_info['store_type'].iloc[0]
        self.retailer = self.store_info['retailer_name'].iloc[0]
        self.branch = self.store_info['branch_name'].iloc[0]
        self.additional_attribute_4 = self.store_info['additional_attribute_4'].iloc[0]
        self.additional_attribute_7 = self.store_info['additional_attribute_7'].iloc[0]
        self.body_armor_delivered = self.get_body_armor_delivery_status()

    # main functions:

    def main_calculation(self, *args, **kwargs):
        """
            This function gets all the scene results from the SceneKPI, after that calculates every session's KPI,
            and in the end it calls "filter results" to choose every KPI and scene and write the results in DB.
        """
        red_score = 0
        main_template = self.templates[Const.KPIS]
        for i, main_line in main_template.iterrows():
            relevant_store_types = self.does_exist(main_line, Const.ADDITIONAL_ATTRIBUTE_7)
            if relevant_store_types and self.additional_attribute_7 not in relevant_store_types:
                continue
            result = self.calculate_main_kpi(main_line)
            if result:
                red_score += main_line[Const.WEIGHT]

        if len(self.common_db.kpi_results) > 0:
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(Const.RED_SCORE_PARENT)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=1, denominator_id=self.store_id, result=red_score,
                                              identifier_result=Const.RED_SCORE_PARENT, should_enter=True)
        return

    def calculate_main_kpi(self, main_line):
        """
        This function gets a line from the main_sheet, transfers it to the match function, and checks all of the
        KPIs in the same name in the match sheet.
        :param main_line: series from the template of the main_sheet.
        """
        relevant_scif = self.scif
        scene_types = self.does_exist(main_line, Const.SCENE_TYPE)
        if scene_types:
            relevant_scif = relevant_scif[relevant_scif['template_name'].isin(scene_types)]
        excluded_scene_types = self.does_exist(main_line, Const.EXCLUDED_SCENE_TYPE)
        if excluded_scene_types:
            relevant_scif = relevant_scif[~relevant_scif['template_name'].isin(excluded_scene_types)]
        template_groups = self.does_exist(main_line, Const.TEMPLATE_GROUP)
        if template_groups:
            relevant_scif = relevant_scif[relevant_scif['template_group'].isin(template_groups)]

        result = self.calculate_kpi_by_type(main_line, relevant_scif)

        return result

    def calculate_kpi_by_type(self, main_line, relevant_scif):
        """
        the function calculates all the kpis
        :param main_line: one kpi line from the main template
        :param relevant_scif:
        :return: boolean, but it can be None if we want not to write it in DB
        """
        kpi_type = main_line[Const.KPI_TYPE]
        relevant_template = self.templates[kpi_type]
        kpi_line = relevant_template[relevant_template[Const.KPI_NAME] == main_line[Const.KPI_NAME]].iloc[0]
        kpi_function = self.get_kpi_function(kpi_type)
        weight = main_line[Const.WEIGHT]

        if relevant_scif.empty:
            result = 0
        else:
            result = kpi_function(kpi_line, relevant_scif, weight)

        result_type_fk = self.ps_data_provider.get_pks_of_result(
            Const.PASS) if result > 0 else self.ps_data_provider.get_pks_of_result(Const.FAIL)

        kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(kpi_name)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                          denominator_id=self.store_id, denominator_result=0, weight=weight,
                                          result=result_type_fk, identifier_parent=Const.RED_SCORE_PARENT,
                                          identifier_result=kpi_name, should_enter=True)

        return result

    # SOS functions
    def calculate_sos(self, kpi_line, relevant_scif, weight):
        market_share_required = self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET)
        if market_share_required:
            market_share_target = self.get_market_share_target()
        else:
            market_share_target = 0

        if not market_share_target:
            market_share_target = 0

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            relevant_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        number_of_facings = relevant_scif['facings'].sum()
        result = 1 if number_of_facings > market_share_target else 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                          denominator_id=self.store_id, denominator_result=0, weight=weight,
                                          result=number_of_facings, target=market_share_target,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    # Availability functions
    def calculate_availability(self, kpi_line, relevant_scif, weight):
        survey_question_skus_required = self.does_exist(kpi_line, Const.SURVEY_QUESTION_SKUS_REQUIRED)
        if survey_question_skus_required:
            survey_question_skus = self.get_relevant_product_assortment_by_kpi_name(kpi_line[Const.KPI_NAME])
            unique_skus = \
                relevant_scif[relevant_scif['product_fk'].isin(survey_question_skus)]['product_fk'].unique().tolist()
        else:
            manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
            if manufacturer:
                relevant_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]
            brand = self.does_exist(kpi_line, Const.BRAND)
            if brand:
                relevant_scif = relevant_scif[relevant_scif['brand_name'].isin(brand)]
            category = self.does_exist(kpi_line, Const.CATEGORY)
            if category:
                relevant_scif = relevant_scif[relevant_scif['category'].isin(category)]
            excluded_brand = self.does_exist(kpi_line, Const.EXCLUDED_BRAND)
            if excluded_brand:
                relevant_scif = relevant_scif[~relevant_scif['brand_name'].isin(excluded_brand)]
            unique_skus = relevant_scif['product_fk'].unique().tolist()

        length_of_unique_skus = len(unique_skus)
        minimum_number_of_skus = kpi_line[Const.MINIMUM_NUMBER_OF_SKUS]

        result = 1 if length_of_unique_skus >= minimum_number_of_skus else 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                          denominator_id=self.store_id, denominator_result=0, weight=weight,
                                          result=length_of_unique_skus, target=minimum_number_of_skus,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    def get_relevant_product_assortment_by_kpi_name(self, kpi_name):
        template = self.templates[Const.SURVEY_QUESTION_SKUS]
        relevant_template = template[template[Const.KPI_NAME] == kpi_name]
        relevant_ean_codes = relevant_template[Const.EAN_CODE].unique().tolist()
        relevant_ean_codes = [str(int(x)) for x in relevant_ean_codes if x != '']  # we need this to fix dumb template
        relevant_products = self.all_products[self.all_products['product_ean_code'].isin(relevant_ean_codes)]
        return relevant_products['product_fk'].unique().tolist()

    # Count of Display functions
    def calculate_count_of_display(self, kpi_line, relevant_scif, weight):
        filtered_scif = relevant_scif

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        brand = self.does_exist(kpi_line, Const.BRAND)
        if brand:
            filtered_scif = filtered_scif[filtered_scif['brand_name'].isin(brand)]

        ssd_still = self.does_exist(kpi_line, Const.ATT4)
        if ssd_still:
            filtered_scif = filtered_scif[filtered_scif['att4'].isin(ssd_still)]

        size_subpackages = self.does_exist(kpi_line, Const.SIZE_SUBPACKAGES_NUM)
        if size_subpackages:
            # convert all pairings of size and number of subpackages to tuples
            size_subpackages_tuples = [tuple([float(i) for i in x.split(';')]) for x in size_subpackages]
            filtered_scif = filtered_scif[pd.Series(list(zip(filtered_scif['size'],
                                                             filtered_scif['number_of_sub_packages'])),
                                                    index=filtered_scif.index).isin(size_subpackages_tuples)]

        sub_packages = self.does_exist(kpi_line, Const.SUBPACKAGES_NUM)
        if sub_packages:
            if sub_packages == [Const.NOT_NULL]:
                filtered_scif = filtered_scif[~filtered_scif['number_of_sub_packages'].isnull()]
            else:
                filtered_scif = filtered_scif[filtered_scif['number_of_sub_packages'].isin([int(i) for i in sub_packages])]

        if self.does_exist(kpi_line, Const.MINIMUM_FACINGS_REQUIRED):
            number_of_passing_displays = self.get_number_of_passing_displays(filtered_scif)

            parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                              denominator_id=self.store_id, denominator_result=0, weight=weight,
                                              result=number_of_passing_displays,
                                              identifier_parent=parent_kpi_name, should_enter=True)
            return 1 if number_of_passing_displays > 0 else 0
        else:
            return 0

    # Share of Display functions
    def calculate_share_of_display(self, kpi_line, relevant_scif, weight):
        filtered_scif = relevant_scif

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        ssd_still = self.does_exist(kpi_line, Const.ATT4)
        if ssd_still:
            filtered_scif = filtered_scif[filtered_scif['att4'].isin(ssd_still)]

        if self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET):
            market_share_target = self.get_market_share_target(ssd_still=ssd_still)
        else:
            market_share_target = 0

        if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
            body_armor_scif = relevant_scif[relevant_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
            filtered_scif = filtered_scif.append(body_armor_scif, sort=False)

        if self.does_exist(kpi_line, Const.MINIMUM_FACINGS_REQUIRED):
            number_of_passing_displays = self.get_number_of_passing_displays(filtered_scif)

            result = 1 if number_of_passing_displays > market_share_target else 0

            parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                              denominator_id=self.store_id, denominator_result=0, weight=weight,
                                              result=number_of_passing_displays, target=market_share_target,
                                              identifier_parent=parent_kpi_name, should_enter=True)

            return result
        else:
            return 0

    def get_number_of_passing_displays(self, filtered_scif):
        if filtered_scif.empty:
            return 0

        filtered_scif['passed_displays'] = \
            filtered_scif.apply(lambda row: self._calculate_pass_status_of_display(row), axis=1)

        return filtered_scif['passed_displays'].sum()

    def _calculate_pass_status_of_display(self, row):  # need to move to external KPI targets
        template = self.templates[Const.MINIMUM_FACINGS]
        package_category = (row['size'], row['number_of_sub_packages'], row['size_unit'])
        relevant_template = template[pd.Series(zip(template['size'],
                                                   template['subpackages_num'],
                                                   template['unit_of_measure'])) == package_category]
        minimum_facings = relevant_template[Const.MINIMUM_FACINGS_REQUIRED_FOR_DISPLAY].min()
        return 1 if row['facings'] > minimum_facings else 0

    # Survey functions
    def calculate_survey(self, kpi_line, relevant_scif, weight):
        return 1 if self.survey.check_survey_answer(kpi_line[Const.QUESTION_TEXT], 'Yes') else 0

    # helper functions
    def get_market_share_target(self, ssd_still=None):  # need to move to external KPI targets
        template = self.templates[Const.MARKET_SHARE]
        relevant_template = template[(template[Const.ADDITIONAL_ATTRIBUTE_4] == self.additional_attribute_4) &
                                     (template[Const.RETAILER] == self.retailer) &
                                     (template[Const.BRANCH] == self.branch)]

        if relevant_template.empty:
            if ssd_still:
                if ssd_still[0].lower() == Const.SSD.lower():
                    return 49
                elif ssd_still[0].lower() == Const.STILL.lower():
                    return 16
                else:
                    return 0
            else:
                return 26

        if ssd_still:
            if ssd_still[0].lower() == Const.SSD.lower():
                return relevant_template[Const.SSD].iloc[0]
            elif ssd_still[0].lower() == Const.STILL.lower():
                return relevant_template[Const.STILL].iloc[0]

        # total 26, ssd only 49, still only 16
        return relevant_template[Const.SSD_AND_STILL].iloc[0]

    def get_body_armor_delivery_status(self):
        if self.store_info['additional_attribute_8'].iloc[0] == 'Y':
            return True
        else:
            return False

    def get_kpi_function(self, kpi_type):
        """
        transfers every kpi to its own function
        :param kpi_type: value from "sheet" column in the main sheet
        :return: function
        """
        if kpi_type == Const.SOS:
            return self.calculate_sos
        elif kpi_type == Const.AVAILABILITY:
            return self.calculate_availability
        elif kpi_type == Const.COUNT_OF_DISPLAY:
            return self.calculate_count_of_display
        elif kpi_type == Const.SHARE_OF_DISPLAY:
            return self.calculate_share_of_display
        elif kpi_type == Const.SURVEY:
            return self.calculate_survey
        else:
            Log.warning(
                "The value '{}' in column sheet in the template is not recognized".format(kpi_type))
            return None

    @staticmethod
    def does_exist(kpi_line, column_name):
        """
        checks if kpi_line has values in this column, and if it does - returns a list of these values
        :param kpi_line: line from template
        :param column_name: str
        :return: list of values if there are, otherwise None
        """
        if column_name in kpi_line.keys() and kpi_line[column_name] != "":
            cell = kpi_line[column_name]
            if type(cell) in [int, float]:
                return [cell]
            elif type(cell) in [unicode, str]:
                return [x.strip() for x in cell.split(",")]
        return None
class FunctionsToolBox:
    def __init__(self, data_provider, output, templates, store_attr):
        self.output = output
        self.data_provider = data_provider
        self.project_name = self.data_provider.project_name
        self.survey = Survey(self.data_provider, self.output)
        self.templates = templates
        if Const.CONVERTERS in self.templates:
            self.converters = self.templates[Const.CONVERTERS]
            self.exclusion_sheet = self.templates[Const.SKU_EXCLUSION]
        self.store_attr = store_attr

    # survey:

    def calculate_survey_specific(self,
                                  kpi_line,
                                  relevant_scif=None,
                                  isnt_dp=None):
        """
        returns a survey line if True or False
        :param kpi_line: line from the survey sheet
        :param relevant_scif: unused, for the general function
        :param isnt_dp: unused, for the general function
        :return: True or False - if the question gets the needed answer
        """
        question = kpi_line[Const.Q_TEXT]
        if not question:
            question_id = kpi_line[Const.Q_ID]
            if question_id == "":
                Log.warning(
                    "The template has a survey question without ID or text")
                return False
            question = ('question_fk', int(question_id))
        answers = kpi_line[Const.ACCEPTED_ANSWER].split(',')
        min_answer = None if kpi_line[Const.REQUIRED_ANSWER] == '' else True
        for answer in answers:
            if self.survey.check_survey_answer(survey_text=question,
                                               target_answer=answer,
                                               min_required_answer=min_answer):
                return True
        return False

    # pointer the other kpi_functions:

    def calculate_kpi_by_type(self, main_line, filtered_scif):
        """
        the function calculates all the kpis
        :param main_line: one kpi line from the main template
        :param filtered_scif:
        :return: boolean, but it can be None if we want not to write it in DB
        """
        kpi_type = main_line[Const.SHEET]
        relevant_template = self.templates[kpi_type]
        relevant_template = relevant_template[relevant_template[Const.KPI_NAME]
                                              == main_line[Const.KPI_NAME]]
        target = len(relevant_template) if main_line[
            Const.GROUP_TARGET] == Const.ALL else main_line[Const.GROUP_TARGET]
        isnt_dp = True if self.store_attr != Const.DP and main_line[
            Const.STORE_ATTRIBUTE] == Const.DP else False
        if main_line[Const.SAME_PACK] == Const.V:
            filtered_scif = filtered_scif.fillna("NAN")
            # only items categorized as SSD should be evaluated in this calculation; see PROS-6342
            filtered_scif = filtered_scif[filtered_scif['att4'] == 'SSD']
            if filtered_scif.empty:
                return False
            sizes = filtered_scif['size'].tolist()
            sub_packages_nums = filtered_scif['number_of_sub_packages'].tolist(
            )
            packages = set(zip(sizes, sub_packages_nums))
            for package in packages:
                filtered_scif = filtered_scif[
                    (filtered_scif['size'] == package[0])
                    & (filtered_scif['number_of_sub_packages'] == package[1])]
                result = self.calculate_specific_kpi(
                    relevant_template, filtered_scif, isnt_dp, target,
                    self.calculate_availability)
                if result is False:
                    return result
            return True
        kpi_function = self.get_kpi_function(kpi_type)
        return self.calculate_specific_kpi(relevant_template, filtered_scif,
                                           isnt_dp, target, kpi_function)

    @staticmethod
    def calculate_specific_kpi(relevant_template, filtered_scif, isnt_dp,
                               target, kpi_function):
        """
        checks if the passed lines are more than target
        :param relevant_template: specific template filtered with the specific kpi lines
        :param filtered_scif:
        :param isnt_dp: the main_line has "DP" flag and the store_attr is not DP
        :param target: integer
        :param kpi_function: specific function for the calculation
        :return: boolean, but it can be None if we want not to write it in DB
        """
        passed_counter = 0
        for i, kpi_line in relevant_template.iterrows():
            answer = kpi_function(kpi_line, filtered_scif, isnt_dp)
            if answer:
                passed_counter += 1
            elif answer is None:
                return None
        return passed_counter >= target

    # availability:

    def calculate_availability(self, kpi_line, relevant_scif, isnt_dp):  # V
        """
        checks if all the lines in the availability sheet passes the KPI (there is at least one product
        in this relevant scif that has the attributes).
        :param relevant_scif: filtered scif
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we shouldn't calculate
        DP lines
        :param kpi_line: line from the availability sheet
        :return: boolean
        """
        if isnt_dp and kpi_line[Const.MANUFACTURER] in Const.DP_MANU:
            return True
        filtered_scif = self.filter_scif_availability(kpi_line, relevant_scif)
        target = kpi_line[Const.TARGET]
        return filtered_scif[
            filtered_scif['facings'] > 0]['facings'].count() >= target

    def filter_scif_availability(self, kpi_line, relevant_scif):  # V
        """
        calls filter_scif_specific for every column in the template of availability
        :param kpi_line:
        :param relevant_scif:
        :return:
        """
        names_of_columns = {
            Const.MANUFACTURER: "manufacturer_name",
            Const.BRAND: "brand_name",
            Const.TRADEMARK: "att2",
            Const.SIZE: "size",
            Const.NUM_SUB_PACKAGES: "number_of_sub_packages",
            Const.PREMIUM_SSD: "Premium SSD",
            Const.INNOVATION_BRAND: "Innovation Brand",
        }
        for name in names_of_columns:
            relevant_scif = self.filter_scif_specific(relevant_scif, kpi_line,
                                                      name,
                                                      names_of_columns[name])
        return relevant_scif

    def filter_scif_specific(self, relevant_scif, kpi_line, name_in_template,
                             name_in_scif):  # V
        """
        takes scif and filters it from the template
        :param relevant_scif: the current filtered scif
        :param kpi_line: line from one sheet (availability for example)
        :param name_in_template: the column name in the template
        :param name_in_scif: the column name in SCIF
        :return:
        """
        values = self.does_exist(kpi_line, name_in_template)
        if values:
            if name_in_scif in Const.NUMERIC_VALUES_TYPES:
                values = [float(x) for x in values]
            return relevant_scif[relevant_scif[name_in_scif].isin(values)]
        return relevant_scif

    # SOS:

    def calculate_sos(self, kpi_line, relevant_scif, isnt_dp):
        """
        calculates SOS line in the relevant scif.
        :param kpi_line: line from SOS sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out of the numerator.
        :return: boolean
        """
        relevant_scif = self.sos_first_filtering(kpi_line, relevant_scif)
        if kpi_line[Const.SSD_STILL] != "":
            relevant_scif = self.filter_by_type_value(
                relevant_scif, Const.SSD_STILL, kpi_line[Const.SSD_STILL])
        num_type = kpi_line[Const.NUM_TYPES_1]
        num_value = kpi_line[Const.NUM_VALUES_1]
        num_scif = self.filter_by_type_value(relevant_scif, num_type,
                                             num_value)
        if isnt_dp:
            num_scif = num_scif[~(
                num_scif['manufacturer_name'].isin(Const.DP_MANU))]
        target = float(kpi_line[Const.TARGET]) / 100
        percentage = num_scif['facings'].sum() / relevant_scif['facings'].sum() if relevant_scif['facings'].sum() > 0 \
            else 0
        return percentage >= target

    def sos_first_filtering(self, kpi_line, relevant_scif):
        """
        The common part of both SOS functions, first filtering
        :param kpi_line: line from the SOS/SOS_maj template
        :param relevant_scif: filtered scif
        :return: new filtered scif
        """
        kpi_name = kpi_line[Const.KPI_NAME]
        if kpi_line[Const.EXCLUSION_SHEET] == Const.V:
            relevant_scif = self.exclude_from_scif(kpi_name, relevant_scif)
        relevant_scif = relevant_scif[relevant_scif['product_type'] != "Empty"]
        den_type = kpi_line[Const.DEN_TYPES_1]
        den_value = kpi_line[Const.DEN_VALUES_1]
        relevant_scif = self.filter_by_type_value(relevant_scif, den_type,
                                                  den_value)
        return relevant_scif

    # SOS majority:

    def calculate_sos_maj(self, kpi_line, relevant_scif, isnt_dp):
        """
        calculates SOS majority line in the relevant scif. Filters the denominator and sends the line to the
        match function (majority or dominant)
        :param kpi_line: line from SOS majority sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out of the numerator (and the denominator of the dominant part).
        :return: boolean
        """
        relevant_scif = self.sos_first_filtering(kpi_line, relevant_scif)
        if kpi_line[Const.EXCLUSION_SHEET] == Const.V:
            kpi_name = kpi_line[Const.KPI_NAME]
            relevant_scif = self.exclude_from_scif(kpi_name, relevant_scif)
        den_type = kpi_line[Const.DEN_TYPES_2]
        den_value = kpi_line[Const.DEN_VALUES_2]
        relevant_scif = self.filter_by_type_value(relevant_scif, den_type,
                                                  den_value)
        if kpi_line[Const.MAJ_DOM] == Const.MAJOR:
            answer = self.calculate_majority_part(kpi_line, relevant_scif,
                                                  isnt_dp)
        elif kpi_line[Const.MAJ_DOM] == Const.DOMINANT:
            answer = self.calculate_dominant_part(kpi_line, relevant_scif,
                                                  isnt_dp)
        else:
            Log.warning("SOS majority does not know '{}' part".format(
                kpi_line[Const.MAJ_DOM]))
            answer = False
        return answer

    def calculate_majority_part(self, kpi_line, relevant_scif, isnt_dp):
        """
        filters the numerator and checks if the SOS is bigger than 50%.
        :param kpi_line: line from SOS majority sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out of the numerator.
        :return: boolean
        """
        num_type = kpi_line[Const.NUM_TYPES_1]
        num_value = kpi_line[Const.NUM_VALUES_1]
        num_scif = self.filter_by_type_value(relevant_scif, num_type,
                                             num_value)
        num_type = kpi_line[Const.NUM_TYPES_2]
        num_value = kpi_line[Const.NUM_VALUES_2]
        num_scif = self.filter_by_type_value(num_scif, num_type, num_value)
        if num_scif.empty:
            return None
        if isnt_dp:
            num_scif = num_scif[~(
                num_scif['manufacturer_name'].isin(Const.DP_MANU))]
        target = Const.MAJORITY_TARGET
        return num_scif['facings'].sum() / relevant_scif['facings'].sum(
        ) >= target

    def calculate_dominant_part(self, kpi_line, relevant_scif, isnt_dp):
        """
        filters the numerator and checks if the given value in the given type is the one with the most facings.
        :param kpi_line: line from SOS majority sheet.
        :param relevant_scif: filtered scif.
        :param isnt_dp: if "store attribute" in the main sheet has DP, and the store is not DP, we should filter
        all the DP products out.
        :return: boolean
        """
        type_name = self.get_column_name(kpi_line[Const.NUM_TYPES_1],
                                         relevant_scif)
        values = str(kpi_line[Const.NUM_VALUES_1]).split(', ')
        if isnt_dp:
            relevant_scif = relevant_scif[~(
                relevant_scif['manufacturer_name'].isin(Const.DP_MANU))]
            if kpi_line[Const.ADD_IF_NOT_DP] != "":
                values_to_add = str(kpi_line[Const.ADD_IF_NOT_DP]).split(', ')
                values = values + values_to_add
        if type_name in Const.NUMERIC_VALUES_TYPES:
            values = [float(x) for x in values]
        max_facings, needed_one = 0, 0
        values_type = relevant_scif[type_name].unique().tolist()
        if None in values_type:
            values_type.remove(None)
            current_sum = relevant_scif[
                relevant_scif[type_name].isnull()]['facings'].sum()
            if current_sum > max_facings:
                max_facings = current_sum
        for value in values_type:
            current_sum = relevant_scif[relevant_scif[type_name] ==
                                        value]['facings'].sum()
            if current_sum > max_facings:
                max_facings = current_sum
            if value in values:
                needed_one += current_sum
        return needed_one >= max_facings

    # helpers:

    def get_column_name(self, field_name, df):
        """
        checks what the real field name in DttFrame is (if it exists in the DF or exists in the "converter" sheet).
        :param field_name: str
        :param df: scif/products
        :return: real column name (if exists)
        """
        if field_name in df.columns:
            return field_name
        if field_name.upper() in self.converters[
                Const.NAME_IN_TEMP].str.upper().tolist():
            field_name = self.converters[self.converters[
                Const.NAME_IN_TEMP].str.upper() == field_name.upper()][
                    Const.NAME_IN_DB].iloc[0]
            return field_name
        return None

    def filter_by_type_value(self, relevant_scif, type_name, value):
        """
        filters scif with the type and value
        :param relevant_scif: current filtered scif
        :param type_name: str (from the template)
        :param value: str
        :return: new scif
        """
        if type_name == "":
            return relevant_scif
        values = value.split(', ')
        new_type_name = self.get_column_name(type_name, relevant_scif)
        if not new_type_name:
            print "There is no field '{}'".format(type_name)
            return relevant_scif
        if new_type_name in Const.NUMERIC_VALUES_TYPES:
            values = [float(x) for x in values]
        return relevant_scif[relevant_scif[new_type_name].isin(values)]

    @staticmethod
    def exclude_scif(exclude_line, relevant_scif):
        """
        filters products out of the scif
        :param exclude_line: line from the exclusion sheet
        :param relevant_scif: current filtered scif
        :return: new scif
        """
        if exclude_line[Const.PRODUCT_EAN] != "":
            exclude_products = exclude_line[Const.PRODUCT_EAN].split(', ')
            relevant_scif = relevant_scif[~(
                relevant_scif['product_ean_code'].isin(exclude_products))]
        if exclude_line[Const.BRAND] != "":
            exclude_brands = exclude_line[Const.BRAND].split(', ')
            relevant_scif = relevant_scif[~(
                relevant_scif['brand_name'].isin(exclude_brands))]
        return relevant_scif

    def exclude_from_scif(self, kpi_name, relevant_scif):
        """
        excludes all the necessary types from the scif
        :param kpi_name:
        :param relevant_scif:
        :return: filtered relevant_scif
        """
        relevant_exclusions = self.exclusion_sheet[self.exclusion_sheet[
            Const.KPI_NAME] == kpi_name]
        for i, exc_line in relevant_exclusions.iterrows():
            relevant_scif = self.exclude_scif(exc_line, relevant_scif)
        return relevant_scif

    @staticmethod
    def does_exist(kpi_line, column_name):
        """
        checks if kpi_line has values in this column, and if it does - returns a list of these values
        :param kpi_line: line from template
        :param column_name: str
        :return: list of values if there are, otherwise None
        """
        if column_name in kpi_line.keys() and kpi_line[column_name] != "":
            cell = kpi_line[column_name]
            if type(cell) in [int, float]:
                return [cell]
            elif type(cell) in [unicode, str]:
                return cell.split(", ")
        return None

    def get_kpi_function(self, kpi_type):
        """
        transfers every kpi to its own function
        :param kpi_type: value from "sheet" column in the main sheet
        :return: function
        """
        if kpi_type == Const.SURVEY:
            return self.calculate_survey_specific
        elif kpi_type == Const.AVAILABILITY:
            return self.calculate_availability
        elif kpi_type == Const.SOS:
            return self.calculate_sos
        elif kpi_type == Const.SOS_MAJOR:
            return self.calculate_sos_maj
        else:
            Log.warning(
                "The value '{}' in column sheet in the template is not recognized"
                .format(kpi_type))
            return None