Skip to content

Commit

Permalink
Resolves #990 Add a weight field to attribute combinations
Browse files Browse the repository at this point in the history
  • Loading branch information
mgesing committed Feb 20, 2025
1 parent 8ccdfbd commit 17af137
Show file tree
Hide file tree
Showing 17 changed files with 122 additions and 49 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Increased overall performance
- #1258 Added **DeepSeek** OpenAI provider.
- #1142 Implemented configurable Content-Security-Policy (CSP) HTTP header.
- #990 Add a weight field to attribute combinations.
- Added a setting to control whether shipping costs are displayed on cart page as long as the customer has not yet entered a shipping address.
- Shipping by total: Added a setting to let the shipping origin determine the shipping cost if the shipping address is missing.
- #501 New category option for ignoring a category in menus.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ public Product Product
/// </summary>
public decimal? Price { get; set; }

/// <summary>
/// Gets or sets the weight.
/// </summary>
public decimal? Weight { get; set; }

/// <summary>
/// Gets or sets the length.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public partial class ProductAttributeMaterializer : IProductAttributeMaterialize
private readonly IRequestCache _requestCache;
private readonly ICacheManager _cache;
private readonly Lazy<IDownloadService> _downloadService;
private readonly Lazy<CatalogSettings> _catalogSettings;
private readonly PerformanceSettings _performanceSettings;

