Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- class AbstractConfigurator(abc.ABC):
- def __init__(self, configuration_data: ConfiguratorInputDTO, **kwargs):
- self._user: UserProfile = configuration_data.user
- self._product: Product = configuration_data.product
- self._alternate_currency = self._user.extra_currency
- self._product_currency: str = get_user_product_currency(self._product, self._user.id)
- # On admin site it calls Area
- self._product_part_type: ProductPartType = self.__get_product_part_type(configuration_data.product_part_type_id)
- 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._sizes: [OutputSizeDataDTO] = None
- self._combination = None
- self._combination_price = 0.0
- # self._price - Product price without calculation with coefficient etc.
- # (usually it will be equal to combination price)
- # Need it to get product price during calculating price of specification
- self._price = 0.0
- # Fully calculated price (multiply on coefficient etc.)
- self._final_price = 0.0
- def __get_product_part_type(self, product_part_type_id: int) -> 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: [InputProductConfigurationDTO]
- ) -> [ProductConfigurationDTO]:
- """
- Took list of areas configurations (area_id, material_id, material_group_id, color_id)
- Return list of DTOs consist of objects (PaletteColor etc.) instead of ids
- """
- try:
- configurations = []
- for area_configuration in product_configuration:
- material = ProductMaterialImage.objects.select_related('default_color__palette').get(
- id=area_configuration.material_id
- )
- # If color_id from payload is None, we should check does the material have a default_color
- if not area_configuration.color_id and material.default_color:
- color = material.default_color
- else:
- color = (
- PaletteColor.objects.select_related('palette').get(id=area_configuration.color_id)
- if area_configuration.color_id
- else None
- )
- configurations.append(
- ProductConfigurationDTO(
- area=AreaName.objects.get(id=area_configuration.area_id),
- material=material,
- material_group=ProductMaterial.objects.get(
- id=area_configuration.material_group_id
- ),
- color=color
- )
- )
- return configurations
- 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 configure(self) -> ConfiguratorOutputDTO:
- """
- ne phai vlasni fucking hands to here
- """
- self._validate_areas()
- self._validate_areas_configurations()
- self._get_combination()
- self._combination_price = self._get_combination_price()
- self._price = self._calculate_price(self._combination_price)
- self._final_price = self._calculate_final_price(self._price)
- self._sizes = self._get_combination_sizes(self._combination)
- return self._configuration_result()
- def _validate_areas(self):
- """
- Validate received areas (instances of AreaName)
- (e.i. compare with expected areas, assigned to product (in combinations)
- """
- raise NotImplementedError
- def _validate_areas_configurations(self):
- """
- Check is it possible to assign some material A to area B and so on
- """
- raise NotImplementedError
- def _get_combination(self):
- """
- Do here some manipulations to find the most suitable combination and return it
- """
- raise NotImplementedError
- def _get_combination_price(self) -> float:
- """
- Return price of combination
- """
- raise NotImplementedError
- def _calculate_price(self, combination_price: float) -> float:
- """
- If you need to change price during calculation this method is created for that.
- Calculates price uses during calculating specification price.
- If there is no need in it then just return combination_price that you`ve got.
- """
- raise NotImplementedError
- def _calculate_final_price(self, price: float) -> float:
- """
- Multiply price by coefficient and round
- """
- raise NotImplementedError
- @staticmethod
- def _get_combination_sizes(combination: ProductMaterialPrice) -> [OutputSizeDataDTO]:
- """
- Return sizes assigned to the combination
- """
- prefetch_related_objects(
- [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 combination.product_data.all()
- ]
- def _configuration_result(self) -> ConfiguratorOutputDTO:
- raise NotImplementedError
- @transaction.atomic
- def save_configuration(self, image=None):
- self.configure() # Do all validations to be sure that it is possible to save the product configuration
- temp, specification = self._get_temp_and_specification() # get or create
- self._clean_specification(specification)
- product_part_specification = self._create_product_part_specification(specification)
- self._apply_configuration_on_specification(self._product_configuration, product_part_specification)
- self._apply_image_to_specification(specification, image)
- return None
- def _get_temp_and_specification(self) -> (Temp, Specification):
- """
- Get or create instances of Temp and Specification models and return them
- """
- raise NotImplementedError
- def _clean_specification(self, specification: Specification):
- """
- Remove SpecificationMaterials and ProductPartSpecifications related to the specification
- """
- raise NotImplementedError
- def _create_product_part_specification(self, specification: Specification) -> ProductPartSpecification:
- raise NotImplementedError
- @staticmethod
- def _apply_configuration_on_specification(
- product_configuration: [ProductConfigurationDTO],
- product_part_specification: ProductPartSpecification
- ):
- raise NotImplementedError
- def _apply_image_to_specification(self, specification, image):
- """
- Assign the image to the specification and DO NOT FORGET to save specification
- """
- raise NotImplementedError
- class BaseConfigurator(AbstractConfigurator):
- def __init__(self, configuration_data: ConfiguratorInputDTO, **kwargs):
- super().__init__(configuration_data, **kwargs)
- 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.palette not in material.available_palettes.all():
- raise ConfigurationError(f'The color "{color.id}" is not available for material "{material.id}".')
- def _get_combination(self):
- """
- Find the 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
- })
- if not min_combinations:
- raise ConfigurationError('There is no suitable combination for the received product configuration.')
- self._combination = max(min_combinations, key=lambda x: x.get('price')).get('combination')
- def _get_combination_price(self) -> float:
- if not hasattr(self._combination, 'combination_price'):
- # In case if some developer break annotation where defines 'combination_price'
- return self._combination.prices.all().aggregate(max_price=Max('price')).get('max_price')
- return self._combination.combination_price
- def _calculate_price(self, combination_price: float) -> float:
- return combination_price
- def _calculate_final_price(self, price: float) -> float:
- return round(float(price) * self._coefficient, 2)
- def _configuration_result(self) -> ConfiguratorOutputDTO:
- sizes = self._sizes
- if not self._user.metric_measurement:
- for size in sizes:
- size['dimension'], size['value'] = sizes_to_imperial(size['dimension'], size['value'])
- price = self._final_price
- if self._alternate_currency:
- price = price_to_currency(price, self._alternate_currency.exchange_rate)
- return ConfiguratorOutputDTO(
- price=price,
- currency=self._alternate_currency.name if self._alternate_currency else self._product_currency,
- sizes=sizes
- )
- def _get_temp_and_specification(self):
- try:
- temp = self._product.get_temp(self._user)
- temp.product = self._product
- specification = Specification.objects.get(temp_configuration=temp)
- specification.product = temp.product
- temp.save()
- specification.save()
- except Temp.DoesNotExist:
- temp = self._product.create_temp(self._user)
- specification = Specification.objects.create(
- user=self._user,
- temp_configuration=temp,
- product=temp.product
- )
- return specification, temp
- def _clean_specification(self, specification: Specification):
- SpecificationMaterial.objects.filter(productpartspecification__specification=specification).delete()
- ProductPartSpecification.objects.filter(specification=specification).delete()
- def _create_product_part_specification(self, specification: Specification):
- return ProductPartSpecification.objects.create(
- currency=self._product_currency,
- product_material_price=self._combination,
- specification=specification,
- duplicated=False, # This should be taken from request (it was in past). But frontend doesn't send it now.
- duplicated_number=0, # The same situation as with the 'duplicated' (above)
- )
- @staticmethod
- def _apply_configuration_on_specification(
- product_configuration: [ProductConfigurationDTO],
- product_part_specification: ProductPartSpecification
- ):
- specification_materials_ids = []
- for configuration in product_configuration:
- area = configuration.area
- material = configuration.material
- material_group = configuration.material_group
- color = configuration.color
- specification_material = SpecificationMaterial.objects.create(
- material_image=material,
- mat_area_name=area,
- material=material_group,
- material_image_palette=color.palette,
- material_image_palette_color=color
- )
- specification_materials_ids.append(specification_material.id)
- product_part_specification.materials.add(
- *(SpecificationMaterial.objects.filter(id__in=specification_materials_ids))
- )
- def _apply_image_to_specification(self, specification: Specification, image=None):
- if image:
- try:
- image_data = b64decode(image)
- except TypeError:
- raise ConfigurationError(
- 'Configuration is not saved, Image of product %s is not base64 encoded' % self._product.name
- )
- image = ContentFile(image_data, make_filename_unique(file_name=f'specification_{specification.pk}.jpg'))
- specification.image = image
- specification.save()
Add Comment
Please, Sign In to add comment