Make metric variable

This was somehow overlooked, but it should work.

This needs to change how the InventoryMutateForm handles quantities, as
the amount has to be split from the metric for addition to work.
This commit is contained in:
Simon Bruder 2024-01-07 00:32:52 +01:00
parent e37c2506b8
commit 7365b384e3
Signed by: simon
GPG key ID: 8D3C82F9F309F8EC
13 changed files with 93 additions and 40 deletions

View file

@ -1,5 +1,5 @@
' SPDX-License-Identifier: AGPL-3.0-or-later
' SPDX-FileCopyrightText: 2023 swt23w23
' SPDX-FileCopyrightText: 2023-2024 swt23w23
@startuml
skinparam linetype ortho
skinparam groupInheritance 2
@ -57,7 +57,7 @@ package catering.catalog {
class Consumable {
- promotionPrice : MonetaryAmount
- wholesalePrice : MonetaryAmount
+ Consumable(name : String, retailPrice : MonetaryAmount, wholesalePrice : MonetaryAmount, promotionPrice : Optional<MonetaryAmount>, Set<OrderType> categories) : Consumable
+ Consumable(name : String, retailPrice : MonetaryAmount, wholesalePrice : MonetaryAmount, promotionPrice : Optional<MonetaryAmount>, Set<OrderType> categories, metric : Metric) : Consumable
+ getPrice() : MonetaryAmount
+ getRetailPrice() : MonetaryAmount
+ setRetailPrice(price : MonetaryAmount) : void
@ -74,7 +74,7 @@ package catering.catalog {
class Rentable {
- wholesalePrice : MonetaryAmount
+ Rentable(name : String, pricePerHour : MonetaryAmount, wholesalePrice : MonetaryAmount, Set<OrderType> categories) : Rentable
+ Rentable(name : String, pricePerHour : MonetaryAmount, wholesalePrice : MonetaryAmount, Set<OrderType> categories, metric : Metric) : Rentable
+ getWholesalePrice() : MonetaryAmount
+ setWholesalePrice(price : MonetaryAmount)
}

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 swt23w23
// SPDX-FileCopyrightText: 2023-2024 swt23w23
package catering.catalog;
import static org.salespointframework.core.Currencies.EURO;
@ -9,6 +9,7 @@ import java.util.Optional;
import org.javamoney.moneta.Money;
import org.salespointframework.core.DataInitializer;
import org.salespointframework.quantity.Metric;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@ -32,21 +33,32 @@ class CatalogDataInitializer implements DataInitializer {
return;
}
// !!! These need to be kept in sync with CatalogUnitTests
cateringCatalog.save(new Consumable(
"Brötchen Vollkorn",
Money.of(1, EURO),
Money.of(0.5, EURO),
Optional.of(Money.of(0.75, EURO)),
Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST)));
Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST),
Metric.UNIT));
cateringCatalog.save(new Consumable(
"Tafelwasser",
Money.of(5, EURO),
Money.of(0.01, EURO),
Optional.empty(),
Set.of(OrderType.PARTY_SERVICE, OrderType.EVENT_CATERING),
Metric.LITER));
cateringCatalog.save(new Rentable(
"Kerze Rot",
Money.of(2, EURO),
Money.of(1.5, EURO),
Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST)));
Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST),
Metric.UNIT));
cateringCatalog.save(new Rentable(
"Brotschneidemaschine Power X 3000",
Money.of(25, EURO),
Money.of(10000, EURO),
Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST)));
Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST),
Metric.UNIT));
}
}

View file

