private async Task <ImageModel> PrepareOrderItemImageModelAsync( Product product, int pictureSize, string productName, ProductVariantAttributeSelection attributeSelection, CatalogSettings catalogSettings) { Guard.NotNull(product, nameof(product)); MediaFileInfo file = null; var combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(product.Id, attributeSelection); if (combination != null) { var mediaIds = combination.GetAssignedMediaIds(); if (mediaIds.Any()) { file = await _mediaService.GetFileByIdAsync(mediaIds[0], MediaLoadFlags.AsNoTracking); } } // No attribute combination image, then load product picture. if (file == null) { var mediaFile = await _db.ProductMediaFiles .AsNoTracking() .Include(x => x.MediaFile) .ApplyProductFilter(product.Id) .FirstOrDefaultAsync(); if (mediaFile?.MediaFile != null) { file = _mediaService.ConvertMediaFile(mediaFile.MediaFile); } } // Let's check whether this product has some parent "grouped" product. if (file == null && product.Visibility == ProductVisibility.Hidden && product.ParentGroupedProductId > 0) { var mediaFile = await _db.ProductMediaFiles .AsNoTracking() .Include(x => x.MediaFile) .ApplyProductFilter(product.ParentGroupedProductId) .FirstOrDefaultAsync(); if (mediaFile?.MediaFile != null) { file = _mediaService.ConvertMediaFile(mediaFile.MediaFile); } } return(new ImageModel { File = file, ThumbSize = pictureSize, Title = file?.File?.GetLocalized(x => x.Title)?.Value.NullEmpty() ?? T("Media.Product.ImageLinkTitleFormat", productName), Alt = file?.File?.GetLocalized(x => x.Alt)?.Value.NullEmpty() ?? T("Media.Product.ImageAlternateTextFormat", productName), NoFallback = catalogSettings.HideProductDefaultPictures }); }
/// <summary> /// Finds and returns first matching product from shopping cart. /// </summary> /// <remarks> /// Products with the same identifier need to have matching attribute selections as well. /// </remarks> /// <param name="cart">Shopping cart to search in.</param> /// <param name="shoppingCartType">Shopping cart type to search in.</param> /// <param name="product">Product to search for.</param> /// <param name="selection">Attribute selection.</param> /// <param name="customerEnteredPrice">Customers entered price needs to match (if enabled by product).</param> /// <returns>Matching <see cref="OrganizedShoppingCartItem"/> or <c>null</c> if none was found.</returns> public static OrganizedShoppingCartItem FindItemInCart( this IList <OrganizedShoppingCartItem> cart, ShoppingCartType shoppingCartType, Product product, ProductVariantAttributeSelection selection, Money customerEnteredPrice) { Guard.NotNull(cart, nameof(cart)); Guard.NotNull(product, nameof(product)); // Return on product bundle with individual item pricing - too complex if (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing) { return(null); } // Filter non group items from correct cart type, with matching product id and product type id var filteredCart = cart .Where(x => x.Item.ShoppingCartType == shoppingCartType && x.Item.ParentItemId == null && x.Item.Product.ProductTypeId == product.ProductTypeId && x.Item.ProductId == product.Id); // There could be multiple matching products with the same identifier but different attributes/selections (etc). // Ensure matching product infos are the same (attributes, gift card values (if it is gift card), customerEnteredPrice). foreach (var cartItem in filteredCart) { // Compare attribute selection var cartItemSelection = cartItem.Item.AttributeSelection; if (cartItemSelection != selection) { continue; } var currentProduct = cartItem.Item.Product; // Compare gift cards info values (if it is a gift card) if (currentProduct.IsGiftCard && (cartItemSelection.GiftCardInfo == null || selection.GiftCardInfo == null || cartItemSelection != selection)) { continue; } // Products with CustomerEntersPrice are equal if the price is the same. // But a system product may only be placed once in the shopping cart. if (currentProduct.CustomerEntersPrice && !currentProduct.IsSystemProduct && customerEnteredPrice.RoundedAmount != decimal.Round(cartItem.Item.CustomerEnteredPrice, customerEnteredPrice.DecimalDigits)) { continue; } // If we got this far, we found a matching product with the same values return(cartItem); } return(null); }
internal async Task <decimal> GetCartItemsAttributesWeightAsync(IList <OrganizedShoppingCartItem> cart, bool multipliedByQuantity = true) { Guard.NotNull(cart, nameof(cart)); var rawAttributes = cart .Where(x => x.Item.RawAttributes.HasValue()) .Select(x => x.Item.RawAttributes); var selection = new ProductVariantAttributeSelection(string.Empty); foreach (var cartItem in cart) { if (cartItem.Item.RawAttributes.IsEmpty() || cartItem.Item.Product.IsGiftCard) { continue; } var attributeSelection = new ProductVariantAttributeSelection(cartItem.Item.RawAttributes); foreach (var attribute in attributeSelection.AttributesMap) { if (attribute.Value.IsNullOrEmpty()) { continue; } selection.AddAttribute(attribute.Key, attribute.Value.ToArray()); } } var attributeValueIds = selection.GetAttributeValueIds(); // Gets either all values of attributes without a product linkage // or linked products which are shipping enabled var query = _db.ProductVariantAttributeValues .Include(x => x.ProductVariantAttribute) .ThenInclude(x => x.Product) .ApplyValueFilter(attributeValueIds) .Where(x => x.ValueTypeId == (int)ProductVariantAttributeValueType.ProductLinkage && x.ProductVariantAttribute.Product != null && x.ProductVariantAttribute.Product.IsShippingEnabled || x.ValueTypeId != (int)ProductVariantAttributeValueType.ProductLinkage); // Calculates attributes weight // Get attributes without product linkage > add attribute weight adjustment var attributesWeight = await query .Where(x => x.ValueTypeId != (int)ProductVariantAttributeValueType.ProductLinkage) // TODO: (ms) (core) Test possible SumAsync SQL projection failure (IIF) .SumAsync(x => x.WeightAdjustment * (multipliedByQuantity ? x.Quantity : 1)); // TODO: (ms) (core) needs to be tested with NullResult // Get attributes with product linkage > add product weigth attributesWeight += await query .Where(x => x.ValueTypeId == (int)ProductVariantAttributeValueType.ProductLinkage) .Select(x => new { x.ProductVariantAttribute.Product, x.Quantity }) .Where(x => x.Product != null && x.Product.IsShippingEnabled) .SumAsync(x => x.Product.Weight * x.Quantity); return(attributesWeight); }
/// <summary> /// Ctor. /// </summary> /// <param name="selection">The selected product attributes.</param> /// <param name="productId">The identifier of the related product.</param> public PriceCalculationAttributes(ProductVariantAttributeSelection selection, int productId) { Guard.NotNull(selection, nameof(selection)); Guard.NotZero(productId, nameof(productId)); Selection = selection; ProductId = productId; }
public virtual Task <IList <OrganizedShoppingCartItem> > GetCartItemsAsync( Customer customer = null, ShoppingCartType cartType = ShoppingCartType.ShoppingCart, int storeId = 0) { customer ??= _workContext.CurrentCustomer; var cacheKey = CartItemsKey.FormatInvariant(customer.Id, (int)cartType, storeId); var result = _requestCache.Get(cacheKey, async() => { var cartItems = new List <ShoppingCartItem>(); // TODO: (ms) (core) Do we need to check for ShoppingCartItems.Product.ProductVariantAttribute is loaded too? Would this direct access be even possible then? if (_db.IsCollectionLoaded(customer, x => x.ShoppingCartItems)) { var filteredCartItems = customer.ShoppingCartItems .Where(x => x.CustomerId == customer.Id && x.ShoppingCartTypeId == (int)cartType); if (storeId > 0) { filteredCartItems = cartItems.Where(x => x.StoreId == storeId); } cartItems = filteredCartItems.ToList(); } else { // TODO: (core) Re-apply data to Customer.ShoppingCartItems collection to prevent reloads. cartItems = await _db.ShoppingCartItems .Include(x => x.Product) .ThenInclude(x => x.ProductVariantAttributes) .ApplyStandardFilter(cartType, storeId, customer) .ToListAsync(); } // Prefetch all product variant attributes var allAttributes = new ProductVariantAttributeSelection(string.Empty); var allAttributeMaps = cartItems.SelectMany(x => x.AttributeSelection.AttributesMap); foreach (var attribute in allAttributeMaps) { if (allAttributes.AttributesMap.Contains(attribute)) { continue; } allAttributes.AddAttribute(attribute.Key, attribute.Value); } // TODO: (ms) (core) Check if this is sufficient and good prefetch -> what about caching or skipping already loaded? await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(allAttributes); return(await OrganizeCartItemsAsync(cartItems)); }); return(result); }
public virtual Task <List <OrganizedShoppingCartItem> > GetCartItemsAsync( Customer customer = null, ShoppingCartType cartType = ShoppingCartType.ShoppingCart, int storeId = 0) { customer ??= _workContext.CurrentCustomer; var cacheKey = CartItemsKey.FormatInvariant(customer.Id, (int)cartType, storeId); var result = _requestCache.Get(cacheKey, async() => { var cartItems = new List <ShoppingCartItem>(); if (_db.IsCollectionLoaded(customer, x => x.ShoppingCartItems)) { var filteredCartItems = customer.ShoppingCartItems .Where(x => x.CustomerId == customer.Id && x.ShoppingCartTypeId == (int)cartType); if (storeId > 0) { filteredCartItems = cartItems.Where(x => x.StoreId == storeId); } cartItems = filteredCartItems.ToList(); } else { cartItems = await _db.ShoppingCartItems .Include(x => x.Product) .ThenInclude(x => x.ProductVariantAttributes) .ApplyStandardFilter(cartType, storeId, customer) .ToListAsync(); customer.ShoppingCartItems = cartItems; } // Prefetch all product variant attributes var allAttributes = new ProductVariantAttributeSelection(string.Empty); var allAttributeMaps = cartItems.SelectMany(x => x.AttributeSelection.AttributesMap); foreach (var attribute in allAttributeMaps) { if (allAttributes.AttributesMap.Contains(attribute)) { continue; } allAttributes.AddAttribute(attribute.Key, attribute.Value); } await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(allAttributes); return(await OrganizeCartItemsAsync(cartItems)); }); return(result); }
// TODO: (mg) (core) Describe pricing pipeline when ready. public static void AddAttributes(this PriceCalculationContext context, ProductVariantAttributeSelection selection, int productId, int?bundleItemId = null) { Guard.NotNull(context, nameof(context)); if (selection?.AttributesMap?.Any() ?? false) { context.Attributes.Add(new PriceCalculationAttributes(selection, productId) { BundleItemId = bundleItemId }); } }
protected virtual async Task <IList <OrganizedShoppingCartItem> > OrganizeCartItemsAsync(ICollection <ShoppingCartItem> cart) { var result = new List <OrganizedShoppingCartItem>(); if (cart.IsNullOrEmpty()) { return(result); } var parents = cart.Where(x => x.ParentItemId is null); // TODO: (ms) (core) to reduce db roundtrips -> load and filter children by parents (id and so on) into lists and try to get from db as batch request foreach (var parent in parents) { var parentItem = new OrganizedShoppingCartItem(parent); var children = cart.Where(x => x.ParentItemId != null && x.ParentItemId == parent.Id && x.Id != parent.Id && x.ShoppingCartTypeId == parent.ShoppingCartTypeId && x.Product.CanBeBundleItem()); // TODO: (ms) (core) Reduce database roundtrips in OrganizeCartItemsAsync foreach (var child in children) { var childItem = new OrganizedShoppingCartItem(child); if (child.RawAttributes.HasValue() && (parent.Product?.BundlePerItemPricing ?? false) && child.BundleItem != null) { var selection = new ProductVariantAttributeSelection(child.RawAttributes); await _productAttributeMaterializer.MergeWithCombinationAsync(child.Product, selection); var attributeValues = await _productAttributeMaterializer .MaterializeProductVariantAttributeValuesAsync(selection); if (!attributeValues.IsNullOrEmpty()) { childItem.BundleItemData.AdditionalCharge += attributeValues.Sum(x => x.PriceAdjustment); } } parentItem.ChildItems.Add(childItem); } result.Add(parentItem); } return(result); }
/// <summary> /// Creates an absolute product URL. /// </summary> /// <param name="productId">Product identifier.</param> /// <param name="productSlug">Product URL slug.</param> /// <param name="selection">Selected attributes.</param> /// <param name="store">Store.</param> /// <param name="language">Language.</param> /// <returns>Absolute product URL.</returns> public virtual async Task <string> GetAbsoluteProductUrlAsync( int productId, string productSlug, ProductVariantAttributeSelection selection = null, Store store = null, Language language = null) { var request = _httpContextAccessor?.HttpContext?.Request; if (request == null || productSlug.IsEmpty()) { return(null); } var url = Url; if (url.IsEmpty()) { store ??= _storeContext.CurrentStore; language ??= _workContext.WorkingLanguage; string hostName = null; try { // Do not create crappy URLs (exclude scheme, include port and no slash)! hostName = new Uri(store.GetHost(true)).Authority; } catch { } url = _urlHelper.Value.RouteUrl( "Product", new { SeName = productSlug, culture = language.UniqueSeoCode }, store.SupportsHttps() ? "https" : "http", hostName); } if (selection?.AttributesMap?.Any() ?? false) { var query = new ProductVariantQuery(); await AddAttributesToQueryAsync(query, selection, productId); url = url.TrimEnd('/') + ToQueryString(query); } return(url); }
private async Task <MediaFileInfo> GetMediaFileFor(Product product, ProductVariantAttributeSelection attrSelection = null) { var attrParser = _services.Resolve <IProductAttributeMaterializer>(); var mediaService = _services.Resolve <IMediaService>(); MediaFileInfo file = null; if (attrSelection != null) { var combination = await attrParser.FindAttributeCombinationAsync(product.Id, attrSelection); if (combination != null) { var fileIds = combination.GetAssignedMediaIds(); if (fileIds?.Any() ?? false) { file = await mediaService.GetFileByIdAsync(fileIds[0], MediaLoadFlags.AsNoTracking); } } } if (file == null) { file = await mediaService.GetFileByIdAsync(product.MainPictureId ?? 0, MediaLoadFlags.AsNoTracking); } if (file == null && product.Visibility == ProductVisibility.Hidden && product.ParentGroupedProductId > 0) { var productFile = await _db.ProductMediaFiles .AsNoTracking() .Include(x => x.MediaFile) .ApplyProductFilter(product.ParentGroupedProductId) .FirstOrDefaultAsync(); if (productFile?.MediaFile != null) { file = mediaService.ConvertMediaFile(productFile.MediaFile); } } return(file); }
private async Task <ProductAskQuestionModel> PrepareAskQuestionModelAsync(Product product) { var customer = Services.WorkContext.CurrentCustomer; var rawAttributes = TempData.Peek("AskQuestionAttributeSelection-" + product.Id) as string; // Check if saved rawAttributes belongs to current product id var formattedAttributes = string.Empty; var selection = new ProductVariantAttributeSelection(rawAttributes); if (selection.AttributesMap.Any()) { formattedAttributes = await _productAttributeFormatter.Value.FormatAttributesAsync( selection, product, customer : null, separator : ", ", includePrices : false, includeGiftCardAttributes : false, includeHyperlinks : false); } var seName = await product.GetActiveSlugAsync(); var model = new ProductAskQuestionModel { Id = product.Id, ProductName = product.GetLocalized(x => x.Name), ProductSeName = seName, SenderEmail = customer.Email, SenderName = customer.GetFullName(), SenderNameRequired = _privacySettings.FullNameOnProductRequestRequired, SenderPhone = customer.GenericAttributes.Phone, DisplayCaptcha = _captchaSettings.CanDisplayCaptcha && _captchaSettings.ShowOnAskQuestionPage, SelectedAttributes = formattedAttributes, ProductUrl = await _productUrlHelper.Value.GetProductUrlAsync(product.Id, seName, selection), IsQuoteRequest = product.CallForPrice }; model.Question = T("Products.AskQuestion.Question." + (model.IsQuoteRequest ? "QuoteRequest" : "GeneralInquiry"), model.ProductName); return(model); }
public virtual async Task <Money> CalculateProductCostAsync(Product product, ProductVariantAttributeSelection selection = null) { Guard.NotNull(product, nameof(product)); Guard.NotNull(selection, nameof(selection)); var productCost = product.ProductCost; if (selection != null) { var attributeValues = await _productAttributeMaterializer.MaterializeProductVariantAttributeValuesAsync(selection); var productLinkageValues = attributeValues .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage && x.LinkedProductId != 0) .ToList(); var linkedProductIds = productLinkageValues .Select(x => x.LinkedProductId) .Distinct() .ToArray(); if (linkedProductIds.Any()) { var linkedProducts = await _db.Products .AsNoTracking() .Where(x => linkedProductIds.Contains(x.Id)) .Select(x => new { x.Id, x.ProductCost }) .ToListAsync(); var linkedProductsDic = linkedProducts.ToDictionarySafe(x => x.Id, x => x.ProductCost); foreach (var value in productLinkageValues) { if (linkedProductsDic.TryGetValue(value.LinkedProductId, out var linkedProductCost)) { productCost += linkedProductCost * value.Quantity; } } } } return(new(productCost, _primaryCurrency)); }
/// <summary> /// Calculates the unit price for a given shopping cart item. /// </summary> /// <param name="priceCalculationService">Price calculation service.</param> /// <param name="cartItem">Shopping cart item.</param> /// <param name="ignoreDiscounts">A value indicating whether to ignore discounts.</param> /// <param name="targetCurrency">The target currency to use for money conversion. Obtained from <see cref="IWorkContext.WorkingCurrency"/> if <c>null</c>.</param> /// <returns>Calculated unit price.</returns> //public static async Task<CalculatedPrice> CalculateUnitPriceAsync( // this IPriceCalculationService2 priceCalculationService, // OrganizedShoppingCartItem cartItem, // bool ignoreDiscounts = false, // Currency targetCurrency = null) //{ // Guard.NotNull(priceCalculationService, nameof(priceCalculationService)); // Guard.NotNull(cartItem, nameof(cartItem)); // var options = priceCalculationService.CreateDefaultOptions(false, cartItem.Item.Customer, targetCurrency); // options.IgnoreDiscounts = ignoreDiscounts; // var context = new PriceCalculationContext(cartItem, options); // return await priceCalculationService.CalculatePriceAsync(context); //} /// <summary> /// Calculates both the unit price and the subtotal for a given shopping cart item. /// The subtotal is calculated by multiplying the unit price by <see cref="ShoppingCartItem.Quantity"/>. /// </summary> /// <param name="priceCalculationService">Price calculation service.</param> /// <param name="cartItem">Shopping cart item.</param> /// <param name="ignoreDiscounts">A value indicating whether to ignore discounts.</param> /// <param name="targetCurrency">The target currency to use for money conversion. Obtained from <see cref="IWorkContext.WorkingCurrency"/> if <c>null</c>.</param> /// <returns>Calculated subtotal.</returns> //public static async Task<(CalculatedPrice UnitPrice, CalculatedPrice Subtotal)> CalculateSubtotalAsync( // this IPriceCalculationService2 priceCalculationService, // OrganizedShoppingCartItem cartItem, // bool ignoreDiscounts = false, // Currency targetCurrency = null) //{ // Guard.NotNull(priceCalculationService, nameof(priceCalculationService)); // Guard.NotNull(cartItem, nameof(cartItem)); // var options = priceCalculationService.CreateDefaultOptions(false, cartItem.Item.Customer, targetCurrency); // options.IgnoreDiscounts = ignoreDiscounts; // var context = new PriceCalculationContext(cartItem, options); // return await priceCalculationService.CalculateSubtotalAsync(context); //} /// <summary> /// Calculates the price adjustments of product attributes, usually <see cref="ProductVariantAttributeValue.PriceAdjustment"/>. /// Typically used to display price adjustments of selected attributes on the cart page. /// The calculated adjustment is always a unit price. /// </summary> /// <param name="priceCalculationService">Price calculation service.</param> /// <param name="product">The product.</param> /// <param name="selection">Attribute selection.</param> /// <param name="quantity"> /// The product quantity. May have impact on the price, e.g. if tier prices are applied to price adjustments. /// Note that the calculated price is always the unit price. /// </param> /// <param name="options">Price calculation options. The default options are used if <c>null</c>.</param> /// <returns>Price adjustments of selected attributes. Key: <see cref="ProductVariantAttributeValue.Id"/>, value: attribute price adjustment.</returns> public static async Task <IDictionary <int, CalculatedPriceAdjustment> > CalculateAttributePriceAdjustmentsAsync( this IPriceCalculationService2 priceCalculationService, Product product, ProductVariantAttributeSelection selection, int quantity = 1, PriceCalculationOptions options = null) { Guard.NotNull(priceCalculationService, nameof(priceCalculationService)); Guard.NotNull(selection, nameof(selection)); options ??= priceCalculationService.CreateDefaultOptions(false); options.DeterminePriceAdjustments = true; var pricingContext = new PriceCalculationContext(product, quantity, options); pricingContext.AddSelectedAttributes(selection, product.Id); var price = await priceCalculationService.CalculatePriceAsync(pricingContext); return(price.AttributePriceAdjustments.ToDictionarySafe(x => x.AttributeValue.Id)); }
public virtual Task <List <OrganizedShoppingCartItem> > GetCartItemsAsync( Customer customer = null, ShoppingCartType cartType = ShoppingCartType.ShoppingCart, int storeId = 0) { customer ??= _workContext.CurrentCustomer; var cacheKey = CartItemsKey.FormatInvariant(customer.Id, (int)cartType, storeId); var result = _requestCache.Get(cacheKey, async() => { await _db.LoadCollectionAsync(customer, x => x.ShoppingCartItems, false, x => { return(x .Include(x => x.Product) .ThenInclude(x => x.ProductVariantAttributes)); }); var cartItems = customer.ShoppingCartItems.FilterByCartType(cartType, storeId); // Prefetch all product variant attributes var allAttributes = new ProductVariantAttributeSelection(string.Empty); var allAttributeMaps = cartItems.SelectMany(x => x.AttributeSelection.AttributesMap); foreach (var attribute in allAttributeMaps) { if (allAttributes.AttributesMap.Contains(attribute)) { continue; } allAttributes.AddAttribute(attribute.Key, attribute.Value); } await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(allAttributes); return(await OrganizeCartItemsAsync(cartItems)); }); return(result); }
private async Task ProcessAttributes(DbContextScope scope, Product product, Product clone, IEnumerable <Language> languages) { var localizedKeySelectors = new List <Expression <Func <ProductVariantAttributeValue, string> > > { x => x.Name, x => x.Alias }; await _db.LoadCollectionAsync(product, x => x.ProductVariantAttributes); await _db.LoadCollectionAsync(product, x => x.ProductVariantAttributeCombinations); // Former attribute id > clone. var attributeMap = new Dictionary <int, ProductVariantAttribute>(); // Former attribute value id > clone. var valueMap = new Dictionary <int, ProductVariantAttributeValue>(); var newCombinations = new List <ProductVariantAttributeCombination>(); // Product attributes. foreach (var attribute in product.ProductVariantAttributes) { // Save associated value (used for combinations copying). attributeMap[attribute.Id] = new ProductVariantAttribute { ProductId = clone.Id, ProductAttributeId = attribute.ProductAttributeId, TextPrompt = attribute.TextPrompt, IsRequired = attribute.IsRequired, AttributeControlTypeId = attribute.AttributeControlTypeId, DisplayOrder = attribute.DisplayOrder }; } // Reverse tracking order to have the clones in the same order in the database as the originals. _db.ProductVariantAttributes.AddRange(attributeMap.Select(x => x.Value).Reverse()); // >>>>>> Commit attributes. await scope.CommitAsync(); // Product variant attribute values. foreach (var attribute in product.ProductVariantAttributes) { var attributeClone = attributeMap[attribute.Id]; foreach (var value in attribute.ProductVariantAttributeValues) { // Save associated value (used for combinations copying). valueMap.Add(value.Id, new ProductVariantAttributeValue { ProductVariantAttributeId = attributeClone.Id, Name = value.Name, Color = value.Color, PriceAdjustment = value.PriceAdjustment, WeightAdjustment = value.WeightAdjustment, IsPreSelected = value.IsPreSelected, DisplayOrder = value.DisplayOrder, ValueTypeId = value.ValueTypeId, LinkedProductId = value.LinkedProductId, Quantity = value.Quantity, MediaFileId = value.MediaFileId }); } } // Reverse tracking order to have the clones in the same order in the database as the originals. _db.ProductVariantAttributeValues.AddRange(valueMap.Select(x => x.Value).Reverse()); // >>>>>> Commit attribute values. await scope.CommitAsync(); // Attribute value localization. var allValues = product.ProductVariantAttributes .Reverse() .SelectMany(x => x.ProductVariantAttributeValues.Reverse()) .ToArray(); foreach (var value in allValues) { if (valueMap.TryGetValue(value.Id, out var newValue)) { await ProcessLocalizations(value, newValue, localizedKeySelectors, languages); } } // >>>>>> Commit localized values. await scope.CommitAsync(); // Attribute combinations. foreach (var combination in product.ProductVariantAttributeCombinations) { var oldAttributesMap = combination.AttributeSelection.AttributesMap; var oldAttributes = await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(combination.AttributeSelection); var newSelection = new ProductVariantAttributeSelection(null); foreach (var oldAttribute in oldAttributes) { if (attributeMap.TryGetValue(oldAttribute.Id, out var newAttribute)) { var item = oldAttributesMap.FirstOrDefault(x => x.Key == oldAttribute.Id); if (item.Key != 0) { foreach (var value in item.Value) { if (newAttribute.IsListTypeAttribute()) { var oldValueId = value.ToString().EmptyNull().ToInt(); if (valueMap.TryGetValue(oldValueId, out var newValue)) { newSelection.AddAttributeValue(newAttribute.Id, newValue.Id); } } else { newSelection.AddAttributeValue(newAttribute.Id, value); } } } } } newCombinations.Add(new ProductVariantAttributeCombination { ProductId = clone.Id, RawAttributes = newSelection.AsJson(), StockQuantity = combination.StockQuantity, AllowOutOfStockOrders = combination.AllowOutOfStockOrders, Sku = combination.Sku, Gtin = combination.Gtin, ManufacturerPartNumber = combination.ManufacturerPartNumber, Price = combination.Price, AssignedMediaFileIds = combination.AssignedMediaFileIds, Length = combination.Length, Width = combination.Width, Height = combination.Height, BasePriceAmount = combination.BasePriceAmount, BasePriceBaseAmount = combination.BasePriceBaseAmount, DeliveryTimeId = combination.DeliveryTimeId, QuantityUnitId = combination.QuantityUnitId, IsActive = combination.IsActive //IsDefaultCombination = combination.IsDefaultCombination }); } // Reverse tracking order to have the clones in the same order in the database as the originals. _db.ProductVariantAttributeCombinations.AddRange(newCombinations.AsEnumerable().Reverse()); // >>>>>> Commit combinations. await scope.CommitAsync(); }
protected async Task <ImageModel> PrepareCartItemPictureModelAsync(Product product, int pictureSize, string productName, ProductVariantAttributeSelection attributeSelection) { Guard.NotNull(product, nameof(product)); Guard.NotNull(attributeSelection, nameof(attributeSelection)); MediaFileInfo file = null; var combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(product.Id, attributeSelection); if (combination != null) { var fileIds = combination.GetAssignedMediaIds(); if (fileIds?.Any() ?? false) { file = await _mediaService.GetFileByIdAsync(fileIds[0], MediaLoadFlags.AsNoTracking); } } // No attribute combination image, then load product picture. if (file == null) { var productMediaFile = await _db.ProductMediaFiles .Include(x => x.MediaFile) .Where(x => x.Id == product.Id) .OrderBy(x => x.DisplayOrder) .FirstOrDefaultAsync(); if (productMediaFile != null) { file = _mediaService.ConvertMediaFile(productMediaFile.MediaFile); } } // Let's check whether this product has some parent "grouped" product. if (file == null && product.Visibility == ProductVisibility.Hidden && product.ParentGroupedProductId > 0) { var productMediaFile = await _db.ProductMediaFiles .Include(x => x.MediaFile) .Where(x => x.Id == product.ParentGroupedProductId) .OrderBy(x => x.DisplayOrder) .FirstOrDefaultAsync(); if (productMediaFile != null) { file = _mediaService.ConvertMediaFile(productMediaFile.MediaFile); } } var pm = new ImageModel { Id = file?.Id ?? 0, ThumbSize = pictureSize, Host = _mediaService.GetUrl(file, pictureSize, null, !_catalogSettings.HideProductDefaultPictures), Title = file?.File?.GetLocalized(x => x.Title)?.Value.NullEmpty() ?? T("Media.Product.ImageLinkTitleFormat", productName), Alt = file?.File?.GetLocalized(x => x.Alt)?.Value.NullEmpty() ?? T("Media.Product.ImageAlternateTextFormat", productName), File = file }; return(pm); }
/// <summary> /// Creates a product URL including variant query string. /// </summary> /// <param name="productId">Product identifier.</param> /// <param name="productSlug">Product URL slug.</param> /// <param name="selection">Selected attributes.</param> /// <returns>Product URL.</returns> public virtual async Task <string> GetProductUrlAsync(int productId, string productSlug, ProductVariantAttributeSelection selection) { var query = new ProductVariantQuery(); await AddAttributesToQueryAsync(query, selection, productId); return(GetProductUrl(productSlug, query)); }
/// <summary> /// Adds selected product variant attributes to a product variant query. /// </summary> /// <param name="query">Target product variant query.</param> /// <param name="source">Selected attributes.</param> /// <param name="productId">Product identifier.</param> /// <param name="bundleItemId">Bundle item identifier.</param> /// <param name="attributes">Product variant attributes.</param> public virtual async Task AddAttributesToQueryAsync( ProductVariantQuery query, ProductVariantAttributeSelection source, int productId, int bundleItemId = 0, ICollection <ProductVariantAttribute> attributes = null) { Guard.NotNull(query, nameof(query)); if (productId == 0 || !(source?.AttributesMap?.Any() ?? false)) { return; } if (attributes == null) { var ids = source.AttributesMap.Select(x => x.Key); attributes = await _db.ProductVariantAttributes.GetManyAsync(ids); } var languageId = _workContext.WorkingLanguage.Id; foreach (var attribute in attributes) { var item = source.AttributesMap.FirstOrDefault(x => x.Key == attribute.Id); if (item.Key != 0) { foreach (var originalValue in item.Value) { var value = originalValue.ToString(); DateTime?date = null; if (attribute.AttributeControlType == AttributeControlType.Datepicker) { date = value.ToDateTime(new[] { "D" }, CultureInfo.CurrentCulture, DateTimeStyles.None, null); if (date == null) { continue; } value = string.Join("-", date.Value.Year, date.Value.Month, date.Value.Day); } var queryItem = new ProductVariantQueryItem(value) { ProductId = productId, BundleItemId = bundleItemId, AttributeId = attribute.ProductAttributeId, VariantAttributeId = attribute.Id, Alias = _catalogSearchQueryAliasMapper.Value.GetVariantAliasById(attribute.ProductAttributeId, languageId), Date = date, IsFile = attribute.AttributeControlType == AttributeControlType.FileUpload, IsText = attribute.AttributeControlType == AttributeControlType.TextBox || attribute.AttributeControlType == AttributeControlType.MultilineTextbox }; if (attribute.IsListTypeAttribute()) { queryItem.ValueAlias = _catalogSearchQueryAliasMapper.Value.GetVariantOptionAliasById(value.ToInt(), languageId); } query.AddVariant(queryItem); } } } }
public virtual async Task <AdjustInventoryResult> AdjustInventoryAsync(Product product, ProductVariantAttributeSelection selection, bool decrease, int quantity) { Guard.NotNull(product, nameof(product)); Guard.NotNull(selection, nameof(selection)); var result = new AdjustInventoryResult(); switch (product.ManageInventoryMethod) { case ManageInventoryMethod.ManageStock: { result.StockQuantityOld = product.StockQuantity; result.StockQuantityNew = decrease ? product.StockQuantity - quantity : product.StockQuantity + quantity; var newPublished = product.Published; var newDisableBuyButton = product.DisableBuyButton; var newDisableWishlistButton = product.DisableWishlistButton; // Check if the minimum quantity is reached. switch (product.LowStockActivity) { case LowStockActivity.DisableBuyButton: newDisableBuyButton = product.MinStockQuantity >= result.StockQuantityNew; newDisableWishlistButton = product.MinStockQuantity >= result.StockQuantityNew; break; case LowStockActivity.Unpublish: newPublished = product.MinStockQuantity <= result.StockQuantityNew; break; } product.StockQuantity = result.StockQuantityNew; product.DisableBuyButton = newDisableBuyButton; product.DisableWishlistButton = newDisableWishlistButton; product.Published = newPublished; // Commit required because of store owner notification. await _db.SaveChangesAsync(); if (decrease && product.NotifyAdminForQuantityBelow > result.StockQuantityNew) { await _messageFactory.SendQuantityBelowStoreOwnerNotificationAsync(product, _localizationSettings.DefaultAdminLanguageId); } } break; case ManageInventoryMethod.ManageStockByAttributes: { var combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(product.Id, selection); if (combination != null) { result.StockQuantityOld = combination.StockQuantity; result.StockQuantityNew = decrease ? combination.StockQuantity - quantity : combination.StockQuantity + quantity; combination.StockQuantity = result.StockQuantityNew; } } break; case ManageInventoryMethod.DontManageStock: default: // Do nothing. break; } var attributeValues = await _productAttributeMaterializer.MaterializeProductVariantAttributeValuesAsync(selection); var productLinkageValues = attributeValues .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage) .ToList(); foreach (var chunk in productLinkageValues.Slice(100)) { var linkedProductIds = chunk.Select(x => x.LinkedProductId).Distinct().ToArray(); var linkedProducts = await _db.Products.GetManyAsync(linkedProductIds, true); var linkedProductsDic = linkedProducts.ToDictionarySafe(x => x.Id); foreach (var value in chunk) { if (linkedProductsDic.TryGetValue(value.LinkedProductId, out var linkedProduct)) { await AdjustInventoryAsync(linkedProduct, null, decrease, quantity *value.Quantity); } } } await _db.SaveChangesAsync(); return(result); }
public virtual async Task <AdjustInventoryResult> AdjustInventoryAsync(Product product, ProductVariantAttributeSelection selection, bool decrease, int quantity) { Guard.NotNull(product, nameof(product)); Guard.NotNull(selection, nameof(selection)); var result = new AdjustInventoryResult(); switch (product.ManageInventoryMethod) { case ManageInventoryMethod.ManageStock: { result.StockQuantityOld = product.StockQuantity; result.StockQuantityNew = decrease ? product.StockQuantity - quantity : product.StockQuantity + quantity; var newPublished = product.Published; var newDisableBuyButton = product.DisableBuyButton; var newDisableWishlistButton = product.DisableWishlistButton; // Check if the minimum quantity is reached. switch (product.LowStockActivity) { case LowStockActivity.DisableBuyButton: newDisableBuyButton = product.MinStockQuantity >= result.StockQuantityNew; newDisableWishlistButton = product.MinStockQuantity >= result.StockQuantityNew; break; case LowStockActivity.Unpublish: newPublished = product.MinStockQuantity <= result.StockQuantityNew; break; } product.StockQuantity = result.StockQuantityNew; product.DisableBuyButton = newDisableBuyButton; product.DisableWishlistButton = newDisableWishlistButton; product.Published = newPublished; // TODO: (mg) (core) ProductService.AdjustInventoryAsync doesn't send SendQuantityBelowStoreOwnerNotification anymore. Must be sent by caller after (!) database commit. // TODO: (mg) (core) The caller should definitely NOT be responsible for figuring out, when and how to publish messages. That would be extremely bad API design. //if (decrease && product.NotifyAdminForQuantityBelow > result.StockQuantityNew) //{ // _services.MessageFactory.SendQuantityBelowStoreOwnerNotification(product, _localizationSettings.DefaultAdminLanguageId); //} } break; case ManageInventoryMethod.ManageStockByAttributes: { var combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(product.Id, selection); if (combination != null) { result.StockQuantityOld = combination.StockQuantity; result.StockQuantityNew = decrease ? combination.StockQuantity - quantity : combination.StockQuantity + quantity; combination.StockQuantity = result.StockQuantityNew; } } break; case ManageInventoryMethod.DontManageStock: default: // Do nothing. break; } var attributeValues = await _productAttributeMaterializer.MaterializeProductVariantAttributeValuesAsync(selection); var productLinkageValues = attributeValues .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage) .ToList(); foreach (var chunk in productLinkageValues.Slice(100)) { var linkedProductIds = chunk.Select(x => x.LinkedProductId).Distinct().ToArray(); var linkedProducts = await _db.Products.GetManyAsync(linkedProductIds, true); var linkedProductsDic = linkedProducts.ToDictionarySafe(x => x.Id); foreach (var value in chunk) { if (linkedProductsDic.TryGetValue(value.LinkedProductId, out var linkedProduct)) { await AdjustInventoryAsync(linkedProduct, null, decrease, quantity *value.Quantity); } } } return(result); }