class ShipmentProduct(models.Model): shipment = models.ForeignKey(Shipment, related_name='products', on_delete=models.PROTECT) product = models.ForeignKey("Product", related_name='shipments') quantity = QuantityField() unit_volume = MeasurementField( unit="m3" ) # volume is m^3, not mm^3, because mm^3 are tiny. like ants. unit_weight = MeasurementField(unit="g") class Meta: verbose_name = _('sent product') verbose_name_plural = _('sent products') def __str__(self): # pragma: no cover return "%(quantity)s of '%(product)s' in Shipment #%(shipment_pk)s" % { 'product': self.product, 'quantity': self.quantity, 'shipment_pk': self.shipment_id, } def cache_values(self): prod = self.product self.unit_volume = (prod.width * prod.height * prod.depth) / CUBIC_MM_TO_CUBIC_METERS_DIVISOR self.unit_weight = prod.gross_weight
class Shipment(models.Model): order = models.ForeignKey("Order", related_name='shipments', on_delete=models.PROTECT) supplier = models.ForeignKey("Supplier", related_name='shipments', on_delete=models.PROTECT) created_on = models.DateTimeField(auto_now_add=True) status = EnumIntegerField(ShipmentStatus, default=ShipmentStatus.NOT_SENT) tracking_code = models.CharField(max_length=64, blank=True, verbose_name=_('tracking code')) description = models.CharField(max_length=255, blank=True) volume = MeasurementField(unit="m3") weight = MeasurementField(unit="kg") # TODO: documents = models.ManyToManyField(FilerFile) class Meta: verbose_name = _('shipment') verbose_name_plural = _('shipments') def __repr__(self): # pragma: no cover return "<Shipment %s for order %s (tracking %r, created %s)>" % ( self.pk, self.order_id, self.tracking_code, self.created_on ) def cache_values(self): """ (Re)cache `.volume` and `.weight` for this Shipment from the ShipmentProducts within. """ total_volume = 0 total_weight = 0 for quantity, volume, weight in self.products.values_list("quantity", "unit_volume", "unit_weight"): total_volume += quantity * volume total_weight += quantity * weight self.volume = total_volume self.weight = total_weight @property def total_products(self): return (self.products.aggregate(quantity=models.Sum("quantity"))["quantity"] or 0)
class WeightBasedPriceRange(TranslatableModel): component = models.ForeignKey("WeightBasedPricingBehaviorComponent", related_name="ranges", on_delete=models.CASCADE) min_value = MeasurementField(unit="g", verbose_name=_("min weight"), blank=True, null=True) max_value = MeasurementField(unit="g", verbose_name=_("max weight"), blank=True, null=True) price_value = MoneyValueField() description = TranslatedField(any_language=True) translations = TranslatedFields(description=models.CharField( max_length=100, blank=True, verbose_name=_("description")), ) def matches_to_value(self, value): return _is_in_range(value, self.min_value, self.max_value)
def test_measurement_field_doesnt_know_bananas(): with pytest.raises(ImproperlyConfigured): scale = MeasurementField(unit="banana")
class Product(AttributableMixin, TranslatableModel): COMMON_SELECT_RELATED = ("type", "primary_image", "tax_class") # Metadata created_on = models.DateTimeField(auto_now_add=True, editable=False) modified_on = models.DateTimeField(auto_now=True, editable=False) deleted = models.BooleanField(default=False, editable=False, db_index=True) # Behavior mode = EnumIntegerField(ProductMode, default=ProductMode.NORMAL) variation_parent = models.ForeignKey( "self", null=True, blank=True, related_name='variation_children', on_delete=models.PROTECT, verbose_name=_('variation parent')) stock_behavior = EnumIntegerField(StockBehavior, default=StockBehavior.UNSTOCKED, verbose_name=_('stock')) shipping_mode = EnumIntegerField(ShippingMode, default=ShippingMode.NOT_SHIPPED, verbose_name=_('shipping mode')) sales_unit = models.ForeignKey("SalesUnit", verbose_name=_('unit'), blank=True, null=True) tax_class = models.ForeignKey("TaxClass", verbose_name=_('tax class')) # Identification type = models.ForeignKey( "ProductType", related_name='products', on_delete=models.PROTECT, db_index=True, verbose_name=_('product type')) sku = models.CharField(db_index=True, max_length=128, verbose_name=_('SKU'), unique=True) gtin = models.CharField(blank=True, max_length=40, verbose_name=_('GTIN'), help_text=_('Global Trade Item Number')) barcode = models.CharField(blank=True, max_length=40, verbose_name=_('barcode')) accounting_identifier = models.CharField(max_length=32, blank=True, verbose_name=_('bookkeeping account')) profit_center = models.CharField(max_length=32, verbose_name=_('profit center'), blank=True) cost_center = models.CharField(max_length=32, verbose_name=_('cost center'), blank=True) # Category is duplicated here because not all products necessarily belong in Shops (i.e. have # ShopProduct instances), but they should nevertheless be searchable by category in other # places, such as administration UIs. category = models.ForeignKey( "Category", related_name='primary_products', blank=True, null=True, verbose_name=_('primary category'), help_text=_("only used for administration and reporting")) # Physical dimensions width = MeasurementField(unit="mm", verbose_name=_('width (mm)')) height = MeasurementField(unit="mm", verbose_name=_('height (mm)')) depth = MeasurementField(unit="mm", verbose_name=_('depth (mm)')) net_weight = MeasurementField(unit="g", verbose_name=_('net weight (g)')) gross_weight = MeasurementField(unit="g", verbose_name=_('gross weight (g)')) # Misc. purchase_price = MoneyField(verbose_name=_('purchase price')) suggested_retail_price = MoneyField(verbose_name=_('suggested retail price')) manufacturer = models.ForeignKey("Manufacturer", blank=True, null=True, verbose_name=_('manufacturer')) primary_image = models.ForeignKey( "ProductMedia", null=True, blank=True, related_name="primary_image_for_products", on_delete=models.SET_NULL) translations = TranslatedFields( name=models.CharField(max_length=256, verbose_name=_('name')), description=models.TextField(blank=True, verbose_name=_('description')), slug=models.SlugField(verbose_name=_('slug'), max_length=255, null=True), keywords=models.TextField(blank=True, verbose_name=_('keywords')), status_text=models.CharField( max_length=128, blank=True, verbose_name=_('status text'), help_text=_( 'This text will be shown alongside the product in the shop.' ' (Ex.: "Available in a month")')), variation_name=models.CharField( max_length=128, blank=True, verbose_name=_('variation name')) ) objects = ProductQuerySet.as_manager() class Meta: ordering = ('-id',) verbose_name = _('product') verbose_name_plural = _('products') def __str__(self): try: return u"%s" % self.name except ObjectDoesNotExist: return self.sku def get_shop_instance(self, shop): """ :type shop: shoop.core.models.shops.Shop :rtype: shoop.core.models.product_shops.ShopProduct """ shop_inst_cache = self.__dict__.setdefault("_shop_inst_cache", {}) cached = shop_inst_cache.get(shop) if cached: return cached shop_inst = self.shop_products.filter(shop=shop).first() if shop_inst: shop_inst._product_cache = self shop_inst._shop_cache = shop shop_inst_cache[shop] = shop_inst return shop_inst def get_cheapest_child_price(self, context, quantity=1): return sorted( c.get_price(context, quantity=quantity) for c in self.variation_children.all() )[0] def get_price(self, context, quantity=1): """ :type context: shoop.core.contexts.PriceTaxContext :rtype: shoop.core.pricing.Price """ from shoop.core.pricing import get_pricing_module module = get_pricing_module() pricing_context = module.get_context(context) return module.get_price(pricing_context, product_id=self.pk, quantity=quantity) def get_base_price(self): from shoop.core.pricing import get_pricing_module module = get_pricing_module() return module.get_base_price(product_id=self.pk) def get_taxed_price(self, context, quantity=1): """ :type context: shoop.core.contexts.PriceTaxContext :rtype: shoop.core.pricing.TaxedPrice """ from shoop.core import taxing module = taxing.get_tax_module() return module.determine_product_tax(context, self) def get_available_attribute_queryset(self): if self.type_id: return self.type.attributes.visible() else: return Attribute.objects.none() @staticmethod def _get_slug_name(self): if self.deleted: return None return (self.safe_translation_getter("name") or self.sku) def save(self, *args, **kwargs): if self.net_weight and self.net_weight > 0: self.gross_weight = max(self.net_weight, self.gross_weight) rv = super(Product, self).save(*args, **kwargs) generate_multilanguage_slugs(self, self._get_slug_name) return rv def delete(self, using=None): raise NotImplementedError("Not implemented: Use `soft_delete()` for products.") def soft_delete(self, user=None): if not self.deleted: self.deleted = True self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user) # Bypassing local `save()` on purpose. super(Product, self).save(update_fields=("deleted",)) def verify_mode(self): if ProductPackageLink.objects.filter(parent=self).count(): self.mode = ProductMode.PACKAGE_PARENT self.external_url = None self.variation_children.clear() elif self.variation_children.count(): if ProductVariationResult.objects.filter(product=self).count(): self.mode = ProductMode.VARIABLE_VARIATION_PARENT else: self.mode = ProductMode.SIMPLE_VARIATION_PARENT self.external_url = None ProductPackageLink.objects.filter(parent=self).delete() elif self.variation_parent: self.mode = ProductMode.VARIATION_CHILD ProductPackageLink.objects.filter(parent=self).delete() self.variation_children.clear() self.external_url = None else: self.mode = ProductMode.NORMAL def unlink_from_parent(self): if self.variation_parent: parent = self.variation_parent self.variation_parent = None self.save() parent.verify_mode() self.verify_mode() self.save() ProductVariationResult.objects.filter(result=self).delete() return True def link_to_parent(self, parent, variables=None): if parent.mode == ProductMode.VARIATION_CHILD: raise ValueError("Multilevel parentage hierarchies aren't supported (parent is a child already)") if parent.mode == ProductMode.VARIABLE_VARIATION_PARENT and not variables: raise ValueError("Parent is a variable variation parent, yet variables were not passed to `link_to_parent`") if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT and variables: raise ValueError("Parent is a simple variation parent, yet variables were passed to `link_to_parent`") if self.mode == ProductMode.SIMPLE_VARIATION_PARENT: raise ValueError( "Multilevel parentage hierarchies aren't supported (this product is a simple variation parent)" ) if self.mode == ProductMode.VARIABLE_VARIATION_PARENT: raise ValueError( "Multilevel parentage hierarchies aren't supported (this product is a variable variation parent)" ) self.unlink_from_parent() self.variation_parent = parent self.verify_mode() self.save() if parent.mode not in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT): parent.verify_mode() parent.save() if variables: mapping = {} for variable_identifier, value_identifier in variables.items(): variable_identifier, _ = ProductVariationVariable.objects.get_or_create( product=parent, identifier=variable_identifier ) value_identifier, _ = ProductVariationVariableValue.objects.get_or_create( variable=variable_identifier, identifier=value_identifier ) mapping[variable_identifier] = value_identifier pvr = ProductVariationResult.objects.create( product=parent, combination_hash=hash_combination(mapping), result=self ) if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT: parent.verify_mode() parent.save() return pvr else: return True def make_package(self, package_def): if self.mode != ProductMode.NORMAL: raise ValueError("Product is currently not a normal product, can't turn into package") for child_product, quantity in six.iteritems(package_def): if child_product.mode in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT): raise ValueError("Variation parents can not belong into a package") if child_product.mode == ProductMode.PACKAGE_PARENT: raise ValueError("Can't nest packages") if quantity <= 0: raise ValueError("Quantity %s is invalid" % quantity) ProductPackageLink.objects.create(parent=self, child=child_product, quantity=quantity) self.verify_mode() def get_package_child_to_quantity_map(self): if self.mode == ProductMode.PACKAGE_PARENT: product_id_to_quantity = dict( ProductPackageLink.objects.filter(parent=self).values_list("child_id", "quantity") ) products = dict((p.pk, p) for p in Product.objects.filter(pk__in=product_id_to_quantity.keys())) return {products[product_id]: quantity for (product_id, quantity) in six.iteritems(product_id_to_quantity)} return {}
class Product(TaxableItem, AttributableMixin, TranslatableModel): COMMON_SELECT_RELATED = ("type", "primary_image", "tax_class") # Metadata created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on')) modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on')) deleted = models.BooleanField(default=False, editable=False, db_index=True, verbose_name=_('deleted')) # Behavior mode = EnumIntegerField(ProductMode, default=ProductMode.NORMAL, verbose_name=_('mode')) variation_parent = models.ForeignKey("self", null=True, blank=True, related_name='variation_children', on_delete=models.PROTECT, verbose_name=_('variation parent')) stock_behavior = EnumIntegerField(StockBehavior, default=StockBehavior.UNSTOCKED, verbose_name=_('stock')) shipping_mode = EnumIntegerField(ShippingMode, default=ShippingMode.NOT_SHIPPED, verbose_name=_('shipping mode')) sales_unit = models.ForeignKey("SalesUnit", verbose_name=_('unit'), blank=True, null=True, on_delete=models.PROTECT) tax_class = models.ForeignKey("TaxClass", verbose_name=_('tax class'), on_delete=models.PROTECT) # Identification type = models.ForeignKey("ProductType", related_name='products', on_delete=models.PROTECT, db_index=True, verbose_name=_('product type')) sku = models.CharField(db_index=True, max_length=128, verbose_name=_('SKU'), unique=True) gtin = models.CharField(blank=True, max_length=40, verbose_name=_('GTIN'), help_text=_('Global Trade Item Number')) barcode = models.CharField(blank=True, max_length=40, verbose_name=_('barcode')) accounting_identifier = models.CharField( max_length=32, blank=True, verbose_name=_('bookkeeping account')) profit_center = models.CharField(max_length=32, verbose_name=_('profit center'), blank=True) cost_center = models.CharField(max_length=32, verbose_name=_('cost center'), blank=True) # Category is duplicated here because not all products necessarily belong in Shops (i.e. have # ShopProduct instances), but they should nevertheless be searchable by category in other # places, such as administration UIs. category = models.ForeignKey( "Category", related_name='primary_products', blank=True, null=True, verbose_name=_('primary category'), help_text=_("only used for administration and reporting"), on_delete=models.PROTECT) # Physical dimensions width = MeasurementField(unit="mm", verbose_name=_('width (mm)')) height = MeasurementField(unit="mm", verbose_name=_('height (mm)')) depth = MeasurementField(unit="mm", verbose_name=_('depth (mm)')) net_weight = MeasurementField(unit="g", verbose_name=_('net weight (g)')) gross_weight = MeasurementField(unit="g", verbose_name=_('gross weight (g)')) # Misc. manufacturer = models.ForeignKey("Manufacturer", blank=True, null=True, verbose_name=_('manufacturer'), on_delete=models.PROTECT) primary_image = models.ForeignKey( "ProductMedia", null=True, blank=True, related_name="primary_image_for_products", on_delete=models.SET_NULL, verbose_name=_("primary image")) translations = TranslatedFields( name=models.CharField(max_length=256, verbose_name=_('name')), description=models.TextField(blank=True, verbose_name=_('description')), slug=models.SlugField(verbose_name=_('slug'), max_length=255, null=True), keywords=models.TextField(blank=True, verbose_name=_('keywords')), status_text=models.CharField( max_length=128, blank=True, verbose_name=_('status text'), help_text=_( 'This text will be shown alongside the product in the shop.' ' (Ex.: "Available in a month")')), variation_name=models.CharField(max_length=128, blank=True, verbose_name=_('variation name'))) objects = ProductQuerySet.as_manager() class Meta: ordering = ('-id', ) verbose_name = _('product') verbose_name_plural = _('products') def __str__(self): try: return u"%s" % self.name except ObjectDoesNotExist: return self.sku def get_shop_instance(self, shop): """ :type shop: shoop.core.models.Shop :rtype: shoop.core.models.ShopProduct """ shop_inst_cache = self.__dict__.setdefault("_shop_inst_cache", {}) cached = shop_inst_cache.get(shop) if cached: return cached shop_inst = self.shop_products.get(shop=shop) shop_inst._product_cache = self shop_inst._shop_cache = shop shop_inst_cache[shop] = shop_inst return shop_inst def get_cheapest_child_price(self, context, quantity=1): price_info = self.get_cheapest_child_price_info(context, quantity) if price_info: return price_info.price def get_child_price_range(self, context, quantity=1): """ Get the prices for cheapest and the most expensive child The attribute used for sorting is `PriceInfo.price`. Return (`None`, `None`) if `self.variation_children` do not exist. This is because we cannot return anything sensible. :type context: shoop.core.pricing.PricingContextable :type quantity: int :return: a tuple of prices :rtype: (shoop.core.pricing.Price, shoop.core.pricing.Price) """ items = [ c.get_price_info(context, quantity=quantity) for c in self.variation_children.all() ] if not items: return (None, None) infos = sorted(items, key=lambda x: x.price) return (infos[0].price, infos[-1].price) def get_cheapest_child_price_info(self, context, quantity=1): """ Get the `PriceInfo` of the cheapest variation child The attribute used for sorting is `PriceInfo.price`. Return `None` if `self.variation_children` do not exist. This is because we cannot return anything sensible. :type context: shoop.core.pricing.PricingContextable :rtype: shoop.core.pricing.PriceInfo """ items = [ c.get_price_info(context, quantity=quantity) for c in self.variation_children.all() ] if not items: return None return sorted(items, key=lambda x: x.price)[0] def get_price_info(self, context, quantity=1): """ Get `PriceInfo` object for the product in given context. Returned `PriceInfo` object contains calculated `price` and `base_price`. The calculation of prices is handled in the current pricing module. :type context: shoop.core.pricing.PricingContextable :rtype: shoop.core.pricing.PriceInfo """ from shoop.core.pricing import get_price_info return get_price_info(product=self, context=context, quantity=quantity) def get_price(self, context, quantity=1): """ Get price of the product within given context. .. note:: When the current pricing module implements pricing steps, it is possible that ``p.get_price(ctx) * 123`` is not equal to ``p.get_price(ctx, quantity=123)``, since there could be quantity discounts in effect, but usually they are equal. :type context: shoop.core.pricing.PricingContextable :rtype: shoop.core.pricing.Price """ return self.get_price_info(context, quantity).price def get_base_price(self, context, quantity=1): """ Get base price of the product within given context. Base price differs from the (effective) price when there are discounts in effect. :type context: shoop.core.pricing.PricingContextable :rtype: shoop.core.pricing.Price """ return self.get_price_info(context, quantity=quantity).base_price def get_available_attribute_queryset(self): if self.type_id: return self.type.attributes.visible() else: return Attribute.objects.none() def get_available_variation_results(self): """ Get a dict of `combination_hash` to product ID of variable variation results. :return: Mapping of combination hashes to product IDs :rtype: dict[str, int] """ return dict( ProductVariationResult.objects.filter(product=self).filter( status=1).values_list("combination_hash", "result_id")) def get_all_available_combinations(self): """ Generate all available combinations of variation variables. If the product is not a variable variation parent, the iterator is empty. Because of possible combinatorial explosion this is a generator function. (For example 6 variables with 5 options each explodes to 15,625 combinations.) :return: Iterable of combination information dicts. :rtype: Iterable[dict] """ return get_all_available_combinations(self) def clear_variation(self): """ Fully remove variation information. Make this product a non-variation parent. """ self.simplify_variation() for child in self.variation_children.all(): if child.variation_parent_id == self.pk: child.unlink_from_parent() self.verify_mode() self.save() def simplify_variation(self): """ Remove variation variables from the given variation parent, turning it into a simple variation (or a normal product, if it has no children). :param product: Variation parent to not be variable any longer. :type product: shoop.core.models.Product """ ProductVariationVariable.objects.filter(product=self).delete() ProductVariationResult.objects.filter(product=self).delete() self.verify_mode() self.save() @staticmethod def _get_slug_name(self): if self.deleted: return None return (self.safe_translation_getter("name") or self.sku) def save(self, *args, **kwargs): if self.net_weight and self.net_weight > 0: self.gross_weight = max(self.net_weight, self.gross_weight) rv = super(Product, self).save(*args, **kwargs) generate_multilanguage_slugs(self, self._get_slug_name) return rv def delete(self, using=None): raise NotImplementedError( "Not implemented: Use `soft_delete()` for products.") def soft_delete(self, user=None): if not self.deleted: self.deleted = True self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user) # Bypassing local `save()` on purpose. super(Product, self).save(update_fields=("deleted", )) def verify_mode(self): if ProductPackageLink.objects.filter(parent=self).exists(): self.mode = ProductMode.PACKAGE_PARENT self.external_url = None self.variation_children.clear() elif ProductVariationVariable.objects.filter(product=self).exists(): self.mode = ProductMode.VARIABLE_VARIATION_PARENT elif self.variation_children.exists(): if ProductVariationResult.objects.filter(product=self).exists(): self.mode = ProductMode.VARIABLE_VARIATION_PARENT else: self.mode = ProductMode.SIMPLE_VARIATION_PARENT self.external_url = None ProductPackageLink.objects.filter(parent=self).delete() elif self.variation_parent: self.mode = ProductMode.VARIATION_CHILD ProductPackageLink.objects.filter(parent=self).delete() self.variation_children.clear() self.external_url = None else: self.mode = ProductMode.NORMAL def unlink_from_parent(self): if self.variation_parent: parent = self.variation_parent self.variation_parent = None self.save() parent.verify_mode() self.verify_mode() self.save() ProductVariationResult.objects.filter(result=self).delete() return True def link_to_parent(self, parent, variables=None, combination_hash=None): """ :param parent: The parent to link to. :type parent: Product :param variables: Optional dict of {variable identifier: value identifier} for complex variable linkage :type variables: dict|None :param combination_hash: Optional combination hash (for variable variations), if precomputed. Mutually exclusive with `variables` :type combination_hash: str|None """ if combination_hash: if variables: raise ValueError( "`combination_hash` and `variables` are mutually exclusive" ) variables = True # Simplifies the below invariant checks self._raise_if_cant_link_to_parent(parent, variables) self.unlink_from_parent() self.variation_parent = parent self.verify_mode() self.save() if not parent.is_variation_parent(): parent.verify_mode() parent.save() if variables: if not combination_hash: # No precalculated hash, need to figure that out combination_hash = get_combination_hash_from_variable_mapping( parent, variables=variables) pvr = ProductVariationResult.objects.create( product=parent, combination_hash=combination_hash, result=self) if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT: parent.verify_mode() parent.save() return pvr else: return True def _raise_if_cant_link_to_parent(self, parent, variables): """ Validates relation possibility for `self.link_to_parent()` :param parent: parent product of self :type parent: Product :param variables: :type variables: dict|None """ if parent.is_variation_child(): raise ImpossibleProductModeException(_( "Multilevel parentage hierarchies aren't supported (parent is a child already)" ), code="multilevel") if parent.mode == ProductMode.VARIABLE_VARIATION_PARENT and not variables: raise ImpossibleProductModeException(_( "Parent is a variable variation parent, yet variables were not passed" ), code="no_variables") if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT and variables: raise ImpossibleProductModeException( "Parent is a simple variation parent, yet variables were passed", code="extra_variables") if self.mode == ProductMode.SIMPLE_VARIATION_PARENT: raise ImpossibleProductModeException(_( "Multilevel parentage hierarchies aren't supported (this product is a simple variation parent)" ), code="multilevel") if self.mode == ProductMode.VARIABLE_VARIATION_PARENT: raise ImpossibleProductModeException(_( "Multilevel parentage hierarchies aren't supported (this product is a variable variation parent)" ), code="multilevel") def make_package(self, package_def): if self.mode != ProductMode.NORMAL: raise ImpossibleProductModeException(_( "Product is currently not a normal product, can't turn into package" ), code="abnormal") for child_product, quantity in six.iteritems(package_def): # :type child_product: Product if child_product.is_variation_parent(): raise ImpossibleProductModeException( _("Variation parents can not belong into a package"), code="abnormal") if child_product.is_package_parent(): raise ImpossibleProductModeException( _("Packages can't be nested"), code="multilevel") if quantity <= 0: raise ImpossibleProductModeException( _("Quantity %s is invalid") % quantity, code="quantity") ProductPackageLink.objects.create(parent=self, child=child_product, quantity=quantity) self.verify_mode() def get_package_child_to_quantity_map(self): if self.is_package_parent(): product_id_to_quantity = dict( ProductPackageLink.objects.filter(parent=self).values_list( "child_id", "quantity")) products = dict((p.pk, p) for p in Product.objects.filter( pk__in=product_id_to_quantity.keys())) return { products[product_id]: quantity for (product_id, quantity) in six.iteritems(product_id_to_quantity) } return {} def is_variation_parent(self): return self.mode in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT) def is_variation_child(self): return (self.mode == ProductMode.VARIATION_CHILD) def get_variation_siblings(self): return Product.objects.filter( variation_parent=self.variation_parent).exclude(pk=self.pk) def is_package_parent(self): return (self.mode == ProductMode.PACKAGE_PARENT) def is_package_child(self): return ProductPackageLink.objects.filter(child=self).exists() def get_all_package_parents(self): return Product.objects.filter( pk__in=(ProductPackageLink.objects.filter( child=self).values_list("parent", flat=True))) def get_all_package_children(self): return Product.objects.filter( pk__in=(ProductPackageLink.objects.filter( parent=self).values_list("child", flat=True))) def get_public_media(self): return self.media.filter(enabled=True, public=True)
class Shipment(models.Model): order = models.ForeignKey("Order", related_name='shipments', on_delete=models.PROTECT, verbose_name=_("order")) supplier = models.ForeignKey("Supplier", related_name='shipments', on_delete=models.PROTECT, verbose_name=_("supplier")) created_on = models.DateTimeField(auto_now_add=True, verbose_name=_("created on")) status = EnumIntegerField(ShipmentStatus, default=ShipmentStatus.NOT_SENT, verbose_name=_("status")) tracking_code = models.CharField(max_length=64, blank=True, verbose_name=_("tracking code")) description = models.CharField(max_length=255, blank=True, verbose_name=_("description")) volume = MeasurementField(unit="m3", verbose_name=_("volume")) weight = MeasurementField(unit="kg", verbose_name=_("weight")) identifier = InternalIdentifierField(unique=True) # TODO: documents = models.ManyToManyField(FilerFile) class Meta: verbose_name = _('shipment') verbose_name_plural = _('shipments') def __init__(self, *args, **kwargs): super(Shipment, self).__init__(*args, **kwargs) if not self.identifier: if self.order and self.order.pk: prefix = '%s/%s/' % (self.order.pk, self.order.shipments.count()) else: prefix = '' self.identifier = prefix + get_random_string(32) def __repr__(self): # pragma: no cover return "<Shipment %s for order %s (tracking %r, created %s)>" % ( self.pk, self.order_id, self.tracking_code, self.created_on) def save(self, *args, **kwargs): super(Shipment, self).save(*args, **kwargs) for product_id in self.products.values_list("product_id", flat=True): self.supplier.module.update_stock(product_id=product_id) def cache_values(self): """ (Re)cache `.volume` and `.weight` for this Shipment from the ShipmentProducts within. """ total_volume = 0 total_weight = 0 for quantity, volume, weight in self.products.values_list( "quantity", "unit_volume", "unit_weight"): total_volume += quantity * volume total_weight += quantity * weight self.volume = total_volume self.weight = total_weight / GRAMS_TO_KILOGRAMS_DIVISOR @property def total_products(self): return (self.products.aggregate( quantity=models.Sum("quantity"))["quantity"] or 0)