Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import abc
- from collections import Counter
- from typing import List, NamedTuple, OrderedDict
- from django.db.models import Q, Max, F, QuerySet, Case, When, Min, Value, BooleanField, prefetch_related_objects, \
- Prefetch
- from backend_api.models.user import UserProfile
- from backend_api.models.product import (
- Product, ProductPartType, ProductMaterialPrice, AreaName, ProductMaterial, ProductMaterialImage, PaletteColor,
- ProductData
- )
- import backend_api.views.product_views
- class OutputSizeDataDTO(OrderedDict):
- """
- Data Transfer Object
- Uses to return size data from configurator
- """
- parameter: str
- dimension: str
- value: float
- class ConfiguratorOutputDTO(NamedTuple):
- """
- Data Transfer Object
- Uses to return data from configurator
- """
- price: int
- currency: str
- sizes: List[OutputSizeDataDTO]
- class InputProductConfigurationDTO(NamedTuple):
- """
- Data Transfer Object
- Uses to transfer data area configuration data to Configurator constructor
- """
- area_id: int
- material_group_id: int
- material_id: int
- color_id: int
- class ConfiguratorInputDTO(NamedTuple):
- """
- Data Transfer Object
- Uses to transfer data to configurator
- """
- user: UserProfile
- product: Product
- product_currency: str # GfgBackend.settings.CURRENCIES
- product_part_type: ProductPartType
- product_configuration: List[InputProductConfigurationDTO]
- class ProductConfigurationDTO(NamedTuple):
- """
- Type hint for _product_configuration (argument in Configurator constructor)
- """
- area: AreaName
- material_group: ProductMaterial
- material: ProductMaterialImage
- color: PaletteColor
- class ConfigurationError(Exception):
- """
- Custom exception, indicates something went wrong in configuration
- """
- def __init__(self, message: str, status=400, *args):
- self.message = message
- self.status = status
- super(ConfigurationError, self).__init__(*args)
- def __str__(self):
- return self.message
- class AbstractBaseConfigurator(abc.ABC):
- def __init__(self, configuration_data: ConfiguratorInputDTO, **kwargs):
- self._user: UserProfile = configuration_data.user
- self._product: Product = configuration_data.product
- self._product_currency: str = configuration_data.product_currency
- self._product_part_type: ProductPartType = self.__get_product_part_type(configuration_data.product_part_type) # On admin site it calls Area
- self._product_configuration: [ProductConfigurationDTO] = self.__product_configuration_ids_to_objects(
- configuration_data.product_configuration
- )
- self._areas: QuerySet(AreaName) = self.__get_product_areas() # On admin site it calls Sub-_areas
- self._product_combinations: QuerySet(ProductMaterialPrice) = self.__get_product_combinations()
- self._producer_discount = self._product.producer.producer_discounts.get(user=self._user)
- self._coefficient = self._producer_discount.coefficient
- self._delivery_price = self._producer_discount.delivery_price
- self._final_price = 0 # or None
- self._combination = None
- self._combination_price = 0
- def __get_product_part_type(self, product_part_type_id) -> ProductPartType:
- try:
- return ProductPartType.objects.get(id=product_part_type_id)
- except ProductPartType.DoesNotExist:
- raise ConfigurationError('Wrong product_part_type_id.')
- def __product_configuration_ids_to_objects(self, product_configuration) -> [ProductConfigurationDTO]:
- try:
- return [
- ProductConfigurationDTO(
- area=AreaName.objects.get(id=area_configuration.area_id),
- material=ProductMaterialImage.objects.get(id=area_configuration.material_id),
- material_group=(
- ProductMaterial.objects.prefetch_related('material_images')
- .get(id=area_configuration.material_group_id)
- ),
- color=PaletteColor.objects.get(id=area_configuration.color_id)
- if area_configuration.color_id
- else None
- ) for area_configuration in product_configuration
- ]
- except AreaName.DoesNotExist:
- raise ConfigurationError('Wrong area_id.')
- except ProductMaterial.DoesNotExist:
- raise ConfigurationError('Wrong material_group_id.')
- except ProductMaterialImage.DoesNotExist:
- raise ConfigurationError('Wrong material_id.')
- except PaletteColor.DoesNotExist:
- raise ConfigurationError('Wrong color_id.')
- def __get_product_areas(self) -> QuerySet[AreaName]:
- product_areas = AreaName.objects.filter(materialareaname__price__product_part__product=self._product).distinct()
- if not product_areas.exists():
- raise ConfigurationError(
- 'There is no area in the _product (must be configured on the admin site).'
- )
- return product_areas
- def __get_product_combinations(self) -> QuerySet[ProductMaterialPrice]:
- # Was taken from previous version
- product_combinations = (
- backend_api.views.product_views.price_set(self._product_part_type.id, self._product, self._product_currency)
- .annotate(combination_price=Max(F('prices__price')))
- )
- if not product_combinations.exists():
- raise ConfigurationError('There is no combination for the product.')
- return product_combinations
- def _validate_areas(self):
- """
- Each _product have 'area' (AreaName instance, e.g. frame, seat, backrest), you must specify in admin panel.
- This method checks do the received '_areas' is equivalent to the expected '_areas' (described the line above).
- """
- received_areas = [area_configuration.area for area_configuration in self._product_configuration]
- if Counter(self._areas) != Counter(received_areas):
- raise ConfigurationError(
- 'Not enough _areas in the _product_configuration!'
- f'Expected sub-_areas: {self._areas.values_list("id", flat=True)}.'
- f'Received _areas: {[area.id for area in received_areas]}.'
- )
- def _validate_areas_configurations(self):
- """
- Validate does it possible to apply a material to an area, a color to material etc.
- """
- for area_configuration in self._product_configuration:
- area = area_configuration.area
- material_group = area_configuration.material_group
- material = area_configuration.material
- color = area_configuration.color
- if not self._product_combinations.filter(
- materialareaname__name=area, materialareaname__material=material_group
- ).exists():
- raise ConfigurationError(
- f'There is no _combination of Area {area.id} and '
- f'Material Group {material_group.id}.'
- )
- if material_group not in self._product.materials.all():
- raise ConfigurationError(
- f'The material group "{material_group.id}" is not present in _product materials.'
- )
- if material not in material_group.material_images.all():
- raise ConfigurationError(
- f'The material "{material.id}" is not present in material group "{material_group.id}".'
- )
- if color and color not in PaletteColor.objects.filter(palette__productmaterialimage=material):
- raise ConfigurationError(f'The color "{color.id}" is not available for material "{material.id}".')
- def _get_combination(self):
- """
- Find most suitable _combination
- """
- combinations = self._product_combinations # Make a copy, with which we will operate
- condition_list = []
- for area_configuration in self._product_configuration:
- condition = Q(**{'materialareaname__name': area_configuration.area}) & \
- Q(**{'materialareaname__material': area_configuration.material_group})
- combinations = combinations.filter(condition)
- condition_list.append(condition)
- if combinations.exists():
- self._combination = combinations.earliest('combination_price') # Return the lowest 'ideal' _combination
- else:
- # Return the richest of 'local lowest' combinations
- # i.e. filter _product_combinations by each condition and take the lowest combination
- min_combinations = []
- for cond in condition_list:
- lowest_combination = self._product_combinations.filter(cond).earliest('combination_price')
- min_combinations.append({
- '_combination': lowest_combination,
- 'price': lowest_combination.combination_price
- })
- self._combination = max(min_combinations, key=lambda x: x.get('price')).get('_combination')
- def _get_combination_price(self):
- # SHIT
- if not hasattr(self._combination, 'combination_price'):
- self._combination_price = self._combination.prices.all().aggregate(max_price=Max('price')).get('max_price')
- else:
- self._combination_price = self._combination.combination_price
- def _calculate_final_price(self):
- self._final_price = round(float(self._combination_price) * self._coefficient, 2)
- def _get_combination_sizes(self) -> [OutputSizeDataDTO]:
- prefetch_related_objects(
- [self._combination],
- Prefetch('product_data', queryset=ProductData.objects.select_related('parameter', 'dimension'))
- )
- return [
- OutputSizeDataDTO(
- parameter=item.parameter.name,
- dimension=item.dimension.name,
- value=item.value
- ) for item in self._combination.product_data.all()
- ]
- def _configuration_result(self) -> ConfiguratorOutputDTO:
- return ConfiguratorOutputDTO(
- price=self._final_price,
- currency=self._product_currency,
- sizes=self._get_combination_sizes()
- )
- def configure(self) -> ConfiguratorOutputDTO:
- self._validate_areas()
- self._validate_areas_configurations()
- self._get_combination()
- self._get_combination_price()
- self._calculate_final_price()
- return self._configuration_result()
- # def create_specification(self):
- # raise NotImplementedError
- #
- # def save_configuration(self):
- # self.configure()
- # self.create_specification()
- class BaseConfigurator(AbstractBaseConfigurator):
- """
- Base configurator
- """
Advertisement
Add Comment
Please, Sign In to add comment