@ -9,6 +9,7 @@ import java.util.Set;
import javax.money.MonetaryAmount;
import org.salespointframework.catalog.Product;
import org.salespointframework.quantity.Metric;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
@ -26,8 +27,8 @@ public class Consumable extends Product {
}
public Consumable(String name, MonetaryAmount price, MonetaryAmount wholesalePrice,
Optional<MonetaryAmount> promotionPrice, Set<OrderType> categories) {
super(name, price);
Optional<MonetaryAmount> promotionPrice, Set<OrderType> categories, Metric metric) {
super(name, price, metric);
Assert.notNull(wholesalePrice, "wholesalePrice must not be null!");
Assert.notNull(promotionPrice, "promotionPrice must not be null!");
this.wholesalePrice = wholesalePrice;

View file

@ -8,6 +8,7 @@ import java.util.Set;
import javax.money.MonetaryAmount;
import org.salespointframework.catalog.Product;
import org.salespointframework.quantity.Metric;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
@ -25,8 +26,8 @@ public class Rentable extends Product {
}
public Rentable(String name, MonetaryAmount pricePerHour, MonetaryAmount wholesalePrice,
Set<OrderType> categories) {
super(name, pricePerHour);
Set<OrderType> categories, Metric metric) {
super(name, pricePerHour, metric);
this.wholesalePrice = wholesalePrice;
Assert.notNull(pricePerHour, "pricePerHour must not be null!");
Assert.notNull(wholesalePrice, "wholesalePrice must not be null!");

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 swt23w23
// SPDX-FileCopyrightText: 2023-2024 swt23w23
package catering.inventory;
import static org.salespointframework.core.Currencies.EURO;
@ -44,13 +44,15 @@ class ConsumableMutateForm extends InventoryMutateForm {
Money.of(getRetailPrice(), EURO),
Money.of(getWholesalePrice(), EURO),
getPromotionPrice().map(price -> Money.of(price, EURO)),
getOrderTypes());
getOrderTypes(),
getMetric());
}
public static ConsumableMutateForm of(Consumable product, UniqueInventoryItem item) {
ConsumableMutateForm form = new ConsumableMutateForm();
form.setName(product.getName());
form.setQuantity(item.getQuantity());
form.setQuantity(item.getQuantity().getAmount().longValueExact());
form.setMetric(product.createQuantity(0).getMetric()); // hack
form.setOrderTypes(orderTypesFromCategories(product.getCategories()));
form.setWholesalePrice(product.getWholesalePrice().getNumber().doubleValueExact());
form.setRetailPrice(product.getRetailPrice().getNumber().doubleValueExact());

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 swt23w23
// SPDX-FileCopyrightText: 2023-2024 swt23w23
package catering.inventory;
import org.salespointframework.catalog.Product;
@ -163,7 +163,7 @@ class InventoryController {
UniqueInventoryItem item = inventory.findByProduct(product).get();
// no setQuantity in enterprise java
// (though returing a modified object is actually nice)
inventory.save(item.increaseQuantity(form.getQuantity().subtract(item.getQuantity())));
inventory.save(item.increaseQuantity(product.createQuantity(form.getQuantity()).subtract(item.getQuantity())));
return "redirect:/inventory";
}
@ -246,7 +246,9 @@ class InventoryController {
if (result.hasErrors()) {
return add(model, form);
}
inventory.save(new UniqueInventoryItem(cateringCatalog.save(form.toProduct()), form.getQuantity()));
Product product = form.toProduct();
inventory.save(
new UniqueInventoryItem(cateringCatalog.save(product), product.createQuantity(form.getQuantity())));
return "redirect:/inventory";
}

View file

@ -1,11 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 swt23w23
// SPDX-FileCopyrightText: 2023-2024 swt23w23
package catering.inventory;
import org.salespointframework.core.DataInitializer;
import org.salespointframework.inventory.UniqueInventory;
import org.salespointframework.inventory.UniqueInventoryItem;
import org.salespointframework.quantity.Quantity;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@ -28,7 +27,7 @@ class InventoryInitializer implements DataInitializer {
public void initialize() {
cateringCatalog.findAll().forEach(product -> {
if (inventory.findByProduct(product).isEmpty()) {
inventory.save(new UniqueInventoryItem(product, Quantity.of(10)));
inventory.save(new UniqueInventoryItem(product, product.createQuantity(10)));
}
});
}

View file

@ -1,9 +1,11 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 swt23w23
// SPDX-FileCopyrightText: 2023-2024 swt23w23
package catering.inventory;
import static org.salespointframework.core.Currencies.EURO;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -12,6 +14,7 @@ import java.util.stream.Stream;
import org.javamoney.moneta.Money;
import org.salespointframework.catalog.Product;
import org.salespointframework.inventory.UniqueInventoryItem;
import org.salespointframework.quantity.Metric;
import org.salespointframework.quantity.Quantity;
import org.springframework.data.util.Streamable;
@ -32,9 +35,10 @@ import jakarta.validation.constraints.PositiveOrZero; // NonNegative in enterpri
*/
abstract class InventoryMutateForm {
private @NotEmpty String name;
private @NotNull Quantity quantity;
private @NotNull @PositiveOrZero Long quantity;
private @NotNull @PositiveOrZero Double retailPrice;
private @NotNull Set<OrderType> orderTypes;
private @NotNull Metric metric = Metric.UNIT;
public InventoryMutateForm() {
}
@ -43,7 +47,7 @@ abstract class InventoryMutateForm {
return name;
}
public Quantity getQuantity() {
public Long getQuantity() {
return quantity;
}
@ -51,6 +55,10 @@ abstract class InventoryMutateForm {
return retailPrice;
}
public Metric getMetric() {
return metric;
}
public void setName(String name) {
this.name = name;
}
@ -59,7 +67,7 @@ abstract class InventoryMutateForm {
return orderTypes;
}
public void setQuantity(Quantity quantity) {
public void setQuantity(Long quantity) {
this.quantity = quantity;
}
@ -71,6 +79,10 @@ abstract class InventoryMutateForm {
this.orderTypes = orderTypes;
}
public void setMetric(Metric metric) {
this.metric = metric;
}
/**
* Creates an empty {@link InventoryMutateForm} for {@link Product}s of the given {@link Class}.
*
@ -158,4 +170,8 @@ abstract class InventoryMutateForm {
.collect(Collectors.toSet())::contains)
.map(OrderType::valueOf).collect(Collectors.toSet());
}
public Collection<Metric> supportedMetrics() {
return List.of(Metric.UNIT, Metric.LITER, Metric.KILOGRAM);
}
}

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 swt23w23
// SPDX-FileCopyrightText: 2023-2024 swt23w23
package catering.inventory;
import static org.salespointframework.core.Currencies.EURO;
@ -26,7 +26,8 @@ class RentableMutateForm extends InventoryMutateForm {
public static RentableMutateForm of(Rentable product, UniqueInventoryItem item) {
RentableMutateForm form = new RentableMutateForm();
form.setName(product.getName());
form.setQuantity(item.getQuantity());
form.setQuantity(item.getQuantity().getAmount().longValueExact());
form.setMetric(product.createQuantity(0).getMetric()); // hack
form.setOrderTypes(orderTypesFromCategories(product.getCategories()));
form.setWholesalePrice(product.getWholesalePrice().getNumber().doubleValueExact());
form.setRetailPrice(product.getRetailPrice().getNumber().doubleValueExact());
@ -39,7 +40,8 @@ class RentableMutateForm extends InventoryMutateForm {
getName(),
Money.of(getRetailPrice(), EURO),
Money.of(getWholesalePrice(), EURO),
getOrderTypes());
getOrderTypes(),
getMetric());
}
@Override

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 swt23w23
// SPDX-FileCopyrightText: 2023-2024 swt23w23
package catering.order;
import catering.catalog.CateringCatalog;
@ -178,7 +178,7 @@ public class OrderController {
return "redirect:/event";
}
Quantity amount = Quantity.of(number > 0 ? number : 1);
Quantity amount = product.createQuantity(number > 0 ? number : 1);
Quantity cartQuantity = cart.getQuantity(product);
Quantity available;

View file

@ -1,6 +1,6 @@
<!--/*-->
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-FileCopyrightText: 2023 swt23w23
SPDX-FileCopyrightText: 2023-2024 swt23w23
<!--*/-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
@ -22,9 +22,21 @@ SPDX-FileCopyrightText: 2023 swt23w23
</div>
<div class="mb-3">
<label class="form-label" for="quantity">Menge im Bestand</label>
<input class="form-control" type="number" th:field="*{quantity}" th:errorclass="is-invalid" required/>
<div th:if="${#fields.hasErrors('quantity')}" class="invalid-feedback">Ungültige Menge.</div>
<div class="input-group" th:classappend="${#fields.hasErrors('quantity') ? 'has-validation' : ''}">
<input class="form-control" type="number" th:field="*{quantity}" th:errorclass="is-invalid" min="0" required/>
<span th:if="${!actionIsAdd}" class="input-group-text" th:text="${form.metric.getAbbreviation()}"></span>
<div th:if="${#fields.hasErrors('quantity')}" class="invalid-feedback">Ungültige Menge.</div>
</div>
</div>
<div class="mb-3" th:if="${actionIsAdd}">
<label class="form-label" for="metric">Einheit</label>
<select class="form-select" th:field="*{metric}" th:errorclass="is-invalid">
<option th:each="metric : ${form.supportedMetrics()}" th:value="${metric}" th:text="${metric.getAbbreviation()}"/>
</select>
<div th:if="${#fields.hasErrors('metric')}" class="invalid-feedback">Ungültige Einheit.</div>
</div>
<!--/* ensures metric on form is synced (should not be trusted, however) */-->
<input th:if="${!actionIsAdd}" class="d-none" type="text" th:field="*{metric}">
<!-- the prices arent bound with th:field as they need special formatting -->
<div class="mb-3">
<label class="form-label" for="wholesalePrice">Einkaufspreis</label>

View file

@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.*;
import static org.salespointframework.core.Currencies.EURO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.salespointframework.quantity.Metric;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
@ -23,20 +24,21 @@ class CatalogUnitTests {
@Test
void findByCategories() {
// !!! These need to be kept in sync with CatalogDataIntializer
assertThat(cateringCatalog.findRentablesByCategoriesContains(OrderType.EVENT_CATERING.toString())).hasSize(2);
assertThat(cateringCatalog.findRentablesByCategoriesContains(OrderType.MOBILE_BREAKFAST.toString())).hasSize(2);
assertThat(cateringCatalog.findRentablesByCategoriesContains(OrderType.RENT_A_COOK.toString())).hasSize(0);
assertThat(cateringCatalog.findRentablesByCategoriesContains(OrderType.PARTY_SERVICE.toString())).hasSize(0);
assertThat(cateringCatalog.findConsumablesByCategoriesContains(OrderType.EVENT_CATERING.toString())).hasSize(1);
assertThat(cateringCatalog.findConsumablesByCategoriesContains(OrderType.EVENT_CATERING.toString())).hasSize(2);
assertThat(cateringCatalog.findConsumablesByCategoriesContains(OrderType.MOBILE_BREAKFAST.toString())).hasSize(1);
assertThat(cateringCatalog.findConsumablesByCategoriesContains(OrderType.RENT_A_COOK.toString())).hasSize(0);
assertThat(cateringCatalog.findConsumablesByCategoriesContains(OrderType.PARTY_SERVICE.toString())).hasSize(0);
assertThat(cateringCatalog.findConsumablesByCategoriesContains(OrderType.PARTY_SERVICE.toString())).hasSize(1);
}
@Test
void findConsumables() {
assertThat(cateringCatalog.findConsumables()).hasSize(1);
assertThat(cateringCatalog.findConsumables()).hasSize(2);
}
@Test
@ -56,7 +58,8 @@ class CatalogUnitTests {
Money.of(1.5, EURO),
Money.of(0.7, EURO),
Optional.of(Money.of(0.90, EURO)),
Set.of(OrderType.MOBILE_BREAKFAST)));
Set.of(OrderType.MOBILE_BREAKFAST),
Metric.UNIT));
assertThat(cateringCatalog.findAll().stream().count()).isEqualTo(1 + countAllBefore);
assertThat(cateringCatalog.findConsumables().stream().count()).isEqualTo(1 + countConsumablesBefore);
assertThat(cateringCatalog.findConsumables()).contains(addedConsumable);

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 swt23w23
// SPDX-FileCopyrightText: 2023-2024 swt23w23
package catering.inventory;
import static catering.inventory.InventoryControllerIntegrationTests.PermissionResult.FORBIDDEN;
@ -31,6 +31,7 @@ import org.salespointframework.catalog.Product;
import org.salespointframework.catalog.Product.ProductIdentifier;
import org.salespointframework.inventory.UniqueInventory;
import org.salespointframework.inventory.UniqueInventoryItem;
import org.salespointframework.quantity.Metric;
import org.salespointframework.quantity.Quantity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
@ -82,11 +83,12 @@ class InventoryControllerIntegrationTests {
anyPid = anyProduct.getId();
anyConsumableItem = inventory.save(new UniqueInventoryItem(
catalog.save(new Consumable("Any Consumable", Money.of(1, EURO), Money.of(0.5, EURO),
Optional.of(Money.of(0.75, EURO)), Set.of(OrderType.EVENT_CATERING, OrderType.PARTY_SERVICE))),
Optional.of(Money.of(0.75, EURO)), Set.of(OrderType.EVENT_CATERING, OrderType.PARTY_SERVICE),
Metric.UNIT)),
Quantity.of(1)));
anyRentableItem = inventory.save(new UniqueInventoryItem(
catalog.save(new Rentable("Any Rentable", Money.of(1, EURO), Money.of(0.5, EURO),
Set.of(OrderType.EVENT_CATERING, OrderType.PARTY_SERVICE))),
Set.of(OrderType.EVENT_CATERING, OrderType.PARTY_SERVICE), Metric.UNIT)),
Quantity.of(1)));
}
@ -131,7 +133,8 @@ class InventoryControllerIntegrationTests {
.isEqualTo(
new UniqueInventoryItem(new Consumable("MOCK Schnitzel Wiener Art (vegan)", Money.of(7.5, EURO),
Money.of(3, EURO), Optional.of(Money.of(6.66, EURO)),
Set.of(OrderType.MOBILE_BREAKFAST, OrderType.PARTY_SERVICE)), Quantity.of(100)));
Set.of(OrderType.MOBILE_BREAKFAST, OrderType.PARTY_SERVICE), Metric.UNIT),
Quantity.of(100)));
}
@Test