diff --git a/src/Smartstore.Core/Checkout/Cart/Services/IShoppingCartValidator.cs b/src/Smartstore.Core/Checkout/Cart/Services/IShoppingCartValidator.cs index 1cdb0e5f5b..7b3e95e35b 100644 --- a/src/Smartstore.Core/Checkout/Cart/Services/IShoppingCartValidator.cs +++ b/src/Smartstore.Core/Checkout/Cart/Services/IShoppingCartValidator.cs @@ -65,11 +65,12 @@ public interface IShoppingCartValidator /// Validates product settings, authorization and availability. /// /// Shopping cart item with product and settings. + /// Shopping cart items of customer to validate. /// List of errors as string. /// Store identifier. /// Quantity to validate. If null, is instead. /// True when product is valid, otherwise false. - Task ValidateProductAsync(ShoppingCartItem cartItem, IList warnings, int? storeId = null, int? quantity = null); + Task ValidateProductAsync(ShoppingCartItem cartItem, IEnumerable cartItems, IList warnings, int? storeId = null, int? quantity = null); /// /// Validates selected product attributes. diff --git a/src/Smartstore.Core/Checkout/Cart/Services/ShoppingCartValidator.cs b/src/Smartstore.Core/Checkout/Cart/Services/ShoppingCartValidator.cs index 519025ce48..8ffb5a6ce9 100644 --- a/src/Smartstore.Core/Checkout/Cart/Services/ShoppingCartValidator.cs +++ b/src/Smartstore.Core/Checkout/Cart/Services/ShoppingCartValidator.cs @@ -58,8 +58,8 @@ public partial class ShoppingCartValidator : IShoppingCartValidator public virtual async Task ValidateAccessPermissionsAsync(Customer customer, ShoppingCartType cartType, IList warnings) { - Guard.NotNull(customer, nameof(customer)); - Guard.NotNull(warnings, nameof(warnings)); + Guard.NotNull(customer); + Guard.NotNull(warnings); var isValid = true; @@ -82,11 +82,10 @@ public virtual async Task ValidateAccessPermissionsAsync(Customer customer public virtual bool ValidateBundleItem(ProductBundleItem bundleItem, IList warnings) { - Guard.NotNull(bundleItem, nameof(bundleItem)); - Guard.NotNull(warnings, nameof(warnings)); + Guard.NotNull(bundleItem); + Guard.NotNull(warnings); var currentWarnings = new List(); - var name = bundleItem.GetLocalizedName(); if (!bundleItem.Published) @@ -118,8 +117,8 @@ public virtual bool ValidateBundleItem(ProductBundleItem bundleItem, IList ValidateCartAsync(ShoppingCart cart, IList warnings, bool validateCheckoutAttributes = false) { - Guard.NotNull(cart, nameof(cart)); - Guard.NotNull(warnings, nameof(warnings)); + Guard.NotNull(cart); + Guard.NotNull(warnings); var currentWarnings = new List(); @@ -185,18 +184,18 @@ public virtual async Task ValidateCartAsync(ShoppingCart cart, IList ValidateAddToCartItemAsync(AddToCartContext ctx, ShoppingCartItem cartItem, IEnumerable cartItems) { - Guard.NotNull(ctx, nameof(ctx)); - Guard.NotNull(cartItem, nameof(cartItem)); - Guard.NotNull(cartItems, nameof(cartItems)); + Guard.NotNull(ctx); + Guard.NotNull(cartItem); + Guard.NotNull(cartItems); var warnings = new List(); - await ValidateProductAsync(cartItem, warnings, ctx.StoreId); + await ValidateProductAsync(cartItem, cartItems, warnings, ctx.StoreId); await this.ValidateProductAttributesAsync(cartItem, cartItems, warnings); ValidateGiftCardInfo(cartItem.Product, cartItem.AttributeSelection, warnings); - // Bundle and bundle items (child items) warnings + // Bundle and bundle items (child items) warnings. if (ctx.BundleItem != null || !ctx.ChildItems.IsNullOrEmpty()) { var bundleItem = ctx.BundleItem ?? ctx.ChildItems.Select(x => x.BundleItem).FirstOrDefault(); @@ -212,7 +211,7 @@ public virtual async Task ValidateAddToCartItemAsync(AddToCartContext ctx, public virtual bool ValidateItemsMaximumCartQuantity(ShoppingCartType cartType, int cartItemsCount, IList warnings) { - Guard.NotNull(warnings, nameof(warnings)); + Guard.NotNull(warnings); var isValid = true; @@ -232,9 +231,9 @@ public virtual bool ValidateItemsMaximumCartQuantity(ShoppingCartType cartType, public virtual bool ValidateGiftCardInfo(Product product, ProductVariantAttributeSelection selection, IList warnings) { - Guard.NotNull(product, nameof(product)); - Guard.NotNull(selection, nameof(selection)); - Guard.NotNull(warnings, nameof(warnings)); + Guard.NotNull(product); + Guard.NotNull(selection); + Guard.NotNull(warnings); if (!product.IsGiftCard) { @@ -276,19 +275,24 @@ public virtual bool ValidateGiftCardInfo(Product product, ProductVariantAttribut return !currentWarnings.Any(); } - public virtual async Task ValidateProductAsync(ShoppingCartItem cartItem, IList warnings, int? storeId = null, int? quantity = null) + public virtual async Task ValidateProductAsync( + ShoppingCartItem cartItem, + IEnumerable cartItems, + IList warnings, + int? storeId = null, + int? quantity = null) { - Guard.NotNull(cartItem, nameof(cartItem)); - Guard.NotNull(warnings, nameof(warnings)); + Guard.NotNull(cartItem); + Guard.NotNull(warnings); - var product = cartItem.Product; - if (product == null) + var p = cartItem.Product; + if (p == null) { warnings.Add(T("Products.NotFound", cartItem.ProductId)); return false; } - if (product.Deleted) + if (p.Deleted) { warnings.Add(T("ShoppingCart.ProductDeleted")); return false; @@ -297,135 +301,144 @@ public virtual async Task ValidateProductAsync(ShoppingCartItem cartItem, var currentWarnings = new List(); // Grouped products are not available for order - if (product.ProductType == ProductType.GroupedProduct) + if (p.ProductType == ProductType.GroupedProduct) { currentWarnings.Add(T("ShoppingCart.ProductNotAvailableForOrder")); } // Validate product bundle, no customer entered price allowed - if (product.ProductType == ProductType.BundledProduct - && product.BundlePerItemPricing + if (p.ProductType == ProductType.BundledProduct + && p.BundlePerItemPricing && cartItem.CustomerEnteredPrice != decimal.Zero) { currentWarnings.Add(T("ShoppingCart.Bundle.NoCustomerEnteredPrice")); } // Not published or no permissions for customer or store - if (!product.Published - || !await _aclService.AuthorizeAsync(product, cartItem.Customer) - || !await _storeMappingService.AuthorizeAsync(product, storeId ?? _storeContext.CurrentStore.Id)) + if (!p.Published + || !await _aclService.AuthorizeAsync(p, cartItem.Customer) + || !await _storeMappingService.AuthorizeAsync(p, storeId ?? _storeContext.CurrentStore.Id)) { currentWarnings.Add(T("ShoppingCart.ProductUnpublished")); } // Disabled buy button - if (cartItem.ShoppingCartType == ShoppingCartType.ShoppingCart && product.DisableBuyButton) + if (cartItem.ShoppingCartType == ShoppingCartType.ShoppingCart && p.DisableBuyButton) { currentWarnings.Add(T("ShoppingCart.BuyingDisabled")); } // Disabled wishlist button - if (cartItem.ShoppingCartType == ShoppingCartType.Wishlist && product.DisableWishlistButton) + if (cartItem.ShoppingCartType == ShoppingCartType.Wishlist && p.DisableWishlistButton) { currentWarnings.Add(T("ShoppingCart.WishlistDisabled")); } // Call for price - if (cartItem.ShoppingCartType == ShoppingCartType.ShoppingCart && product.CallForPrice) + if (cartItem.ShoppingCartType == ShoppingCartType.ShoppingCart && p.CallForPrice) { currentWarnings.Add(T("Products.CallForPrice")); } // Customer entered price - if (product.CustomerEntersPrice && - (cartItem.CustomerEnteredPrice < product.MinimumCustomerEnteredPrice - || cartItem.CustomerEnteredPrice > product.MaximumCustomerEnteredPrice)) + if (p.CustomerEntersPrice && + (cartItem.CustomerEnteredPrice < p.MinimumCustomerEnteredPrice + || cartItem.CustomerEnteredPrice > p.MaximumCustomerEnteredPrice)) { - var min = _currencyService.ConvertToWorkingCurrency(product.MinimumCustomerEnteredPrice); - var max = _currencyService.ConvertToWorkingCurrency(product.MaximumCustomerEnteredPrice); + var min = _currencyService.ConvertToWorkingCurrency(p.MinimumCustomerEnteredPrice); + var max = _currencyService.ConvertToWorkingCurrency(p.MaximumCustomerEnteredPrice); currentWarnings.Add(T("ShoppingCart.CustomerEnteredPrice.RangeError", min, max)); } // Quantity validation var hasQuantityWarnings = false; - var quanitityToValidate = quantity ?? cartItem.Quantity; - if (quanitityToValidate <= 0) + var quantityToValidate = quantity ?? cartItem.Quantity; + if (quantityToValidate <= 0) { currentWarnings.Add(T("ShoppingCart.QuantityShouldPositive")); hasQuantityWarnings = true; } - if (quanitityToValidate < product.OrderMinimumQuantity) + if (quantityToValidate < p.OrderMinimumQuantity) { - currentWarnings.Add(T("ShoppingCart.MinimumQuantity", product.OrderMinimumQuantity)); + currentWarnings.Add(T("ShoppingCart.MinimumQuantity", p.OrderMinimumQuantity)); hasQuantityWarnings = true; } - if (quanitityToValidate > product.OrderMaximumQuantity) + if (quantityToValidate > p.OrderMaximumQuantity) { - currentWarnings.Add(T("ShoppingCart.MaximumQuantity", product.OrderMaximumQuantity)); + currentWarnings.Add(T("ShoppingCart.MaximumQuantity", p.OrderMaximumQuantity)); hasQuantityWarnings = true; } - var allowedQuantities = product.ParseAllowedQuantities(); - if (allowedQuantities.Length > 0 && !allowedQuantities.Contains(quanitityToValidate)) + var allowedQuantities = p.ParseAllowedQuantities(); + if (allowedQuantities.Length > 0 && !allowedQuantities.Contains(quantityToValidate)) { currentWarnings.Add(T("ShoppingCart.AllowedQuantities", string.Join(", ", allowedQuantities))); } // Stock validation - var validateOutOfStock = cartItem.ShoppingCartType == ShoppingCartType.ShoppingCart || !_cartSettings.AllowOutOfStockItemsToBeAddedToWishlist; - if (validateOutOfStock && !hasQuantityWarnings) + var validateStock = cartItem.ShoppingCartType == ShoppingCartType.ShoppingCart || !_cartSettings.AllowOutOfStockItemsToBeAddedToWishlist; + if (validateStock && !hasQuantityWarnings) { - switch (product.ManageInventoryMethod) + if (p.ManageInventoryMethod == ManageInventoryMethod.ManageStock && p.BackorderMode == BackorderMode.NoBackorders) { - case ManageInventoryMethod.ManageStock: + // INFO: bundles with per-item-pricing are always added in single positions (see ShoppingCart.FindItemInCart). + if (cartItems != null + && (_cartSettings.AddProductsToBasketInSinglePositions || (p.ProductType == ProductType.BundledProduct && p.BundlePerItemPricing))) { - if (product.BackorderMode != BackorderMode.NoBackorders || product.StockQuantity >= quanitityToValidate) - break; - - var warning = product.StockQuantity > 0 - ? T("ShoppingCart.QuantityExceedsStock", product.StockQuantity) - : T("ShoppingCart.OutOfStock"); + quantityToValidate += cartItems + .Select(x => x.Item) + .Where(x => x.ProductId == p.Id && x.ParentItemId == null) + .Sum(x => x.Quantity); + } - currentWarnings.Add(warning); + if (p.StockQuantity < quantityToValidate) + { + currentWarnings.Add(p.StockQuantity > 0 + ? T("ShoppingCart.QuantityExceedsStock", p.StockQuantity) + : T("ShoppingCart.OutOfStock")); } - break; - case ManageInventoryMethod.ManageStockByAttributes: + } + else if (p.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) + { + var combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(p.Id, cartItem.AttributeSelection); + if (combination != null && !combination.AllowOutOfStockOrders) { - var combination = await _productAttributeMaterializer.FindAttributeCombinationAsync(product.Id, cartItem.AttributeSelection); - if (combination == null || combination.AllowOutOfStockOrders || combination.StockQuantity >= quanitityToValidate) - break; - - var warning = combination.StockQuantity > 0 - ? T("ShoppingCart.QuantityExceedsStock", combination.StockQuantity) - : T("ShoppingCart.OutOfStock"); + if (cartItems != null && _cartSettings.AddProductsToBasketInSinglePositions) + { + quantityToValidate += cartItems + .Select(x => x.Item) + .Where(x => x.ProductId == p.Id && x.ParentItemId == null && x.AttributeSelection.Equals(cartItem.AttributeSelection)) + .Sum(x => x.Quantity); + } - currentWarnings.Add(warning); + if (combination.StockQuantity < quantityToValidate) + { + currentWarnings.Add(combination.StockQuantity > 0 + ? T("ShoppingCart.QuantityExceedsStock", combination.StockQuantity) + : T("ShoppingCart.OutOfStock")); + } } - break; - case ManageInventoryMethod.DontManageStock: - default: - break; } } // Validate availability - var availableStartDateError = false; - if (product.AvailableStartDateTimeUtc.HasValue) + var invalidStartDate = false; + if (p.AvailableStartDateTimeUtc.HasValue) { - var availableStartDate = DateTime.SpecifyKind(product.AvailableStartDateTimeUtc.Value, DateTimeKind.Utc); + var availableStartDate = DateTime.SpecifyKind(p.AvailableStartDateTimeUtc.Value, DateTimeKind.Utc); if (availableStartDate.CompareTo(DateTime.UtcNow) > 0) { currentWarnings.Add(T("ShoppingCart.NotAvailable")); - availableStartDateError = true; + invalidStartDate = true; } } - if (product.AvailableEndDateTimeUtc.HasValue && !availableStartDateError) + if (p.AvailableEndDateTimeUtc.HasValue && !invalidStartDate) { - var availableEndDate = DateTime.SpecifyKind(product.AvailableEndDateTimeUtc.Value, DateTimeKind.Utc); + var availableEndDate = DateTime.SpecifyKind(p.AvailableEndDateTimeUtc.Value, DateTimeKind.Utc); if (availableEndDate.CompareTo(DateTime.UtcNow) < 0) { currentWarnings.Add(T("ShoppingCart.NotAvailable")); @@ -600,7 +613,7 @@ public virtual async Task ValidateProductAsync(ShoppingCartItem cartItem, public virtual async Task ValidateRequiredProductsAsync(Product product, IEnumerable cartItems, IList warnings) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(product); if (!product.RequireOtherProducts) return true; diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.AssignableProducts.cs b/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.AssignableProducts.cs index 80fe807a95..8d4c3cfcaa 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.AssignableProducts.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.AssignableProducts.cs @@ -424,6 +424,7 @@ public async Task BundleItemList(GridCommand command, int product ProductName = x.Product.Name, ProductTypeName = x.Product.GetProductTypeLabel(Services.Localization), ProductTypeLabelHint = x.Product.ProductTypeLabelHint, + ProductEditUrl = Url.Action(nameof(ProductController.Edit), "Product", new { id = x.Product.Id, area = "Admin" }), Sku = x.Product.Sku, Quantity = x.Quantity, Discount = x.Discount, diff --git a/src/Smartstore.Web/Areas/Admin/Models/Catalog/ProductModel.cs b/src/Smartstore.Web/Areas/Admin/Models/Catalog/ProductModel.cs index 484f269c77..cd5264a679 100644 --- a/src/Smartstore.Web/Areas/Admin/Models/Catalog/ProductModel.cs +++ b/src/Smartstore.Web/Areas/Admin/Models/Catalog/ProductModel.cs @@ -417,6 +417,7 @@ public partial class BundleItemModel : EntityModelBase [LocalizedDisplay("Admin.Catalog.Products.Fields.ProductType")] public string ProductTypeName { get; set; } public string ProductTypeLabelHint { get; set; } + public string ProductEditUrl { get; set; } [LocalizedDisplay("Admin.Catalog.Products.Fields.Sku")] public string Sku { get; set; } diff --git a/src/Smartstore.Web/Areas/Admin/Views/Product/Grids/_Grid.BundleItems.cshtml b/src/Smartstore.Web/Areas/Admin/Views/Product/Grids/_Grid.BundleItems.cshtml index fd94bb0b6f..f97048f670 100644 --- a/src/Smartstore.Web/Areas/Admin/Views/Product/Grids/_Grid.BundleItems.cshtml +++ b/src/Smartstore.Web/Areas/Admin/Views/Product/Grids/_Grid.BundleItems.cshtml @@ -44,7 +44,7 @@ - @Html.LabeledProductName() + @Html.LabeledProductName(urlExpression: "item.row.ProductEditUrl", valueExpression: "item.row.ProductName") diff --git a/src/Smartstore.Web/Models/ShoppingCart/Mappers/CartItemMapperBase.cs b/src/Smartstore.Web/Models/ShoppingCart/Mappers/CartItemMapperBase.cs index bc4ba908ff..c0dbb1ecbf 100644 --- a/src/Smartstore.Web/Models/ShoppingCart/Mappers/CartItemMapperBase.cs +++ b/src/Smartstore.Web/Models/ShoppingCart/Mappers/CartItemMapperBase.cs @@ -46,8 +46,8 @@ public abstract class CartItemMapperBase : Mapper(); - - if (!await ShoppingCartValidator.ValidateProductAsync(from.Item, itemWarnings)) + if (!await ShoppingCartValidator.ValidateProductAsync(from.Item, null, itemWarnings)) { to.Warnings.AddRange(itemWarnings); } - var attrWarnings = new List(); var cart = await ShoppingCartService.GetCartAsync(customer, shoppingCartType, store.Id); - if (!await ShoppingCartValidator.ValidateProductAttributesAsync(item, cart.Items, attrWarnings)) + var attributeWarnings = new List(); + if (!await ShoppingCartValidator.ValidateProductAttributesAsync(item, cart.Items, attributeWarnings)) { - to.Warnings.AddRange(attrWarnings); + to.Warnings.AddRange(attributeWarnings); } } }