darkor

configurator

Jun 24th, 2022 (edited)
922
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 17.00 KB | None | 0 0
  1. class AbstractConfigurator(abc.ABC):
  2.  
  3.     def __init__(self, configuration_data: ConfiguratorInputDTO, **kwargs):
  4.         self._user: UserProfile = configuration_data.user
  5.         self._product: Product = configuration_data.product
  6.         self._alternate_currency = self._user.extra_currency
  7.         self._product_currency: str = get_user_product_currency(self._product, self._user.id)
  8.  
  9.         # On admin site it calls Area
  10.         self._product_part_type: ProductPartType = self.__get_product_part_type(configuration_data.product_part_type_id)
  11.         self._product_configuration: [ProductConfigurationDTO] = self.__product_configuration_ids_to_objects(
  12.             configuration_data.product_configuration
  13.         )
  14.  
  15.         self._areas: QuerySet(AreaName) = self.__get_product_areas()  # On admin site it calls Sub-areas
  16.         self._product_combinations: QuerySet(ProductMaterialPrice) = self.__get_product_combinations()
  17.  
  18.         self._producer_discount = self._product.producer.producer_discounts.get(user=self._user)
  19.         self._coefficient = self._producer_discount.coefficient
  20.         self._delivery_price = self._producer_discount.delivery_price
  21.  
  22.         self._sizes: [OutputSizeDataDTO] = None
  23.         self._combination = None
  24.  
  25.         self._combination_price = 0.0
  26.  
  27.         # self._price - Product price without calculation with coefficient etc.
  28.         #   (usually it will be equal to combination price)
  29.         # Need it to get product price during calculating price of specification
  30.         self._price = 0.0
  31.  
  32.         # Fully calculated price (multiply on coefficient etc.)
  33.         self._final_price = 0.0
  34.  
  35.     def __get_product_part_type(self, product_part_type_id: int) -> ProductPartType:
  36.         try:
  37.             return ProductPartType.objects.get(id=product_part_type_id)
  38.         except ProductPartType.DoesNotExist:
  39.             raise ConfigurationError('Wrong product_part_type_id.')
  40.  
  41.     def __product_configuration_ids_to_objects(
  42.         self, product_configuration: [InputProductConfigurationDTO]
  43.     ) -> [ProductConfigurationDTO]:
  44.         """
  45.        Took list of areas configurations (area_id, material_id, material_group_id, color_id)
  46.        Return list of DTOs consist of objects (PaletteColor etc.) instead of ids
  47.        """
  48.         try:
  49.             configurations = []
  50.  
  51.             for area_configuration in product_configuration:
  52.                 material = ProductMaterialImage.objects.select_related('default_color__palette').get(
  53.                     id=area_configuration.material_id
  54.                 )
  55.  
  56.                 # If color_id from payload is None, we should check does the material have a default_color
  57.                 if not area_configuration.color_id and material.default_color:
  58.                     color = material.default_color
  59.                 else:
  60.                     color = (
  61.                         PaletteColor.objects.select_related('palette').get(id=area_configuration.color_id)
  62.                         if area_configuration.color_id
  63.                         else None
  64.                     )
  65.  
  66.                 configurations.append(
  67.                     ProductConfigurationDTO(
  68.                         area=AreaName.objects.get(id=area_configuration.area_id),
  69.                         material=material,
  70.                         material_group=ProductMaterial.objects.get(
  71.                             id=area_configuration.material_group_id
  72.                         ),
  73.                         color=color
  74.                     )
  75.                 )
  76.  
  77.             return configurations
  78.         except AreaName.DoesNotExist:
  79.             raise ConfigurationError('Wrong area_id.')
  80.         except ProductMaterial.DoesNotExist:
  81.             raise ConfigurationError('Wrong material_group_id.')
  82.         except ProductMaterialImage.DoesNotExist:
  83.             raise ConfigurationError('Wrong material_id.')
  84.         except PaletteColor.DoesNotExist:
  85.             raise ConfigurationError('Wrong color_id.')
  86.  
  87.     def __get_product_areas(self) -> QuerySet[AreaName]:
  88.         product_areas = AreaName.objects.filter(materialareaname__price__product_part__product=self._product).distinct()
  89.         if not product_areas.exists():
  90.             raise ConfigurationError(
  91.                 'There is no area in the _product (must be configured on the admin site).'
  92.             )
  93.         return product_areas
  94.  
  95.     def __get_product_combinations(self) -> QuerySet[ProductMaterialPrice]:
  96.         # Was taken from previous version
  97.         product_combinations = (
  98.             backend_api.views.product_views.price_set(self._product_part_type.id, self._product, self._product_currency)
  99.             .annotate(combination_price=Max(F('prices__price')))
  100.         )
  101.         if not product_combinations.exists():
  102.             raise ConfigurationError('There is no combination for the product.')
  103.  
  104.         return product_combinations
  105.  
  106.     def configure(self) -> ConfiguratorOutputDTO:
  107.         """
  108.        ne phai vlasni fucking hands to here
  109.        """
  110.         self._validate_areas()
  111.         self._validate_areas_configurations()
  112.         self._get_combination()
  113.         self._combination_price = self._get_combination_price()
  114.         self._price = self._calculate_price(self._combination_price)
  115.         self._final_price = self._calculate_final_price(self._price)
  116.         self._sizes = self._get_combination_sizes(self._combination)
  117.         return self._configuration_result()
  118.  
  119.     def _validate_areas(self):
  120.         """
  121.        Validate received areas (instances of AreaName)
  122.        (e.i. compare with expected areas, assigned to product (in combinations)
  123.        """
  124.         raise NotImplementedError
  125.  
  126.     def _validate_areas_configurations(self):
  127.         """
  128.        Check is it possible to assign some material A to area B and so on
  129.        """
  130.         raise NotImplementedError
  131.  
  132.     def _get_combination(self):
  133.         """
  134.        Do here some manipulations to find the most suitable combination and return it
  135.        """
  136.         raise NotImplementedError
  137.  
  138.     def _get_combination_price(self) -> float:
  139.         """
  140.        Return price of combination
  141.        """
  142.         raise NotImplementedError
  143.  
  144.     def _calculate_price(self, combination_price: float) -> float:
  145.         """
  146.        If you need to change price during calculation this method is created for that.
  147.        Calculates price uses during calculating specification price.
  148.        If there is no need in it then just return combination_price that you`ve got.
  149.        """
  150.         raise NotImplementedError
  151.  
  152.     def _calculate_final_price(self, price: float) -> float:
  153.         """
  154.        Multiply price by coefficient and round
  155.        """
  156.         raise NotImplementedError
  157.  
  158.     @staticmethod
  159.     def _get_combination_sizes(combination: ProductMaterialPrice) -> [OutputSizeDataDTO]:
  160.         """
  161.        Return sizes assigned to the combination
  162.        """
  163.         prefetch_related_objects(
  164.             [combination],
  165.             Prefetch('product_data', queryset=ProductData.objects.select_related('parameter', 'dimension'))
  166.         )
  167.         return [
  168.             OutputSizeDataDTO(
  169.                 parameter=item.parameter.name,
  170.                 dimension=item.dimension.name,
  171.                 value=item.value
  172.             ) for item in combination.product_data.all()
  173.         ]
  174.  
  175.     def _configuration_result(self) -> ConfiguratorOutputDTO:
  176.         raise NotImplementedError
  177.  
  178.     @transaction.atomic
  179.     def save_configuration(self, image=None):
  180.         self.configure()  # Do all validations to be sure that it is possible to save the product configuration
  181.         temp, specification = self._get_temp_and_specification()  # get or create
  182.         self._clean_specification(specification)
  183.         product_part_specification = self._create_product_part_specification(specification)
  184.         self._apply_configuration_on_specification(self._product_configuration, product_part_specification)
  185.         self._apply_image_to_specification(specification, image)
  186.         return None
  187.  
  188.     def _get_temp_and_specification(self) -> (Temp, Specification):
  189.         """
  190.        Get or create instances of Temp and Specification models and return them
  191.        """
  192.         raise NotImplementedError
  193.  
  194.     def _clean_specification(self, specification: Specification):
  195.         """
  196.        Remove SpecificationMaterials and ProductPartSpecifications related to the specification
  197.        """
  198.         raise NotImplementedError
  199.  
  200.     def _create_product_part_specification(self, specification: Specification) -> ProductPartSpecification:
  201.         raise NotImplementedError
  202.  
  203.     @staticmethod
  204.     def _apply_configuration_on_specification(
  205.         product_configuration: [ProductConfigurationDTO],
  206.         product_part_specification: ProductPartSpecification
  207.     ):
  208.         raise NotImplementedError
  209.  
  210.     def _apply_image_to_specification(self, specification, image):
  211.         """
  212.        Assign the image to the specification and DO NOT FORGET to save specification
  213.        """
  214.         raise NotImplementedError
  215.  
  216.  
  217. class BaseConfigurator(AbstractConfigurator):
  218.  
  219.     def __init__(self, configuration_data: ConfiguratorInputDTO, **kwargs):
  220.         super().__init__(configuration_data, **kwargs)
  221.  
  222.     def _validate_areas(self):
  223.         """
  224.        Each product have 'area' (AreaName instance, e.g. frame, seat, backrest), you must specify in admin panel.
  225.        This method checks do the received 'areas' is equivalent to the expected 'areas' (described the line above).
  226.        """
  227.         received_areas = [area_configuration.area for area_configuration in self._product_configuration]
  228.         if Counter(self._areas) != Counter(received_areas):
  229.             raise ConfigurationError(
  230.                 'Not enough areas in the product_configuration!'
  231.                 f'Expected sub-areas: {self._areas.values_list("id", flat=True)}.'
  232.                 f'Received areas: {[area.id for area in received_areas]}.'
  233.             )
  234.  
  235.     def _validate_areas_configurations(self):
  236.         """
  237.        Validate does it possible to apply a material to an area, a color to material etc.
  238.        """
  239.         for area_configuration in self._product_configuration:
  240.             area = area_configuration.area
  241.             material_group = area_configuration.material_group
  242.             material = area_configuration.material
  243.             color = area_configuration.color
  244.  
  245.             if not self._product_combinations.filter(
  246.                     materialareaname__name=area, materialareaname__material=material_group
  247.             ).exists():
  248.                 raise ConfigurationError(
  249.                     f'There is no _combination of Area {area.id} and '
  250.                     f'Material Group {material_group.id}.'
  251.                 )
  252.  
  253.             if material_group not in self._product.materials.all():
  254.                 raise ConfigurationError(
  255.                     f'The material group "{material_group.id}" is not present in product materials.'
  256.                 )
  257.  
  258.             if material not in material_group.material_images.all():
  259.                 raise ConfigurationError(
  260.                     f'The material "{material.id}" is not present in material group "{material_group.id}".'
  261.                 )
  262.  
  263.             if color and color.palette not in material.available_palettes.all():
  264.                 raise ConfigurationError(f'The color "{color.id}" is not available for material "{material.id}".')
  265.  
  266.     def _get_combination(self):
  267.         """
  268.        Find the most suitable combination
  269.        """
  270.         combinations = self._product_combinations  # Make a copy, with which we will operate
  271.         condition_list = []
  272.  
  273.         for area_configuration in self._product_configuration:
  274.             condition = Q(**{'materialareaname__name': area_configuration.area}) & \
  275.                         Q(**{'materialareaname__material': area_configuration.material_group})
  276.             combinations = combinations.filter(condition)
  277.             condition_list.append(condition)
  278.  
  279.         if combinations.exists():
  280.             self._combination = combinations.earliest('combination_price')  # Return the lowest 'ideal' combination
  281.         else:
  282.             # Return the richest of 'local lowest' combinations
  283.             # i.e. filter product_combinations by each condition and take the lowest combination
  284.             min_combinations = []
  285.             for cond in condition_list:
  286.                 lowest_combination = self._product_combinations.filter(cond).earliest('combination_price')
  287.                 min_combinations.append({
  288.                     'combination': lowest_combination,
  289.                     'price': lowest_combination.combination_price
  290.                 })
  291.  
  292.             if not min_combinations:
  293.                 raise ConfigurationError('There is no suitable combination for the received product configuration.')
  294.  
  295.             self._combination = max(min_combinations, key=lambda x: x.get('price')).get('combination')
  296.  
  297.     def _get_combination_price(self) -> float:
  298.         if not hasattr(self._combination, 'combination_price'):
  299.             # In case if some developer break annotation where defines 'combination_price'
  300.             return self._combination.prices.all().aggregate(max_price=Max('price')).get('max_price')
  301.         return self._combination.combination_price
  302.  
  303.     def _calculate_price(self, combination_price: float) -> float:
  304.         return combination_price
  305.  
  306.     def _calculate_final_price(self, price: float) -> float:
  307.         return round(float(price) * self._coefficient, 2)
  308.  
  309.     def _configuration_result(self) -> ConfiguratorOutputDTO:
  310.         sizes = self._sizes
  311.         if not self._user.metric_measurement:
  312.             for size in sizes:
  313.                 size['dimension'], size['value'] = sizes_to_imperial(size['dimension'], size['value'])
  314.  
  315.         price = self._final_price
  316.         if self._alternate_currency:
  317.             price = price_to_currency(price, self._alternate_currency.exchange_rate)
  318.  
  319.         return ConfiguratorOutputDTO(
  320.             price=price,
  321.             currency=self._alternate_currency.name if self._alternate_currency else self._product_currency,
  322.             sizes=sizes
  323.         )
  324.  
  325.     def _get_temp_and_specification(self):
  326.         try:
  327.             temp = self._product.get_temp(self._user)
  328.             temp.product = self._product
  329.             specification = Specification.objects.get(temp_configuration=temp)
  330.             specification.product = temp.product
  331.             temp.save()
  332.             specification.save()
  333.         except Temp.DoesNotExist:
  334.             temp = self._product.create_temp(self._user)
  335.             specification = Specification.objects.create(
  336.                 user=self._user,
  337.                 temp_configuration=temp,
  338.                 product=temp.product
  339.             )
  340.         return specification, temp
  341.  
  342.     def _clean_specification(self, specification: Specification):
  343.         SpecificationMaterial.objects.filter(productpartspecification__specification=specification).delete()
  344.         ProductPartSpecification.objects.filter(specification=specification).delete()
  345.  
  346.     def _create_product_part_specification(self, specification: Specification):
  347.         return ProductPartSpecification.objects.create(
  348.             currency=self._product_currency,
  349.             product_material_price=self._combination,
  350.             specification=specification,
  351.             duplicated=False,  # This should be taken from request (it was in past). But frontend doesn't send it now.
  352.             duplicated_number=0,  # The same situation as with the 'duplicated' (above)
  353.         )
  354.  
  355.     @staticmethod
  356.     def _apply_configuration_on_specification(
  357.         product_configuration: [ProductConfigurationDTO],
  358.         product_part_specification: ProductPartSpecification
  359.     ):
  360.         specification_materials_ids = []
  361.  
  362.         for configuration in product_configuration:
  363.             area = configuration.area
  364.             material = configuration.material
  365.             material_group = configuration.material_group
  366.             color = configuration.color
  367.  
  368.             specification_material = SpecificationMaterial.objects.create(
  369.                 material_image=material,
  370.                 mat_area_name=area,
  371.                 material=material_group,
  372.                 material_image_palette=color.palette,
  373.                 material_image_palette_color=color
  374.             )
  375.             specification_materials_ids.append(specification_material.id)
  376.  
  377.         product_part_specification.materials.add(
  378.             *(SpecificationMaterial.objects.filter(id__in=specification_materials_ids))
  379.         )
  380.  
  381.     def _apply_image_to_specification(self, specification: Specification, image=None):
  382.         if image:
  383.             try:
  384.                 image_data = b64decode(image)
  385.             except TypeError:
  386.                 raise ConfigurationError(
  387.                     'Configuration is not saved, Image of product %s is not base64 encoded' % self._product.name
  388.                 )
  389.  
  390.             image = ContentFile(image_data, make_filename_unique(file_name=f'specification_{specification.pk}.jpg'))
  391.             specification.image = image
  392.             specification.save()
Add Comment
Please, Sign In to add comment