Skip to content

Commit

Permalink
OCC-163: PriceVariantsPart doesn't support InventoryPart (#315)
Browse files Browse the repository at this point in the history
* Working on reworking Inventory (moderately messy)

* Adding method to fake interface

* Adding InventoryPart to Price Variant Product

* Adapting inventory management methods to dictionary inventories

* Adapting product availability displays to dictionary inventory

* Hiding unavailable Price Variant Products items

* Handling item quantity verification

* Using new method

* Renaming property

* Handling out of stock message

* Adding editor for inventory values

* Updating inventory keys upon SKU change

* Updating Product's inventory to its SKU from "DEFAULT"

* Code styling adjustments

* Also updating CanBeBought entry keys

* WIP something

* Working on reworking inventory keys updating

* Trying to resolve bug with dictionaries

* Fixing user-facing variant availability display

* Same

* Removing

* Adding StringComparer to view model's property

* Finding workaround for giga dictionaries bug

* Removing unnecessary code

* Initializing new inventory when creating a new Product item

* Eliminating small bug with workaround

* Completing workaround

* Applying filtering in other driver as well

* Initializing new inventories in drivers

* Removing unnecessary placement

* Reverting

* Fixing out of stock message display conditions

* Fixing submit button clickability

* Donating brain cell to spell-checking

* Adding minor improvements

* Adding inventory filtering extension method
  • Loading branch information
porgabi authored Jul 24, 2023
1 parent 0133185 commit d8bf8f2
Show file tree
Hide file tree
Showing 29 changed files with 445 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Http;
using OrchardCore.Commerce.Inventory.Models;
using OrchardCore.Commerce.Inventory.ViewModels;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.ContentManagement.Display.Models;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace OrchardCore.Commerce.Inventory.Drivers;

