Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCC-163: PriceVariantsPart doesn't support InventoryPart #315

Merged
merged 37 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
18560c0
Working on reworking Inventory (moderately messy)
porgabi Jul 11, 2023
1d8b48d
Adding method to fake interface
porgabi Jul 11, 2023
e5b8d1c
Adding InventoryPart to Price Variant Product
porgabi Jul 11, 2023
ce66045
Adapting inventory management methods to dictionary inventories
porgabi Jul 12, 2023
6a53547
Adapting product availability displays to dictionary inventory
porgabi Jul 12, 2023
d6add07
Hiding unavailable Price Variant Products items
porgabi Jul 12, 2023
d93d52b
Handling item quantity verification
porgabi Jul 13, 2023
71a3330
Using new method
porgabi Jul 13, 2023
41b4007
Renaming property
porgabi Jul 13, 2023
9a22dd6
Handling out of stock message
porgabi Jul 14, 2023
24cc5c8
Adding editor for inventory values
porgabi Jul 14, 2023
43dde59
Updating inventory keys upon SKU change
porgabi Jul 14, 2023
db3162a
Updating Product's inventory to its SKU from "DEFAULT"
porgabi Jul 15, 2023
7dae3f8
Code styling adjustments
porgabi Jul 15, 2023
7dad30f
Also updating CanBeBought entry keys
porgabi Jul 17, 2023
ba50896
WIP something
porgabi Jul 17, 2023
382fc9b
Working on reworking inventory keys updating
porgabi Jul 17, 2023
8402424
Trying to resolve bug with dictionaries
porgabi Jul 17, 2023
9c2b900
Fixing user-facing variant availability display
porgabi Jul 18, 2023
b365173
Merge remote-tracking branch 'origin/main' into issue/OCC-163
porgabi Jul 19, 2023
eb80b01
Same
porgabi Jul 19, 2023
632c767
Removing
porgabi Jul 19, 2023
558ba25
Adding StringComparer to view model's property
porgabi Jul 19, 2023
8333b66
Finding workaround for giga dictionaries bug
porgabi Jul 20, 2023
86baf4c
Removing unnecessary code
porgabi Jul 20, 2023
9433356
Initializing new inventory when creating a new Product item
porgabi Jul 20, 2023
e0c267e
Eliminating small bug with workaround
porgabi Jul 20, 2023
d5eb05d
Completing workaround
porgabi Jul 20, 2023
3ef9267
Applying filtering in other driver as well
porgabi Jul 20, 2023
b7a9f81
Initializing new inventories in drivers
porgabi Jul 20, 2023
2468cf6
Removing unnecessary placement
porgabi Jul 21, 2023
b6ceb2a
Reverting
porgabi Jul 21, 2023
ab5c45f
Fixing out of stock message display conditions
porgabi Jul 21, 2023
c1b2150
Fixing submit button clickability
porgabi Jul 21, 2023
cda7ca1
Donating brain cell to spell-checking
porgabi Jul 21, 2023
39aef9e
Adding minor improvements
porgabi Jul 24, 2023
a9ecbf7
Adding inventory filtering extension method
porgabi Jul 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The InventoryKeys list property is used as a workaround to the incorrectly working dictionary property. For some reason, list properties do not retain cleared values (which is working as intended), while dictionaries do.

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;
Psichorex marked this conversation as resolved.
Show resolved Hide resolved

<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