darkor

configurator

Jun 8th, 2022
615
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.97 KB | None | 0 0
  1. import abc
  2. from collections import Counter
  3. from typing import List, NamedTuple, OrderedDict
  4.  
  5. from django.db.models import Q, Max, F, QuerySet, Case, When, Min, Value, BooleanField, prefetch_related_objects, \
  6.     Prefetch
  7.  
  8. from backend_api.models.user import UserProfile
  9. from backend_api.models.product import (
  10.     Product, ProductPartType, ProductMaterialPrice, AreaName, ProductMaterial, ProductMaterialImage, PaletteColor,
  11.     ProductData
  12. )
  13. import backend_api.views.product_views
  14.  
  15.  
  16. class OutputSizeDataDTO(OrderedDict):
  17.     """
  18.    Data Transfer Object
  19.    Uses to return size data from configurator
  20.    """
  21.     parameter: str
  22.     dimension: str
  23.     value: float
  24.  
  25.  
  26. class ConfiguratorOutputDTO(NamedTuple):
  27.     """
  28.    Data Transfer Object
  29.    Uses to return data from configurator
  30.    """
  31.     price: int
  32.     currency: str
  33.     sizes: List[OutputSizeDataDTO]
  34.  
  35.  
  36. class InputProductConfigurationDTO(NamedTuple):
  37.     """
  38.    Data Transfer Object
  39.    Uses to transfer data area configuration data to Configurator constructor
  40.    """
  41.     area_id: int
  42.     material_group_id: int
  43.     material_id: int
  44.     color_id: int
  45.  
  46.  
  47. class ConfiguratorInputDTO(NamedTuple):
  48.     """
  49.    Data Transfer Object
  50.    Uses to transfer data to configurator
  51.    """
  52.     user: UserProfile
  53.     product: Product
  54.     product_currency: str  # GfgBackend.settings.CURRENCIES
  55.     product_part_type: ProductPartType
  56.     product_configuration: List[InputProductConfigurationDTO]
  57.  
  58.  
  59. class ProductConfigurationDTO(NamedTuple):
  60.     """
  61.    Type hint for _product_configuration (argument in Configurator constructor)
  62.    """
  63.  
  64.     area: AreaName
  65.     material_group: ProductMaterial
  66.     material: ProductMaterialImage
  67.     color: PaletteColor
  68.  
  69.  
  70. class ConfigurationError(Exception):
  71.     """
  72.    Custom exception, indicates something went wrong in configuration
  73.    """
  74.  
  75.     def __init__(self, message: str, status=400, *args):
  76.         self.message = message
  77.         self.status = status
  78.         super(ConfigurationError, self).__init__(*args)
  79.  
  80.     def __str__(self):
  81.         return self.message
  82.  
  83.  
  84. class AbstractBaseConfigurator(abc.ABC):
  85.  
  86.     def __init__(self, configuration_data: ConfiguratorInputDTO, **kwargs):
  87.         self._user: UserProfile = configuration_data.user
  88.         self._product: Product = configuration_data.product
  89.         self._product_currency: str = configuration_data.product_currency
  90.         self._product_part_type: ProductPartType = self.__get_product_part_type(configuration_data.product_part_type)  # On admin site it calls Area
  91.         self._product_configuration: [ProductConfigurationDTO] = self.__product_configuration_ids_to_objects(
  92.             configuration_data.product_configuration
  93.         )
  94.  
  95.         self._areas: QuerySet(AreaName) = self.__get_product_areas()  # On admin site it calls Sub-_areas
  96.         self._product_combinations: QuerySet(ProductMaterialPrice) = self.__get_product_combinations()
  97.  
  98.         self._producer_discount = self._product.producer.producer_discounts.get(user=self._user)
  99.         self._coefficient = self._producer_discount.coefficient
  100.         self._delivery_price = self._producer_discount.delivery_price
  101.  
  102.         self._final_price = 0  # or None
  103.         self._combination = None
  104.         self._combination_price = 0
  105.  
  106.     def __get_product_part_type(self, product_part_type_id) -> ProductPartType:
  107.         try:
  108.             return ProductPartType.objects.get(id=product_part_type_id)
  109.         except ProductPartType.DoesNotExist:
  110.             raise ConfigurationError('Wrong product_part_type_id.')
  111.  
  112.     def __product_configuration_ids_to_objects(self, product_configuration) -> [ProductConfigurationDTO]:
  113.         try:
  114.             return [
  115.                 ProductConfigurationDTO(
  116.                     area=AreaName.objects.get(id=area_configuration.area_id),
  117.                     material=ProductMaterialImage.objects.get(id=area_configuration.material_id),
  118.                     material_group=(
  119.                         ProductMaterial.objects.prefetch_related('material_images')
  120.                         .get(id=area_configuration.material_group_id)
  121.                     ),
  122.                     color=PaletteColor.objects.get(id=area_configuration.color_id)
  123.                     if area_configuration.color_id
  124.                     else None
  125.                 ) for area_configuration in product_configuration
  126.             ]
  127.         except AreaName.DoesNotExist:
  128.             raise ConfigurationError('Wrong area_id.')
  129.         except ProductMaterial.DoesNotExist:
  130.             raise ConfigurationError('Wrong material_group_id.')
  131.         except ProductMaterialImage.DoesNotExist:
  132.             raise ConfigurationError('Wrong material_id.')
  133.         except PaletteColor.DoesNotExist:
  134.             raise ConfigurationError('Wrong color_id.')
  135.  
  136.     def __get_product_areas(self) -> QuerySet[AreaName]:
  137.         product_areas = AreaName.objects.filter(materialareaname__price__product_part__product=self._product).distinct()
  138.         if not product_areas.exists():
  139.             raise ConfigurationError(
  140.                 'There is no area in the _product (must be configured on the admin site).'
  141.             )
  142.         return product_areas
  143.  
  144.     def __get_product_combinations(self) -> QuerySet[ProductMaterialPrice]:
  145.         # Was taken from previous version
  146.         product_combinations = (
  147.             backend_api.views.product_views.price_set(self._product_part_type.id, self._product, self._product_currency)
  148.             .annotate(combination_price=Max(F('prices__price')))
  149.         )
  150.         if not product_combinations.exists():
  151.             raise ConfigurationError('There is no combination for the product.')
  152.  
  153.         return product_combinations
  154.  
  155.     def _validate_areas(self):
  156.         """
  157.        Each _product have 'area' (AreaName instance, e.g. frame, seat, backrest), you must specify in admin panel.
  158.        This method checks do the received '_areas' is equivalent to the expected '_areas' (described the line above).
  159.        """
  160.         received_areas = [area_configuration.area for area_configuration in self._product_configuration]
  161.         if Counter(self._areas) != Counter(received_areas):
  162.             raise ConfigurationError(
  163.                 'Not enough _areas in the _product_configuration!'
  164.                 f'Expected sub-_areas: {self._areas.values_list("id", flat=True)}.'
  165.                 f'Received _areas: {[area.id for area in received_areas]}.'
  166.             )
  167.  
  168.     def _validate_areas_configurations(self):
  169.         """
  170.        Validate does it possible to apply a material to an area, a color to material etc.
  171.        """
  172.         for area_configuration in self._product_configuration:
  173.             area = area_configuration.area
  174.             material_group = area_configuration.material_group
  175.             material = area_configuration.material
  176.             color = area_configuration.color
  177.  
  178.             if not self._product_combinations.filter(
  179.                     materialareaname__name=area, materialareaname__material=material_group
  180.             ).exists():
  181.                 raise ConfigurationError(
  182.                     f'There is no _combination of Area {area.id} and '
  183.                     f'Material Group {material_group.id}.'
  184.                 )
  185.  
  186.             if material_group not in self._product.materials.all():
  187.                 raise ConfigurationError(
  188.                     f'The material group "{material_group.id}" is not present in _product materials.'
  189.                 )
  190.  
  191.             if material not in material_group.material_images.all():
  192.                 raise ConfigurationError(
  193.                     f'The material "{material.id}" is not present in material group "{material_group.id}".'
  194.                 )
  195.  
  196.             if color and color not in PaletteColor.objects.filter(palette__productmaterialimage=material):
  197.                 raise ConfigurationError(f'The color "{color.id}" is not available for material "{material.id}".')
  198.  
  199.     def _get_combination(self):
  200.         """
  201.        Find most suitable _combination
  202.        """
  203.         combinations = self._product_combinations  # Make a copy, with which we will operate
  204.         condition_list = []
  205.  
  206.         for area_configuration in self._product_configuration:
  207.             condition = Q(**{'materialareaname__name': area_configuration.area}) & \
  208.                         Q(**{'materialareaname__material': area_configuration.material_group})
  209.             combinations = combinations.filter(condition)
  210.             condition_list.append(condition)
  211.  
  212.         if combinations.exists():
  213.             self._combination = combinations.earliest('combination_price')  # Return the lowest 'ideal' _combination
  214.         else:
  215.             # Return the richest of 'local lowest' combinations
  216.             # i.e. filter _product_combinations by each condition and take the lowest combination
  217.             min_combinations = []
  218.             for cond in condition_list:
  219.                 lowest_combination = self._product_combinations.filter(cond).earliest('combination_price')
  220.                 min_combinations.append({
  221.                     '_combination': lowest_combination,
  222.                     'price': lowest_combination.combination_price
  223.                 })
  224.  
  225.             self._combination = max(min_combinations, key=lambda x: x.get('price')).get('_combination')
  226.  
  227.     def _get_combination_price(self):
  228.         # SHIT
  229.         if not hasattr(self._combination, 'combination_price'):
  230.             self._combination_price = self._combination.prices.all().aggregate(max_price=Max('price')).get('max_price')
  231.         else:
  232.             self._combination_price = self._combination.combination_price
  233.  
  234.     def _calculate_final_price(self):
  235.         self._final_price = round(float(self._combination_price) * self._coefficient, 2)
  236.  
  237.     def _get_combination_sizes(self) -> [OutputSizeDataDTO]:
  238.         prefetch_related_objects(
  239.             [self._combination],
  240.             Prefetch('product_data', queryset=ProductData.objects.select_related('parameter', 'dimension'))
  241.         )
  242.         return [
  243.             OutputSizeDataDTO(
  244.                 parameter=item.parameter.name,
  245.                 dimension=item.dimension.name,
  246.                 value=item.value
  247.             ) for item in self._combination.product_data.all()
  248.         ]
  249.  
  250.     def _configuration_result(self) -> ConfiguratorOutputDTO:
  251.         return ConfiguratorOutputDTO(
  252.             price=self._final_price,
  253.             currency=self._product_currency,
  254.             sizes=self._get_combination_sizes()
  255.         )
  256.  
  257.     def configure(self) -> ConfiguratorOutputDTO:
  258.         self._validate_areas()
  259.         self._validate_areas_configurations()
  260.         self._get_combination()
  261.         self._get_combination_price()
  262.         self._calculate_final_price()
  263.         return self._configuration_result()
  264.  
  265.     # def create_specification(self):
  266.     #     raise NotImplementedError
  267.     #
  268.     # def save_configuration(self):
  269.     #     self.configure()
  270.     #     self.create_specification()
  271.  
  272.  
  273. class BaseConfigurator(AbstractBaseConfigurator):
  274.     """
  275.    Base configurator
  276.    """
  277.  
Advertisement
Add Comment
Please, Sign In to add comment