public void Can_calculate_total_quantity_when_we_do_use_multiple_warehouses_without_reserved() { var product = new Product { ManageInventoryMethod = ManageInventoryMethod.ManageStock, UseMultipleWarehouses = true, StockQuantity = 6, }; product.ProductWarehouseInventory.Add(new ProductWarehouseInventory { WarehouseId = 1, StockQuantity = 7, ReservedQuantity = 4, }); product.ProductWarehouseInventory.Add(new ProductWarehouseInventory { WarehouseId = 2, StockQuantity = 8, ReservedQuantity = 1, }); product.ProductWarehouseInventory.Add(new ProductWarehouseInventory { WarehouseId = 3, StockQuantity = -2, }); var result = product.GetTotalStockQuantity(false); result.ShouldEqual(13); }
public void Can_calculate_total_quantity_when_we_do_not_use_multiple_warehouses() { var product = new Product { ManageInventoryMethod = ManageInventoryMethod.ManageStock, UseMultipleWarehouses = false, StockQuantity = 6, }; product.ProductWarehouseInventory.Add(new ProductWarehouseInventory { WarehouseId = 1, StockQuantity = 7, }); product.ProductWarehouseInventory.Add(new ProductWarehouseInventory { WarehouseId = 2, StockQuantity = 8, }); product.ProductWarehouseInventory.Add(new ProductWarehouseInventory { WarehouseId = 3, StockQuantity = -2, }); var result = product.GetTotalStockQuantity(true); result.ShouldEqual(6); }
/// <summary> /// Adjust inventory /// </summary> /// <param name="product">Product</param> /// <param name="quantityToChange">Quantity to increase or descrease</param> /// <param name="attributesXml">Attributes in XML format</param> public virtual void AdjustInventory(Product product, int quantityToChange, string attributesXml = "") { if (product == null) throw new ArgumentNullException("product"); if (quantityToChange == 0) return; if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock) { //previous stock var prevStockQuantity = product.GetTotalStockQuantity(); //update stock quantity if (product.UseMultipleWarehouses) { //use multiple warehouses if (quantityToChange < 0) ReserveInventory(product, quantityToChange); else UnblockReservedInventory(product, quantityToChange); } else { //do not use multiple warehouses //simple inventory management product.StockQuantity += quantityToChange; UpdateProduct(product); } //qty is reduced. check if minimum stock quantity is reached if (quantityToChange < 0 && product.MinStockQuantity >= product.GetTotalStockQuantity()) { //what should we do now? disable buy button, unpublish the product, or do nothing? check "Low stock activity" property switch (product.LowStockActivity) { case LowStockActivity.DisableBuyButton: product.DisableBuyButton = true; product.DisableWishlistButton = true; UpdateProduct(product); break; case LowStockActivity.Unpublish: product.Published = false; UpdateProduct(product); break; default: break; } } //qty is increased. product is back in stock (minimum stock quantity is reached again)? if (_catalogSettings.PublishBackProductWhenCancellingOrders) { if (quantityToChange > 0 && prevStockQuantity <= product.MinStockQuantity && product.MinStockQuantity < product.GetTotalStockQuantity()) { switch (product.LowStockActivity) { case LowStockActivity.DisableBuyButton: product.DisableBuyButton = false; product.DisableWishlistButton = false; UpdateProduct(product); break; case LowStockActivity.Unpublish: product.Published = true; UpdateProduct(product); break; default: break; } } } //send email notification if (quantityToChange < 0 && product.GetTotalStockQuantity() < product.NotifyAdminForQuantityBelow) { _workflowMessageService.SendQuantityBelowStoreOwnerNotification(product, _localizationSettings.DefaultAdminLanguageId); } } if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) { var combination = _productAttributeParser.FindProductAttributeCombination(product, attributesXml); if (combination != null) { combination.StockQuantity += quantityToChange; _productAttributeService.UpdateProductAttributeCombination(combination); //send email notification if (quantityToChange < 0 && combination.StockQuantity < combination.NotifyAdminForQuantityBelow) { _workflowMessageService.SendQuantityBelowStoreOwnerNotification(combination, _localizationSettings.DefaultAdminLanguageId); } } } //bundled products var attributeValues = _productAttributeParser.ParseProductAttributeValues(attributesXml); foreach (var attributeValue in attributeValues) { if (attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct) { //associated product (bundle) var associatedProduct = GetProductById(attributeValue.AssociatedProductId); if (associatedProduct != null) { AdjustInventory(associatedProduct, quantityToChange * attributeValue.Quantity); } } } //TODO send back in stock notifications? //also do not forget to uncomment some code above ("prevStockQuantity") //if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock && // product.BackorderMode == BackorderMode.NoBackorders && // product.AllowBackInStockSubscriptions && // product.GetTotalStockQuantity() > 0 && // prevStockQuantity <= 0 && // product.Published && // !product.Deleted) //{ // //_backInStockSubscriptionService.SendNotificationsToSubscribers(product); //} }
/// <summary> /// Validates a product for standard properties /// </summary> /// <param name="customer">Customer</param> /// <param name="shoppingCartType">Shopping cart type</param> /// <param name="product">Product</param> /// <param name="attributesXml">Attributes in XML format</param> /// <param name="customerEnteredPrice">Customer entered price</param> /// <param name="quantity">Quantity</param> /// <returns>Warnings</returns> public virtual IList<string> GetStandardWarnings(Customer customer, ShoppingCartType shoppingCartType, Product product, string attributesXml, decimal customerEnteredPrice, int quantity) { if (customer == null) throw new ArgumentNullException("customer"); if (product == null) throw new ArgumentNullException("product"); var warnings = new List<string>(); //deleted if (product.Deleted) { warnings.Add(_localizationService.GetResource("ShoppingCart.ProductDeleted")); return warnings; } //published if (!product.Published) { warnings.Add(_localizationService.GetResource("ShoppingCart.ProductUnpublished")); } //we can add only simple products if (product.ProductType != ProductType.SimpleProduct) { warnings.Add("This is not simple product"); } //ACL if (!_aclService.Authorize(product, customer)) { warnings.Add(_localizationService.GetResource("ShoppingCart.ProductUnpublished")); } //Store mapping if (!_storeMappingService.Authorize(product, _storeContext.CurrentStore.ID)) { warnings.Add(_localizationService.GetResource("ShoppingCart.ProductUnpublished")); } //disabled "add to cart" button if (shoppingCartType == ShoppingCartType.ShoppingCart && product.DisableBuyButton) { warnings.Add(_localizationService.GetResource("ShoppingCart.BuyingDisabled")); } //disabled "add to wishlist" button if (shoppingCartType == ShoppingCartType.Wishlist && product.DisableWishlistButton) { warnings.Add(_localizationService.GetResource("ShoppingCart.WishlistDisabled")); } //call for price if (shoppingCartType == ShoppingCartType.ShoppingCart && product.CallForPrice) { warnings.Add(_localizationService.GetResource("Products.CallForPrice")); } //customer entered price if (product.CustomerEntersPrice) { if (customerEnteredPrice < product.MinimumCustomerEnteredPrice || customerEnteredPrice > product.MaximumCustomerEnteredPrice) { decimal minimumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MinimumCustomerEnteredPrice, _workContext.WorkingCurrency); decimal maximumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MaximumCustomerEnteredPrice, _workContext.WorkingCurrency); warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.CustomerEnteredPrice.RangeError"), _priceFormatter.FormatPrice(minimumCustomerEnteredPrice, false, false), _priceFormatter.FormatPrice(maximumCustomerEnteredPrice, false, false))); } } //quantity validation var hasQtyWarnings = false; if (quantity < product.OrderMinimumQuantity) { warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.MinimumQuantity"), product.OrderMinimumQuantity)); hasQtyWarnings = true; } if (quantity > product.OrderMaximumQuantity) { warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.MaximumQuantity"), product.OrderMaximumQuantity)); hasQtyWarnings = true; } var allowedQuantities = product.ParseAllowedQuatities(); if (allowedQuantities.Length > 0 && !allowedQuantities.Contains(quantity)) { warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.AllowedQuantities"), string.Join(", ", allowedQuantities))); } var validateOutOfStock = shoppingCartType == ShoppingCartType.ShoppingCart || !_shoppingCartSettings.AllowOutOfStockItemsToBeAddedToWishlist; if (validateOutOfStock && !hasQtyWarnings) { switch (product.ManageInventoryMethod) { case ManageInventoryMethod.DontManageStock: { //do nothing } break; case ManageInventoryMethod.ManageStock: { if (product.BackorderMode == BackorderMode.NoBackorders) { int maximumQuantityCanBeAdded = product.GetTotalStockQuantity(); if (maximumQuantityCanBeAdded < quantity) { if (maximumQuantityCanBeAdded <= 0) warnings.Add(_localizationService.GetResource("ShoppingCart.OutOfStock")); else warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.QuantityExceedsStock"), maximumQuantityCanBeAdded)); } } } break; case ManageInventoryMethod.ManageStockByAttributes: { var combination = _productAttributeParser.FindProductAttributeCombination(product, attributesXml); if (combination != null) { //combination exists //let's check stock level if (!combination.AllowOutOfStockOrders && combination.StockQuantity < quantity) { int maximumQuantityCanBeAdded = combination.StockQuantity; if (maximumQuantityCanBeAdded <= 0) { warnings.Add(_localizationService.GetResource("ShoppingCart.OutOfStock")); } else { warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.QuantityExceedsStock"), maximumQuantityCanBeAdded)); } } } else { //combination doesn't exist if (product.AllowAddingOnlyExistingAttributeCombinations) { //maybe, is it better to display something like "No such product/combination" message? warnings.Add(_localizationService.GetResource("ShoppingCart.OutOfStock")); } } } break; default: break; } } //availability dates bool availableStartDateError = false; if (product.AvailableStartDateTimeUtc.HasValue) { DateTime now = DateTime.UtcNow; DateTime availableStartDateTime = DateTime.SpecifyKind(product.AvailableStartDateTimeUtc.Value, DateTimeKind.Utc); if (availableStartDateTime.CompareTo(now) > 0) { warnings.Add(_localizationService.GetResource("ShoppingCart.NotAvailable")); availableStartDateError = true; } } if (product.AvailableEndDateTimeUtc.HasValue && !availableStartDateError) { DateTime now = DateTime.UtcNow; DateTime availableEndDateTime = DateTime.SpecifyKind(product.AvailableEndDateTimeUtc.Value, DateTimeKind.Utc); if (availableEndDateTime.CompareTo(now) < 0) { warnings.Add(_localizationService.GetResource("ShoppingCart.NotAvailable")); } } return warnings; }
protected virtual ProductDetailsModel PrepareProductDetailsPageModel(Product product, ShoppingCartItem updatecartitem = null, bool isAssociatedProduct = false) { if (product == null) throw new ArgumentNullException("product"); #region Standard properties var model = new ProductDetailsModel { Id = product.Id, Name = product.GetLocalized(x => x.Name), ShortDescription = product.GetLocalized(x => x.ShortDescription), FullDescription = product.GetLocalized(x => x.FullDescription), MetaKeywords = product.GetLocalized(x => x.MetaKeywords), MetaDescription = product.GetLocalized(x => x.MetaDescription), MetaTitle = product.GetLocalized(x => x.MetaTitle), SeName = product.GetSeName(), ShowSku = _catalogSettings.ShowProductSku, Sku = product.Sku, ShowManufacturerPartNumber = _catalogSettings.ShowManufacturerPartNumber, FreeShippingNotificationEnabled = _catalogSettings.ShowFreeShippingNotification, ManufacturerPartNumber = product.ManufacturerPartNumber, ShowGtin = _catalogSettings.ShowGtin, Gtin = product.Gtin, StockAvailability = product.FormatStockMessage("", _localizationService, _productAttributeParser), HasSampleDownload = product.IsDownload && product.HasSampleDownload, DisplayDiscontinuedMessage = !product.Published && _catalogSettings.DisplayDiscontinuedMessageForUnpublishedProducts }; //automatically generate product description? if (_seoSettings.GenerateProductMetaDescription && String.IsNullOrEmpty(model.MetaDescription)) { //based on short description model.MetaDescription = model.ShortDescription; } //shipping info model.IsShipEnabled = product.IsShipEnabled; if (product.IsShipEnabled) { model.IsFreeShipping = product.IsFreeShipping; //delivery date var deliveryDate = _shippingService.GetDeliveryDateById(product.DeliveryDateId); if (deliveryDate != null) { model.DeliveryDate = deliveryDate.GetLocalized(dd => dd.Name); } } //email a friend model.EmailAFriendEnabled = _catalogSettings.EmailAFriendEnabled; //compare products model.CompareProductsEnabled = _catalogSettings.CompareProductsEnabled; #endregion #region Vendor details //vendor if (_vendorSettings.ShowVendorOnProductDetailsPage) { var vendor = _vendorService.GetVendorById(product.VendorId); if (vendor != null && !vendor.Deleted && vendor.Active) { model.ShowVendor = true; model.VendorModel = new VendorBriefInfoModel { Id = vendor.Id, Name = vendor.GetLocalized(x => x.Name), SeName = vendor.GetSeName(), }; } } #endregion #region Page sharing if (_catalogSettings.ShowShareButton && !String.IsNullOrEmpty(_catalogSettings.PageShareCode)) { var shareCode = _catalogSettings.PageShareCode; if (_webHelper.IsCurrentConnectionSecured()) { //need to change the addthis link to be https linked when the page is, so that the page doesnt ask about mixed mode when viewed in https... shareCode = shareCode.Replace("http://", "https://"); } model.PageShareCode = shareCode; } #endregion #region Back in stock subscriptions if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock && product.BackorderMode == BackorderMode.NoBackorders && product.AllowBackInStockSubscriptions && product.GetTotalStockQuantity() <= 0) { //out of stock model.DisplayBackInStockSubscription = true; } #endregion #region Breadcrumb //do not prepare this model for the associated products. anyway it's not used if (_catalogSettings.CategoryBreadcrumbEnabled && !isAssociatedProduct) { var breadcrumbCacheKey = string.Format(ModelCacheEventConsumer.PRODUCT_BREADCRUMB_MODEL_KEY, product.Id, _workContext.WorkingLanguage.Id, string.Join(",", _workContext.CurrentCustomer.GetCustomerRoleIds()), _storeContext.CurrentStore.Id); model.Breadcrumb = _cacheManager.Get(breadcrumbCacheKey, () => { var breadcrumbModel = new ProductDetailsModel.ProductBreadcrumbModel { Enabled = _catalogSettings.CategoryBreadcrumbEnabled, ProductId = product.Id, ProductName = product.GetLocalized(x => x.Name), ProductSeName = product.GetSeName() }; var productCategories = _categoryService.GetProductCategoriesByProductId(product.Id); if (productCategories.Count > 0) { var category = productCategories[0].Category; if (category != null) { foreach (var catBr in category.GetCategoryBreadCrumb(_categoryService, _aclService, _storeMappingService)) { breadcrumbModel.CategoryBreadcrumb.Add(new CategorySimpleModel { Id = catBr.Id, Name = catBr.GetLocalized(x => x.Name), SeName = catBr.GetSeName(), IncludeInTopMenu = catBr.IncludeInTopMenu }); } } } return breadcrumbModel; }); } #endregion #region Product tags //do not prepare this model for the associated products. anyway it's not used if (!isAssociatedProduct) { var productTagsCacheKey = string.Format(ModelCacheEventConsumer.PRODUCTTAG_BY_PRODUCT_MODEL_KEY, product.Id, _workContext.WorkingLanguage.Id, _storeContext.CurrentStore.Id); model.ProductTags = _cacheManager.Get(productTagsCacheKey, () => product.ProductTags //filter by store .Where(x => _productTagService.GetProductCount(x.Id, _storeContext.CurrentStore.Id) > 0) .Select(x => new ProductTagModel { Id = x.Id, Name = x.GetLocalized(y => y.Name), SeName = x.GetSeName(), ProductCount = _productTagService.GetProductCount(x.Id, _storeContext.CurrentStore.Id) }) .ToList()); } #endregion #region Templates var templateCacheKey = string.Format(ModelCacheEventConsumer.PRODUCT_TEMPLATE_MODEL_KEY, product.ProductTemplateId); model.ProductTemplateViewPath = _cacheManager.Get(templateCacheKey, () => { var template = _productTemplateService.GetProductTemplateById(product.ProductTemplateId); if (template == null) template = _productTemplateService.GetAllProductTemplates().FirstOrDefault(); if (template == null) throw new Exception("No default template could be loaded"); return template.ViewPath; }); #endregion #region Pictures model.DefaultPictureZoomEnabled = _mediaSettings.DefaultPictureZoomEnabled; //default picture var defaultPictureSize = isAssociatedProduct ? _mediaSettings.AssociatedProductPictureSize : _mediaSettings.ProductDetailsPictureSize; //prepare picture models var productPicturesCacheKey = string.Format(ModelCacheEventConsumer.PRODUCT_DETAILS_PICTURES_MODEL_KEY, product.Id, defaultPictureSize, isAssociatedProduct, _workContext.WorkingLanguage.Id, _webHelper.IsCurrentConnectionSecured(), _storeContext.CurrentStore.Id); var cachedPictures = _cacheManager.Get(productPicturesCacheKey, () => { var pictures = _pictureService.GetPicturesByProductId(product.Id); var defaultPicture = pictures.FirstOrDefault(); var defaultPictureModel = new PictureModel { ImageUrl = _pictureService.GetPictureUrl(defaultPicture, defaultPictureSize, !isAssociatedProduct), FullSizeImageUrl = _pictureService.GetPictureUrl(defaultPicture, 0, !isAssociatedProduct), Title = string.Format(_localizationService.GetResource("Media.Product.ImageLinkTitleFormat.Details"), model.Name), AlternateText = string.Format(_localizationService.GetResource("Media.Product.ImageAlternateTextFormat.Details"), model.Name), }; //"title" attribute defaultPictureModel.Title = (defaultPicture != null && !string.IsNullOrEmpty(defaultPicture.TitleAttribute)) ? defaultPicture.TitleAttribute : string.Format(_localizationService.GetResource("Media.Product.ImageLinkTitleFormat.Details"), model.Name); //"alt" attribute defaultPictureModel.AlternateText = (defaultPicture != null && !string.IsNullOrEmpty(defaultPicture.AltAttribute)) ? defaultPicture.AltAttribute : string.Format(_localizationService.GetResource("Media.Product.ImageAlternateTextFormat.Details"), model.Name); //all pictures var pictureModels = new List<PictureModel>(); foreach (var picture in pictures) { var pictureModel = new PictureModel { ImageUrl = _pictureService.GetPictureUrl(picture, _mediaSettings.ProductThumbPictureSizeOnProductDetailsPage), FullSizeImageUrl = _pictureService.GetPictureUrl(picture), Title = string.Format(_localizationService.GetResource("Media.Product.ImageLinkTitleFormat.Details"), model.Name), AlternateText = string.Format(_localizationService.GetResource("Media.Product.ImageAlternateTextFormat.Details"), model.Name), }; //"title" attribute pictureModel.Title = !string.IsNullOrEmpty(picture.TitleAttribute) ? picture.TitleAttribute : string.Format(_localizationService.GetResource("Media.Product.ImageLinkTitleFormat.Details"), model.Name); //"alt" attribute pictureModel.AlternateText = !string.IsNullOrEmpty(picture.AltAttribute) ? picture.AltAttribute : string.Format(_localizationService.GetResource("Media.Product.ImageAlternateTextFormat.Details"), model.Name); pictureModels.Add(pictureModel); } return new { DefaultPictureModel = defaultPictureModel, PictureModels = pictureModels }; }); model.DefaultPictureModel = cachedPictures.DefaultPictureModel; model.PictureModels = cachedPictures.PictureModels; #endregion #region Product price //for GoupDealProduct Products //model.ProductPrice.SpecialPrice = _priceFormatter.FormatPrice(System.Convert.ToDecimal(product.SpecialPrice)); //model.ProductPrice.SpecialPriceValue = product.SpecialPrice; model.AvailableEndDateTimeUtc = Convert.ToDateTime(product.AvailableEndDateTimeUtc); model.AvailableStartDateTimeUtc = Convert.ToDateTime(product.AvailableStartDateTimeUtc); // End Here model.ProductPrice.ProductId = product.Id; if (_permissionService.Authorize(StandardPermissionProvider.DisplayPrices)) { model.ProductPrice.HidePrices = false; if (product.CustomerEntersPrice) { model.ProductPrice.CustomerEntersPrice = true; } else { if (product.CallForPrice) { model.ProductPrice.CallForPrice = true; } else { decimal taxRate; decimal oldPriceBase = _taxService.GetProductPrice(product, product.OldPrice, out taxRate); decimal finalPriceWithoutDiscountBase = _taxService.GetProductPrice(product, _priceCalculationService.GetFinalPrice(product, _workContext.CurrentCustomer, includeDiscounts: false), out taxRate); decimal finalPriceWithDiscountBase = _taxService.GetProductPrice(product, _priceCalculationService.GetFinalPrice(product, _workContext.CurrentCustomer, includeDiscounts: true), out taxRate); //for group deal if (product.SpecialPrice != null) { model.ProductPrice.SpecialPriceValue = _currencyService.ConvertFromPrimaryStoreCurrency(Convert.ToDecimal(product.SpecialPrice), _workContext.WorkingCurrency); model.ProductPrice.SpecialPrice = _priceFormatter.FormatPrice(System.Convert.ToDecimal(model.ProductPrice.SpecialPriceValue)); model.ProductPrice.FinalPriceWithoutSpecialPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.Price, _workContext.WorkingCurrency); } decimal oldPrice = _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, _workContext.WorkingCurrency); decimal finalPriceWithoutDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceWithoutDiscountBase, _workContext.WorkingCurrency); decimal finalPriceWithDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceWithDiscountBase, _workContext.WorkingCurrency); if (finalPriceWithoutDiscountBase != oldPriceBase && oldPriceBase > decimal.Zero) model.ProductPrice.OldPrice = _priceFormatter.FormatPrice(oldPrice); model.ProductPrice.Price = _priceFormatter.FormatPrice(finalPriceWithoutDiscount); if (finalPriceWithoutDiscountBase != finalPriceWithDiscountBase) model.ProductPrice.PriceWithDiscount = _priceFormatter.FormatPrice(finalPriceWithDiscount); model.ProductPrice.PriceValue = finalPriceWithDiscount; model.ProductPrice.PriceWithDiscountValue = finalPriceWithDiscount; //property for German market //we display tax/shipping info only with "shipping enabled" for this product //we also ensure this it's not free shipping model.ProductPrice.DisplayTaxShippingInfo = _catalogSettings.DisplayTaxShippingInfoProductDetailsPage && product.IsShipEnabled && !product.IsFreeShipping; //PAngV baseprice (used in Germany) model.ProductPrice.BasePricePAngV = product.FormatBasePrice(finalPriceWithDiscountBase, _localizationService, _measureService, _currencyService, _workContext, _priceFormatter); //currency code model.ProductPrice.CurrencyCode = _workContext.WorkingCurrency.CurrencyCode; //rental if (product.IsRental) { model.ProductPrice.IsRental = true; var priceStr = _priceFormatter.FormatPrice(finalPriceWithDiscount); model.ProductPrice.RentalPrice = _priceFormatter.FormatRentalProductPeriod(product, priceStr); } } } } else { model.ProductPrice.HidePrices = true; model.ProductPrice.OldPrice = null; model.ProductPrice.Price = null; } #endregion #region 'Add to cart' model model.AddToCart.ProductId = product.Id; model.AddToCart.UpdatedShoppingCartItemId = updatecartitem != null ? updatecartitem.Id : 0; //quantity model.AddToCart.EnteredQuantity = updatecartitem != null ? updatecartitem.Quantity : product.OrderMinimumQuantity; //allowed quantities var allowedQuantities = product.ParseAllowedQuantities(); foreach (var qty in allowedQuantities) { model.AddToCart.AllowedQuantities.Add(new SelectListItem { Text = qty.ToString(), Value = qty.ToString(), Selected = updatecartitem != null && updatecartitem.Quantity == qty }); } //minimum quantity notification if (product.OrderMinimumQuantity > 1) { model.AddToCart.MinimumQuantityNotification = string.Format(_localizationService.GetResource("Products.MinimumQuantityNotification"), product.OrderMinimumQuantity); } //'add to cart', 'add to wishlist' buttons model.AddToCart.DisableBuyButton = product.DisableBuyButton || !_permissionService.Authorize(StandardPermissionProvider.EnableShoppingCart); model.AddToCart.DisableWishlistButton = product.DisableWishlistButton || !_permissionService.Authorize(StandardPermissionProvider.EnableWishlist); if (!_permissionService.Authorize(StandardPermissionProvider.DisplayPrices)) { model.AddToCart.DisableBuyButton = true; model.AddToCart.DisableWishlistButton = true; } //pre-order if (product.AvailableForPreOrder) { model.AddToCart.AvailableForPreOrder = !product.PreOrderAvailabilityStartDateTimeUtc.HasValue || product.PreOrderAvailabilityStartDateTimeUtc.Value >= DateTime.UtcNow; model.AddToCart.PreOrderAvailabilityStartDateTimeUtc = product.PreOrderAvailabilityStartDateTimeUtc; } //rental model.AddToCart.IsRental = product.IsRental; //customer entered price model.AddToCart.CustomerEntersPrice = product.CustomerEntersPrice; if (model.AddToCart.CustomerEntersPrice) { decimal minimumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MinimumCustomerEnteredPrice, _workContext.WorkingCurrency); decimal maximumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MaximumCustomerEnteredPrice, _workContext.WorkingCurrency); model.AddToCart.CustomerEnteredPrice = updatecartitem != null ? updatecartitem.CustomerEnteredPrice : minimumCustomerEnteredPrice; model.AddToCart.CustomerEnteredPriceRange = string.Format(_localizationService.GetResource("Products.EnterProductPrice.Range"), _priceFormatter.FormatPrice(minimumCustomerEnteredPrice, false, false), _priceFormatter.FormatPrice(maximumCustomerEnteredPrice, false, false)); } #endregion #region Gift card model.GiftCard.IsGiftCard = product.IsGiftCard; if (model.GiftCard.IsGiftCard) { model.GiftCard.GiftCardType = product.GiftCardType; if (updatecartitem == null) { model.GiftCard.SenderName = _workContext.CurrentCustomer.GetFullName(); model.GiftCard.SenderEmail = _workContext.CurrentCustomer.Email; } else { string giftCardRecipientName, giftCardRecipientEmail, giftCardSenderName, giftCardSenderEmail, giftCardMessage; _productAttributeParser.GetGiftCardAttribute(updatecartitem.AttributesXml, out giftCardRecipientName, out giftCardRecipientEmail, out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); model.GiftCard.RecipientName = giftCardRecipientName; model.GiftCard.RecipientEmail = giftCardRecipientEmail; model.GiftCard.SenderName = giftCardSenderName; model.GiftCard.SenderEmail = giftCardSenderEmail; model.GiftCard.Message = giftCardMessage; } } #endregion #region Product attributes //performance optimization //We cache a value indicating whether a product has attributes IList<ProductAttributeMapping> productAttributeMapping = null; string cacheKey = string.Format(ModelCacheEventConsumer.PRODUCT_HAS_PRODUCT_ATTRIBUTES_KEY, product.Id); var hasProductAttributesCache = _cacheManager.Get<bool?>(cacheKey); if (!hasProductAttributesCache.HasValue) { //no value in the cache yet //let's load attributes and cache the result (true/false) productAttributeMapping = _productAttributeService.GetProductAttributeMappingsByProductId(product.Id); hasProductAttributesCache = productAttributeMapping.Count > 0; _cacheManager.Set(cacheKey, hasProductAttributesCache, 60); } if (hasProductAttributesCache.Value && productAttributeMapping == null) { //cache indicates that the product has attributes //let's load them productAttributeMapping = _productAttributeService.GetProductAttributeMappingsByProductId(product.Id); } if (productAttributeMapping == null) { productAttributeMapping = new List<ProductAttributeMapping>(); } foreach (var attribute in productAttributeMapping) { var attributeModel = new ProductDetailsModel.ProductAttributeModel { Id = attribute.Id, ProductId = product.Id, ProductAttributeId = attribute.ProductAttributeId, Name = attribute.ProductAttribute.GetLocalized(x => x.Name), Description = attribute.ProductAttribute.GetLocalized(x => x.Description), TextPrompt = attribute.TextPrompt, IsRequired = attribute.IsRequired, AttributeControlType = attribute.AttributeControlType, DefaultValue = updatecartitem != null ? null : attribute.DefaultValue, HasCondition = !String.IsNullOrEmpty(attribute.ConditionAttributeXml) }; if (!String.IsNullOrEmpty(attribute.ValidationFileAllowedExtensions)) { attributeModel.AllowedFileExtensions = attribute.ValidationFileAllowedExtensions .Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) .ToList(); } if (attribute.ShouldHaveValues()) { //values var attributeValues = _productAttributeService.GetProductAttributeValues(attribute.Id); foreach (var attributeValue in attributeValues) { var valueModel = new ProductDetailsModel.ProductAttributeValueModel { Id = attributeValue.Id, Name = attributeValue.GetLocalized(x => x.Name), ColorSquaresRgb = attributeValue.ColorSquaresRgb, //used with "Color squares" attribute type IsPreSelected = attributeValue.IsPreSelected }; attributeModel.Values.Add(valueModel); //display price if allowed if (_permissionService.Authorize(StandardPermissionProvider.DisplayPrices)) { decimal taxRate; decimal attributeValuePriceAdjustment = _priceCalculationService.GetProductAttributeValuePriceAdjustment(attributeValue); decimal priceAdjustmentBase = _taxService.GetProductPrice(product, attributeValuePriceAdjustment, out taxRate); decimal priceAdjustment = _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, _workContext.WorkingCurrency); if (priceAdjustmentBase > decimal.Zero) valueModel.PriceAdjustment = "+" + _priceFormatter.FormatPrice(priceAdjustment, false, false); else if (priceAdjustmentBase < decimal.Zero) valueModel.PriceAdjustment = "-" + _priceFormatter.FormatPrice(-priceAdjustment, false, false); valueModel.PriceAdjustmentValue = priceAdjustment; } //picture if (attributeValue.PictureId > 0) { var productAttributePictureCacheKey = string.Format(ModelCacheEventConsumer.PRODUCTATTRIBUTE_PICTURE_MODEL_KEY, attributeValue.PictureId, _webHelper.IsCurrentConnectionSecured(), _storeContext.CurrentStore.Id); valueModel.PictureModel = _cacheManager.Get(productAttributePictureCacheKey, () => { var valuePicture = _pictureService.GetPictureById(attributeValue.PictureId); if (valuePicture != null) { return new PictureModel { FullSizeImageUrl = _pictureService.GetPictureUrl(valuePicture), ImageUrl = _pictureService.GetPictureUrl(valuePicture, defaultPictureSize) }; } return new PictureModel(); }); } } } //set already selected attributes (if we're going to update the existing shopping cart item) if (updatecartitem != null) { switch (attribute.AttributeControlType) { case AttributeControlType.DropdownList: case AttributeControlType.RadioList: case AttributeControlType.Checkboxes: case AttributeControlType.ColorSquares: { if (!String.IsNullOrEmpty(updatecartitem.AttributesXml)) { //clear default selection foreach (var item in attributeModel.Values) item.IsPreSelected = false; //select new values var selectedValues = _productAttributeParser.ParseProductAttributeValues(updatecartitem.AttributesXml); foreach (var attributeValue in selectedValues) foreach (var item in attributeModel.Values) if (attributeValue.Id == item.Id) item.IsPreSelected = true; } } break; case AttributeControlType.ReadonlyCheckboxes: { //do nothing //values are already pre-set } break; case AttributeControlType.TextBox: case AttributeControlType.MultilineTextbox: { if (!String.IsNullOrEmpty(updatecartitem.AttributesXml)) { var enteredText = _productAttributeParser.ParseValues(updatecartitem.AttributesXml, attribute.Id); if (enteredText.Count > 0) attributeModel.DefaultValue = enteredText[0]; } } break; case AttributeControlType.Datepicker: { //keep in mind my that the code below works only in the current culture var selectedDateStr = _productAttributeParser.ParseValues(updatecartitem.AttributesXml, attribute.Id); if (selectedDateStr.Count > 0) { DateTime selectedDate; if (DateTime.TryParseExact(selectedDateStr[0], "D", CultureInfo.CurrentCulture, DateTimeStyles.None, out selectedDate)) { //successfully parsed attributeModel.SelectedDay = selectedDate.Day; attributeModel.SelectedMonth = selectedDate.Month; attributeModel.SelectedYear = selectedDate.Year; } } } break; case AttributeControlType.FileUpload: { if (!String.IsNullOrEmpty(updatecartitem.AttributesXml)) { var downloadGuidStr = _productAttributeParser.ParseValues(updatecartitem.AttributesXml, attribute.Id).FirstOrDefault(); Guid downloadGuid; Guid.TryParse(downloadGuidStr, out downloadGuid); var download = _downloadService.GetDownloadByGuid(downloadGuid); if (download != null) attributeModel.DefaultValue = download.DownloadGuid.ToString(); } } break; default: break; } } model.ProductAttributes.Add(attributeModel); } #endregion #region Product specifications //do not prepare this model for the associated products. any it's not used if (!isAssociatedProduct) { model.ProductSpecifications = this.PrepareProductSpecificationModel(_workContext, _specificationAttributeService, _cacheManager, product); } #endregion #region Product review overview model.ProductReviewOverview = new ProductReviewOverviewModel { ProductId = product.Id, RatingSum = product.ApprovedRatingSum, TotalReviews = product.ApprovedTotalReviews, AllowCustomerReviews = product.AllowCustomerReviews }; #endregion #region Tier prices if (product.HasTierPrices && _permissionService.Authorize(StandardPermissionProvider.DisplayPrices)) { model.TierPrices = product.TierPrices .OrderBy(x => x.Quantity) .ToList() .FilterByStore(_storeContext.CurrentStore.Id) .FilterForCustomer(_workContext.CurrentCustomer) .RemoveDuplicatedQuantities() .Select(tierPrice => { var m = new ProductDetailsModel.TierPriceModel { Quantity = tierPrice.Quantity, }; decimal taxRate; decimal priceBase = _taxService.GetProductPrice(product, _priceCalculationService.GetFinalPrice(product, _workContext.CurrentCustomer, decimal.Zero, _catalogSettings.DisplayTierPricesWithDiscounts, tierPrice.Quantity), out taxRate); decimal price = _currencyService.ConvertFromPrimaryStoreCurrency(priceBase, _workContext.WorkingCurrency); m.Price = _priceFormatter.FormatPrice(price, false, false); return m; }) .ToList(); } #endregion #region Manufacturers //do not prepare this model for the associated products. any it's not used if (!isAssociatedProduct) { string manufacturersCacheKey = string.Format(ModelCacheEventConsumer.PRODUCT_MANUFACTURERS_MODEL_KEY, product.Id, _workContext.WorkingLanguage.Id, string.Join(",", _workContext.CurrentCustomer.GetCustomerRoleIds()), _storeContext.CurrentStore.Id); model.ProductManufacturers = _cacheManager.Get(manufacturersCacheKey, () => _manufacturerService.GetProductManufacturersByProductId(product.Id) .Select(x => x.Manufacturer.ToModel()) .ToList() ); } #endregion #region Rental products if (product.IsRental) { model.IsRental = true; //set already entered dates attributes (if we're going to update the existing shopping cart item) if (updatecartitem != null) { model.RentalStartDate = updatecartitem.RentalStartDateUtc; model.RentalEndDate = updatecartitem.RentalEndDateUtc; } } #endregion #region Associated products if (product.ProductType == ProductType.GroupedProduct) { //ensure no circular references if (!isAssociatedProduct) { var associatedProducts = _productService.GetAssociatedProducts(product.Id, _storeContext.CurrentStore.Id); foreach (var associatedProduct in associatedProducts) model.AssociatedProducts.Add(PrepareProductDetailsPageModel(associatedProduct, null, true)); } } #endregion return model; }
public virtual void AddProductTokens(IList<Token> tokens, Product product, int languageId) { tokens.Add(new Token("Product.ID", product.Id.ToString())); tokens.Add(new Token("Product.Name", product.GetLocalized(x => x.Name, languageId))); tokens.Add(new Token("Product.ShortDescription", product.GetLocalized(x => x.ShortDescription, languageId), true)); tokens.Add(new Token("Product.SKU", product.Sku)); tokens.Add(new Token("Product.StockQuantity", product.GetTotalStockQuantity().ToString())); //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) var productUrl = string.Format("{0}{1}", GetStoreUrl(), product.GetSeName()); tokens.Add(new Token("Product.ProductURLForCustomer", productUrl, true)); //event notification _eventPublisher.EntityTokensAdded(product, tokens); }
/// <summary> /// Adjust inventory /// </summary> /// <param name="product">Product</param> /// <param name="quantityToChange">Quantity to increase or descrease</param> /// <param name="attributesXml">Attributes in XML format</param> public virtual void AdjustInventory(Product product, int quantityToChange, string attributesXml = "") { if (product == null) throw new ArgumentNullException("product"); if (quantityToChange == 0) return; //var prevStockQuantity = product.GetTotalStockQuantity(); if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock) { var prevStockQuantity = product.GetTotalStockQuantity(); //update stock quantity if (product.UseMultipleWarehouses) { //use multiple warehouses if (quantityToChange < 0) ReserveInventory(product, quantityToChange); else UnblockReservedInventory(product, quantityToChange); } else { //do not use multiple warehouses //simple inventory management product.StockQuantity += quantityToChange; UpdateStockProduct(product); } //check if minimum quantity is reached if (quantityToChange < 0 && product.MinStockQuantity >= product.GetTotalStockQuantity()) { switch (product.LowStockActivity) { case LowStockActivity.DisableBuyButton: product.DisableBuyButton = true; product.DisableWishlistButton = true; var filter = Builders<Product>.Filter.Eq("Id", product.Id); var update = Builders<Product>.Update .Set(x => x.DisableBuyButton, product.DisableBuyButton) .Set(x => x.DisableWishlistButton, product.DisableWishlistButton) .CurrentDate("UpdateDate"); _productRepository.Collection.UpdateOneAsync(filter, update); //cache _cacheManager.RemoveByPattern(string.Format(PRODUCTS_BY_ID_KEY, product.Id)); //event notification _eventPublisher.EntityUpdated(product); //UpdateProduct(product); break; case LowStockActivity.Unpublish: product.Published = false; var filter2 = Builders<Product>.Filter.Eq("Id", product.Id); var update2 = Builders<Product>.Update .Set(x => x.Published, product.Published) .CurrentDate("UpdateDate"); _productRepository.Collection.UpdateOneAsync(filter2, update2); //cache _cacheManager.RemoveByPattern(string.Format(PRODUCTS_BY_ID_KEY, product.Id)); if(product.ShowOnHomePage) _cacheManager.RemoveByPattern(PRODUCTS_SHOWONHOMEPAGE); //event notification _eventPublisher.EntityUpdated(product); //UpdateProduct(product); break; default: break; } } //qty is increased. product is back in stock (minimum stock quantity is reached again)? if (_catalogSettings.PublishBackProductWhenCancellingOrders) { if (quantityToChange > 0 && prevStockQuantity <= product.MinStockQuantity && product.MinStockQuantity < product.GetTotalStockQuantity()) { switch (product.LowStockActivity) { case LowStockActivity.DisableBuyButton: //product.DisableBuyButton = false; //product.DisableWishlistButton = false; //UpdateProduct(product); var filter = Builders<Product>.Filter.Eq("Id", product.Id); var update = Builders<Product>.Update .Set(x => x.DisableBuyButton, product.DisableBuyButton) .Set(x => x.DisableWishlistButton, product.DisableWishlistButton) .CurrentDate("UpdateDate"); _productRepository.Collection.UpdateOneAsync(filter, update); //cache _cacheManager.RemoveByPattern(string.Format(PRODUCTS_BY_ID_KEY, product.Id)); break; case LowStockActivity.Unpublish: //product.Published = true; //UpdateProduct(product); product.Published = false; var filter2 = Builders<Product>.Filter.Eq("Id", product.Id); var update2 = Builders<Product>.Update .Set(x => x.Published, product.Published) .CurrentDate("UpdateDate"); _productRepository.Collection.UpdateOneAsync(filter2, update2); //cache _cacheManager.RemoveByPattern(string.Format(PRODUCTS_BY_ID_KEY, product.Id)); if (product.ShowOnHomePage) _cacheManager.RemoveByPattern(PRODUCTS_SHOWONHOMEPAGE); break; default: break; } } } //send email notification if (quantityToChange < 0 && product.GetTotalStockQuantity() < product.NotifyAdminForQuantityBelow) { _workflowMessageService.SendQuantityBelowStoreOwnerNotification(product, _localizationSettings.DefaultAdminLanguageId); } } if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) { var combination = _productAttributeParser.FindProductAttributeCombination(product, attributesXml); if (combination != null) { combination.StockQuantity += quantityToChange; _productAttributeService.UpdateProductAttributeCombination(combination); product.StockQuantity += quantityToChange; UpdateStockProduct(product); //send email notification if (quantityToChange < 0 && combination.StockQuantity < combination.NotifyAdminForQuantityBelow) { _workflowMessageService.SendQuantityBelowStoreOwnerNotification(combination, _localizationSettings.DefaultAdminLanguageId); } } } //bundled products var attributeValues = _productAttributeParser.ParseProductAttributeValues(product, attributesXml); foreach (var attributeValue in attributeValues) { if (attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct) { //associated product (bundle) var associatedProduct = GetProductById(attributeValue.AssociatedProductId); if (associatedProduct != null) { AdjustInventory(associatedProduct, quantityToChange * attributeValue.Quantity); } } } }