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);
}
}
}