public ProductAttributeMaterializer(
Expand All @@ -41,15 +40,13 @@ public ProductAttributeMaterializer(
IRequestCache requestCache,
ICacheManager cache,
Lazy<IDownloadService> downloadService,
Lazy<CatalogSettings> catalogSettings,
PerformanceSettings performanceSettings)
{
_db = db;
_httpContextAccessor = httpContextAccessor;
_requestCache = requestCache;
_cache = cache;
_downloadService = downloadService;
_catalogSettings = catalogSettings;
_performanceSettings = performanceSettings;
}

Expand Down
8 changes: 7 additions & 1 deletion src/Smartstore.Core/Catalog/Products/Domain/Product.cs
Original file line number Diff line number Diff line change
Expand Up @@ -724,10 +724,16 @@ public PriceLabel ComparePriceLabel
/// </summary>
public AttributeChoiceBehaviour AttributeChoiceBehaviour { get; set; }

private decimal _weight;
/// <summary>
/// Gets or sets the weight.
/// </summary>
public decimal Weight { get; set; }
public decimal Weight
{
[DebuggerStepThrough]
get => this.GetMergedDataValue(nameof(Weight), _weight);
set => _weight = value;
}

private decimal _length;
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,39 @@ public static void MergeWithCombination(this Product product, ProductVariantAttr

if (ManageInventoryMethod.ManageStockByAttributes == (ManageInventoryMethod)product.ManageInventoryMethodId)
{
values.Add("StockQuantity", combination.StockQuantity);
values.Add("BackorderModeId", combination.AllowOutOfStockOrders ? (int)BackorderMode.AllowQtyBelow0 : (int)BackorderMode.NoBackorders);
values.Add(nameof(Product.StockQuantity), combination.StockQuantity);
values.Add(nameof(Product.BackorderModeId), combination.AllowOutOfStockOrders ? (int)BackorderMode.AllowQtyBelow0 : (int)BackorderMode.NoBackorders);
}

if (combination.Sku.HasValue())
values.Add("Sku", combination.Sku);
values.Add(nameof(Product.Sku), combination.Sku);
if (combination.Gtin.HasValue())
values.Add("Gtin", combination.Gtin);
values.Add(nameof(Product.Gtin), combination.Gtin);
if (combination.ManufacturerPartNumber.HasValue())
values.Add("ManufacturerPartNumber", combination.ManufacturerPartNumber);
values.Add(nameof(Product.ManufacturerPartNumber), combination.ManufacturerPartNumber);

if (combination.Price.HasValue)
values.Add("Price", combination.Price.Value);
values.Add(nameof(Product.Price), combination.Price.Value);

if (combination.DeliveryTimeId.HasValue && combination.DeliveryTimeId.Value > 0)
values.Add("DeliveryTimeId", combination.DeliveryTimeId);
values.Add(nameof(Product.DeliveryTimeId), combination.DeliveryTimeId);

if (combination.QuantityUnitId.HasValue && combination.QuantityUnitId.Value > 0)
values.Add("QuantityUnitId", combination.QuantityUnitId);
values.Add(nameof(Product.QuantityUnitId), combination.QuantityUnitId);

if (combination.Weight.HasValue)
values.Add(nameof(Product.Weight), combination.Weight.Value);
if (combination.Length.HasValue)
values.Add("Length", combination.Length.Value);
values.Add(nameof(Product.Length), combination.Length.Value);
if (combination.Width.HasValue)
values.Add("Width", combination.Width.Value);
values.Add(nameof(Product.Width), combination.Width.Value);
if (combination.Height.HasValue)
values.Add("Height", combination.Height.Value);
values.Add(nameof(Product.Height), combination.Height.Value);

if (combination.BasePriceAmount.HasValue)
values.Add("BasePriceAmount", combination.BasePriceAmount);
values.Add(nameof(Product.BasePriceAmount), combination.BasePriceAmount);
if (combination.BasePriceBaseAmount.HasValue)
values.Add("BasePriceBaseAmount", combination.BasePriceBaseAmount);
values.Add(nameof(Product.BasePriceBaseAmount), combination.BasePriceBaseAmount);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,7 @@ protected virtual async Task ProcessAttributeCombinationsAsync(ImportExecuteCont
row.SetProperty(context.Result, (x) => x.ManufacturerPartNumber);
row.SetProperty(context.Result, (x) => x.StockQuantity, 10000);
row.SetProperty(context.Result, (x) => x.Price);
row.SetProperty(context.Result, (x) => x.Weight);
row.SetProperty(context.Result, (x) => x.Length);
row.SetProperty(context.Result, (x) => x.Width);
row.SetProperty(context.Result, (x) => x.Height);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ private async Task ProcessAttributes(DbContextScope scope, Product product, Prod
ManufacturerPartNumber = combination.ManufacturerPartNumber,
Price = combination.Price,
AssignedMediaFileIds = combination.AssignedMediaFileIds,
Weight = combination.Weight,
Length = combination.Length,
Width = combination.Width,
Height = combination.Height,
Expand Down
50 changes: 30 additions & 20 deletions src/Smartstore.Core/Checkout/Shipping/Services/ShippingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public virtual IEnumerable<Provider<IShippingRateComputationMethod>> LoadEnabled

_settingFactory.SaveSettingsAsync(_shippingSettings).GetAwaiter().GetResult();

return new Provider<IShippingRateComputationMethod>[] { fallbackProvider };
return [fallbackProvider];
}

if (DataSettings.DatabaseIsInstalled())
Expand Down Expand Up @@ -132,19 +132,24 @@ public virtual async Task<decimal> GetCartTotalWeightAsync(ShoppingCart cart, bo
{
Guard.NotNull(cart);

var cartWeight = await GetCartWeight(cart.Items, true, includeFreeShippingProducts);

// Checkout attributes.
if (cart.Customer != null)
var cacheKey = $"shipping-cart-total-weight:{cart.GetHashCode()}-{includeFreeShippingProducts}";
var cartWeight = await _requestCache.GetAsync(cacheKey, async () =>
{
var checkoutAttributes = cart.Customer.GenericAttributes.CheckoutAttributes;
if (checkoutAttributes.AttributesMap.Any())
{
var attributeValues = await _checkoutAttributeMaterializer.MaterializeCheckoutAttributeValuesAsync(checkoutAttributes);
var weight = await GetCartWeight(cart.Items, true, includeFreeShippingProducts);

cartWeight += attributeValues.Sum(x => x.WeightAdjustment);
// Checkout attributes.
if (cart.Customer != null)
{
var checkoutAttributes = cart.Customer.GenericAttributes.CheckoutAttributes;
if (checkoutAttributes.AttributesMap.Any())
{
var attributeValues = await _checkoutAttributeMaterializer.MaterializeCheckoutAttributeValuesAsync(checkoutAttributes);
weight += attributeValues.Sum(x => x.WeightAdjustment);
}
}
}

return weight;
});

return cartWeight;
}
Expand Down Expand Up @@ -255,7 +260,7 @@ private async Task<decimal> GetCartWeight(OrganizedShoppingCartItem[] cart, bool
}
else if (cart.Length == 1)
{
if (IgnoreCartItem(cart[0]))
if (IgnoreCartItem(cart[0].Item))
{
return decimal.Zero;
}
Expand All @@ -265,11 +270,11 @@ private async Task<decimal> GetCartWeight(OrganizedShoppingCartItem[] cart, bool
else
{
selection = new ProductVariantAttributeSelection(string.Empty);
foreach (var item in cart)
foreach (var item in cart.Select(x => x.Item))
{
if (!IgnoreCartItem(item))
{
foreach (var attribute in item.Item.AttributeSelection.AttributesMap)
foreach (var attribute in item.AttributeSelection.AttributesMap)
{
if (!attribute.Value.IsNullOrEmpty())
{
Expand All @@ -295,17 +300,22 @@ private async Task<decimal> GetCartWeight(OrganizedShoppingCartItem[] cart, bool
: [];

// INFO: always iterate cart items (not attribute values). Attributes can occur multiple times in cart.
foreach (var item in cart)
foreach (var item in cart.Select(x => x.Item))
{
if (IgnoreCartItem(item))
{
continue;
}

var itemWeight = item.Item.Product.Weight;
if (item.AttributeSelection.HasAttributes)
{
await _productAttributeMaterializer.MergeWithCombinationAsync(item.Product, item.AttributeSelection, null);
}

var itemWeight = item.Product.Weight;

// Add attributes weight.
foreach (var pair in item.Item.AttributeSelection.AttributesMap)
foreach (var pair in item.AttributeSelection.AttributesMap)
{
var integerValues = pair.Value
.Select(x => x.ToString())
Expand Down Expand Up @@ -335,17 +345,17 @@ private async Task<decimal> GetCartWeight(OrganizedShoppingCartItem[] cart, bool

if (multiplyByQuantity)
{
itemWeight *= item.Item.Quantity;
itemWeight *= item.Quantity;
}

cartWeight += itemWeight;
}

return cartWeight;

bool IgnoreCartItem(OrganizedShoppingCartItem cartItem)
bool IgnoreCartItem(ShoppingCartItem cartItem)
{
return !includeFreeShippingProducts && cartItem.Item.Product.IsFreeShipping;
return !includeFreeShippingProducts && cartItem.Product.IsFreeShipping;
}

static string CreateAttributeKey(int productVariantAttributeId, int productVariantAttributeValueId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using FluentMigrator;
using Smartstore.Core.Catalog.Attributes;

namespace Smartstore.Core.Data.Migrations
{
[MigrationVersion("2025-02-20 10:30:00", "Core: Attribute combination weight")]
internal class AttributeCombinationWeight : Migration
{
const string AttributeCombinationTable = nameof(ProductVariantAttributeCombination);
const string WeightColumn = nameof(ProductVariantAttributeCombination.Weight);

public override void Up()
{
if (!Schema.Table(AttributeCombinationTable).Column(WeightColumn).Exists())
{
Create.Column(WeightColumn).OnTable(AttributeCombinationTable)
.AsDecimal(18, 4)
.Nullable();
}
}

public override void Down()
{
if (Schema.Table(AttributeCombinationTable).Column(WeightColumn).Exists())
{
Delete.Column(WeightColumn).FromTable(AttributeCombinationTable);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,7 @@ private void WriteAttributes(dynamic product)
_writer.WriteElementString(nameof(ProductVariantAttributeCombination.ManufacturerPartNumber), entityPvac.ManufacturerPartNumber);

_writer.WriteElementString(nameof(ProductVariantAttributeCombination.Price), entityPvac.Price?.ToString(_culture) ?? string.Empty);
_writer.WriteElementString(nameof(ProductVariantAttributeCombination.Price), entityPvac.Price?.ToString(_culture) ?? string.Empty);
_writer.WriteElementString(nameof(ProductVariantAttributeCombination.Weight), entityPvac.Weight?.ToString(_culture) ?? string.Empty);
_writer.WriteElementString(nameof(ProductVariantAttributeCombination.Length), entityPvac.Length?.ToString(_culture) ?? string.Empty);
_writer.WriteElementString(nameof(ProductVariantAttributeCombination.Width), entityPvac.Width?.ToString(_culture) ?? string.Empty);
_writer.WriteElementString(nameof(ProductVariantAttributeCombination.Height), entityPvac.Height?.ToString(_culture) ?? string.Empty);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1062,11 +1062,13 @@ private async Task PrepareProductAttributeCombinationModel(
Guard.NotNull(model);
Guard.NotNull(product);

var baseWeight = await _db.MeasureWeights.FindByIdAsync(_measureSettings.BaseWeightId, false);
var baseDimension = await _db.MeasureDimensions.FindByIdAsync(_measureSettings.BaseDimensionId);

model.ProductId = product.Id;
model.PrimaryStoreCurrencyCode = _currencyService.PrimaryCurrency.CurrencyCode;
model.BaseWeightIn = baseWeight?.GetLocalized(x => x.Name) ?? string.Empty;
model.BaseDimensionIn = baseDimension?.GetLocalized(x => x.Name) ?? string.Empty;
model.PrimaryStoreCurrencyCode = _currencyService.PrimaryCurrency.CurrencyCode;

if (entity != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1755,11 +1755,11 @@ private async Task PrepareProductModelAsync(ProductModel model, Product product,
ViewBag.NumberOfAttributeCombinations = 0;
}

var measure = await _db.MeasureWeights.FindByIdAsync(_measureSettings.BaseWeightId, false);
var dimension = await _db.MeasureDimensions.FindByIdAsync(_measureSettings.BaseDimensionId, false);
var baseWeight = await _db.MeasureWeights.FindByIdAsync(_measureSettings.BaseWeightId, false);
var baseDimension = await _db.MeasureDimensions.FindByIdAsync(_measureSettings.BaseDimensionId, false);

model.BaseWeightIn = measure?.GetLocalized(x => x.Name) ?? string.Empty;
model.BaseDimensionIn = dimension?.GetLocalized(x => x.Name) ?? string.Empty;
model.BaseWeightIn = baseWeight?.GetLocalized(x => x.Name) ?? string.Empty;
model.BaseDimensionIn = baseDimension?.GetLocalized(x => x.Name) ?? string.Empty;

model.NumberOfAvailableProductAttributes = await _db.ProductAttributes.CountAsync();
model.NumberOfAvailableManufacturers = await _db.Manufacturers.CountAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ public class ProductVariantAttributeCombinationModel : EntityModelBase
public int? QuantityUnitId { get; set; }

[LocalizedDisplay("*Pictures")]
public int[] AssignedPictureIds { get; set; } = Array.Empty<int>();
public int[] AssignedPictureIds { get; set; } = [];

public List<PictureSelectItemModel> AssignablePictures { get; set; } = [];

[LocalizedDisplay("Admin.Catalog.Products.Fields.Weight")]
public decimal? Weight { get; set; }

[LocalizedDisplay("Admin.Catalog.Products.Fields.Length")]
public decimal? Length { get; set; }

Expand Down Expand Up @@ -70,6 +73,7 @@ public class ProductVariantAttributeCombinationModel : EntityModelBase
public int EntityIndex { get; set; }
public string PrimaryStoreCurrencyCode { get; set; }
public string BaseDimensionIn { get; set; }
public string BaseWeightIn { get; set; }

#region Nested classes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
<column for="Price" halign="center" width="1fr" />
<column for="StockQuantity" halign="center" />
<column for="AllowOutOfStockOrders" halign="center" />
<column for="Weight" halign="center" visible="false" />
<column for="Length" halign="center" visible="false" />
<column for="Width" halign="center" visible="false" />
<column for="Height" halign="center" visible="false" />
</columns>
<row-commands>
<a datarow-action="DataRowAction.Custom" :href="item.row.ProductUrl">@T("Common.OpenInShop")</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,16 @@
<div class="adminRow">
<div class="adminSeparator"><hr /></div>
</div>
<div class="adminRow" id="pnlLength">
<div class="adminRow">
<div class="adminTitle">
<smart-label asp-for="Weight" />
</div>
<div class="adminData">
<editor asp-for="Weight" sm-postfix="@Model.BaseWeightIn" />
<span asp-validation-for="Weight"></span>
</div>
</div>
<div class="adminRow">
<div class="adminTitle">
<smart-label asp-for="Length" />
</div>
Expand All @@ -174,7 +183,7 @@
<span asp-validation-for="Length"></span>
</div>
</div>
<div class="adminRow" id="pnlWidth">
<div class="adminRow">
<div class="adminTitle">
<smart-label asp-for="Width" />
</div>
Expand All @@ -183,7 +192,7 @@
<span asp-validation-for="Width"></span>
</div>
</div>
<div class="adminRow" id="pnlHeight">
<div class="adminRow">
<div class="adminTitle">
<smart-label asp-for="Height" />
</div>
Expand Down
Loading

0 comments on commit 17af137

Please sign in to comment.