public class InventoryPartDisplayDriver : ContentPartDisplayDriver<InventoryPart>
{
private readonly IHttpContextAccessor _hca;

public InventoryPartDisplayDriver(IHttpContextAccessor hca) => _hca = hca;

public override IDisplayResult Display(InventoryPart part, BuildPartDisplayContext context) =>
Initialize<InventoryPartViewModel>(GetDisplayShapeType(context), viewModel => BuildViewModel(viewModel, part))
.Location("Detail", "Content:26")
.Location("Summary", "Meta");

public override IDisplayResult Edit(InventoryPart part, BuildPartEditorContext context) =>
Initialize<InventoryPartViewModel>(GetEditorShapeType(context), viewModel => BuildViewModel(viewModel, part));

public override async Task<IDisplayResult> UpdateAsync(
InventoryPart part,
IUpdateModel updater,
UpdatePartEditorContext context)
{
var viewModel = new InventoryPartViewModel();
if (await updater.TryUpdateModelAsync(viewModel, Prefix))
{
var currentSku = _hca.HttpContext.Request.Form["ProductPart.Sku"].ToString().ToUpperInvariant();
var skuBefore = viewModel.Inventory.FirstOrDefault().Key != null
? viewModel.Inventory.FirstOrDefault().Key.Split("-").First()
: "DEFAULT";

part.Inventory.Clear();
part.Inventory.AddRange(viewModel.Inventory);

// If SKU was changed, inventory keys need to be updated.
if (!string.IsNullOrEmpty(currentSku) && currentSku != skuBefore)
{
part.InventoryKeys.Clear();

var newInventory = new Dictionary<string, int>();
var oldInventory = part.Inventory.ToDictionary(key => key.Key, value => value.Value);
foreach (var inventoryEntry in oldInventory)
{
var updatedKey = oldInventory.Count > 1
? currentSku + "-" + inventoryEntry.Key.Split('-').Last()
: currentSku;

part.Inventory.Remove(inventoryEntry.Key);
newInventory.Add(updatedKey, inventoryEntry.Value);

part.InventoryKeys.Add(updatedKey);
}

part.Inventory.Clear();
part.Inventory.AddRange(newInventory);
}

part.ProductSku = currentSku;
}

return await EditAsync(part, context);
}

// Despite the Clear() calls inside UpdateAsync(), the Inventory property retains its old values along with the
// new ones, hence the filtering below.
private static void BuildViewModel(InventoryPartViewModel model, InventoryPart part)
{
var inventory = part.Inventory ?? new Dictionary<string, int>();
if (inventory.Any())
{
// Workaround for InventoryPart storing the outdated inventory entries along with the updated ones.
var filteredInventory = part.Inventory.FilterOutdatedEntries(part.InventoryKeys);

model.Inventory.AddRange(filteredInventory);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Linq;

namespace OrchardCore.Commerce.Inventory;

public static class InventoryDictionaryExtensions
{
public static IDictionary<string, int> FilterOutdatedEntries(
this IDictionary<string, int> inventory, IList<string> inventoryKeys) =>
inventory
.Where(inventory => inventoryKeys.Contains(inventory.Key))
.ToDictionary(key => key.Key, value => value.Value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,6 @@ public int Create()
Hint = "Makes it so Inventory is ignored (same as if no InventoryPart was present). Useful for digital products for example.",
})
)
.WithField(part => part.Inventory, field => field
.WithDisplayName("Inventory")
.WithSettings(new NumericFieldSettings
{
Hint = "The number of items in stock.",
})
)
.WithField(part => part.MaximumOrderQuantity, field => field
.WithDisplayName("Maximum Order Quantity")
.WithSettings(new NumericFieldSettings
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
using OrchardCore.ContentFields.Fields;
using OrchardCore.ContentManagement;
using System;
using System.Collections.Generic;

namespace OrchardCore.Commerce.Inventory.Models;

public class InventoryPart : ContentPart
{
public BooleanField AllowsBackOrder { get; set; } = new();
public BooleanField IgnoreInventory { get; set; } = new();
public NumericField Inventory { get; set; } = new();

public IDictionary<string, int> Inventory { get; } = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

public NumericField MaximumOrderQuantity { get; set; } = new();
public NumericField MinimumOrderQuantity { get; set; } = new();
public HtmlField OutOfStockMessage { get; set; } = new();

public IList<string> InventoryKeys { get; } = new List<string>();

public string ProductSku { get; set; }
}
3 changes: 3 additions & 0 deletions src/Modules/OrchardCore.Commerce.Inventory/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Commerce.Inventory.Drivers;
using OrchardCore.Commerce.Inventory.Migrations;
using OrchardCore.Commerce.Inventory.Models;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.Modules;

namespace OrchardCore.Commerce.Inventory;
Expand All @@ -10,5 +12,6 @@ public class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services) =>
services.AddContentPart<InventoryPart>()
.UseDisplayDriver<InventoryPartDisplayDriver>()
.WithMigration<InventoryPartMigrations>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;

namespace OrchardCore.Commerce.Inventory.ViewModels;

public class InventoryPartViewModel
{
public bool AllowsBackOrder { get; set; }
public bool IgnoreInventory { get; set; }

public IDictionary<string, int> Inventory { get; } = new Dictionary<string, int>();

public int MaximumOrderQuantity { get; set; }
public int MinimumOrderQuantity { get; set; }
public string OutOfStockMessage { get; set; }
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
@{
if (Model.Part.Inventory.Value is not decimal inventory || inventory > 0) { return; }
if (Model.Part.Inventory is not IDictionary<string, int> inventory) { return; }

// If any inventories contain items or back ordering is allowed, the product is not out of stock.
foreach (var inventoryEntry in inventory)
{
if (inventoryEntry.Value > 0 || Model.Part.AllowsBackOrder.Value) { return; }
}

var html = Model.Part.OutOfStockMessage.Html is string htmlString && !string.IsNullOrWhiteSpace(htmlString)
? htmlString
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@model InventoryPartViewModel

<h3>@T["Inventories"]</h3>
<div class="row">
@foreach (var inventoryKey in Model.Inventory.Keys)
{
var inventoryTitle = Model.Inventory.Count > 1 ? inventoryKey.Split('-').Last() : string.Empty;

<div class="col-md-3">
<div class="mb-3" asp-validation-class-for="Inventory[inventoryKey]">
<label asp-for="Inventory[inventoryKey]">@inventoryTitle @T["Inventory"] </label>
<div class="input-group">
<input asp-for="Inventory[inventoryKey]" class="form-control text-muted" />
</div>
<span asp-validation-for="Inventory[inventoryKey]"></span>
</div>
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@model InventoryPartViewModel

@if (Model.Inventory is { } inventory)
{
<div class="pb-3 field field-type-numericfield field-name-inventory-part-inventory">
@foreach (var inventoryEntry in inventory)
{
var inventoryTitle = inventory.Count > 1 ? inventoryEntry.Key.Split('-').Last() : string.Empty;

<span class="d-block w-100">
<strong class="field-name-inventory-part-inventory-title">@inventoryTitle @T["Inventory"]:</strong>
@T["{0}", inventoryEntry.Value]
</span>
}
</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
@addTagHelper *, OrchardCore.DisplayManagement
@addTagHelper *, OrchardCore.ResourceManagement

@using OrchardCore.Commerce.Inventory.ViewModels;
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
namespace OrchardCore.Commerce.Abstractions;

/// <summary>
/// Contains inventory management related methods.
/// Contains methods for inventory management.
/// </summary>
public interface IProductInventoryProvider : ISortableUpdaterProvider<IList<ShoppingCartItem>>
{
/// <summary>
/// Returns the current inventory count.
/// Returns the current count of all inventories.
/// </summary>
Task<int> QueryInventoryAsync(string sku);
Task<IDictionary<string, int>> QueryAllInventoriesAsync(string sku);

/// <summary>
/// Returns the current count of a specific inventory.
/// </summary>
Task<int> QueryInventoryAsync(string sku, string fullSku = null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public interface IProductService
/// </summary>
string GetVariantKey(string sku);

/// <summary>
/// Returns the full SKU of a Price Variant Product's variant.
/// </summary>
string GetOrderFullSku(ShoppingCartItem item, ProductPart productPart);

/// <summary>
/// Returns the exact variant of a product, as well as its identifying key, associated with the provided SKU.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Microsoft.Extensions.Options;
using OrchardCore.Commerce.Abstractions;
using OrchardCore.Commerce.Inventory.Models;
using OrchardCore.Commerce.Models;
using OrchardCore.Commerce.MoneyDataType;
using OrchardCore.Commerce.MoneyDataType.Abstractions;
using OrchardCore.Commerce.Settings;
using OrchardCore.Commerce.ViewModels;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.ContentManagement.Display.Models;
using OrchardCore.ContentManagement.Utilities;
Expand Down Expand Up @@ -99,5 +101,15 @@ private void BuildViewModel(PriceVariantsPartViewModel model, PriceVariantsPart
: _currencyOptions.Value.CurrentDisplayCurrency);

model.InitializeVariants(variants, values, currencies);

// When creating a new PriceVariantsProduct item, initialize default inventories.
if (part.ContentItem.As<InventoryPart>() is { } inventoryPart && !inventoryPart.Inventory.Any())
{
foreach (var variantKey in allVariantsKeys)
{
inventoryPart.Inventory.Add(variantKey, 0);
inventoryPart.InventoryKeys.Add(variantKey);
}
}
}
}
Loading

0 comments on commit d8bf8f2

Please sign in to comment.