Allow specifying order types in inventory

Fixes #95
This commit is contained in:
Simon Bruder 2023-12-07 18:35:52 +01:00
parent 0200330625
commit 3e2cc3d0b2
Signed by: simon
GPG key ID: 8D3C82F9F309F8EC
6 changed files with 77 additions and 14 deletions

View file

@ -19,7 +19,6 @@ package catering.inventory;
import static org.salespointframework.core.Currencies.EURO;
import java.util.Optional;
import java.util.Set;
import javax.money.MonetaryAmount;
import javax.money.NumberValue;
@ -29,7 +28,6 @@ import org.salespointframework.catalog.Product;
import org.salespointframework.inventory.UniqueInventoryItem;
import catering.catalog.Consumable;
import catering.order.OrderType;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero; // NonNegative in enterprise java
@ -60,14 +58,14 @@ class ConsumableMutateForm extends InventoryMutateForm {
Money.of(getRetailPrice(), EURO),
Money.of(getWholesalePrice(), EURO),
getPromotionPrice().map(price -> Money.of(price, EURO)),
// FIXME: categories for creation of consumables are hardcoded
Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST));
getOrderTypes());
}
public static ConsumableMutateForm of(Consumable product, UniqueInventoryItem item) {
ConsumableMutateForm form = new ConsumableMutateForm();
form.setName(product.getName());
form.setQuantity(item.getQuantity());
form.setOrderTypes(orderTypesFromCategories(product.getCategories()));
form.setWholesalePrice(product.getWholesalePrice().getNumber().doubleValueExact());
form.setRetailPrice(product.getRetailPrice().getNumber().doubleValueExact());
form.setPromotionPrice(

View file

@ -18,13 +18,20 @@ package catering.inventory;
import static org.salespointframework.core.Currencies.EURO;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.javamoney.moneta.Money;
import org.salespointframework.catalog.Product;
import org.salespointframework.inventory.UniqueInventoryItem;
import org.salespointframework.quantity.Quantity;
import org.springframework.data.util.Streamable;
import catering.catalog.Consumable;
import catering.catalog.Rentable;
import catering.order.OrderType;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero; // NonNegative in enterprise java
@ -41,6 +48,7 @@ abstract class InventoryMutateForm {
private @NotEmpty String name;
private @NotNull Quantity quantity;
private @NotNull @PositiveOrZero Double retailPrice;
private @NotNull Set<OrderType> orderTypes;
public InventoryMutateForm() {
}
@ -61,6 +69,10 @@ abstract class InventoryMutateForm {
this.name = name;
}
public Set<OrderType> getOrderTypes() {
return orderTypes;
}
public void setQuantity(Quantity quantity) {
this.quantity = quantity;
}
@ -69,6 +81,10 @@ abstract class InventoryMutateForm {
this.retailPrice = retailPrice;
}
public void setOrderTypes(Set<OrderType> orderTypes) {
this.orderTypes = orderTypes;
}
/**
* Creates an empty {@link InventoryMutateForm} for {@link Product}s of the given {@link Class}.
*
@ -125,8 +141,35 @@ abstract class InventoryMutateForm {
public void modifyProduct(Product product) {
product.setName(getName());
product.setPrice(Money.of(getRetailPrice(), EURO));
// first, remove all categories that are *not* selected
Stream.of(OrderType.class.getEnumConstants())
.filter(Predicate.not(getOrderTypes()::contains))
.map(OrderType::toString)
.forEach(product::removeCategory);
// then, add all categories that *are* selected
getOrderTypes().stream()
.map(OrderType::toString)
.forEach(product::addCategory);
modifyProductPrimitive(product);
}
protected abstract void modifyProductPrimitive(Product product);
/**
* Returns a {@link Set} of {@link OrderType}s from the categories of a
* {@link Product}
*
* @param categories a {@link Streamable} of category {@link String}s
* @return a {@link Set} of all {@link OrderType}s attached to the product
*/
public static Set<OrderType> orderTypesFromCategories(Streamable<String> categories) {
return categories.stream()
.filter(Stream.of(OrderType.class.getEnumConstants())
.map(OrderType::toString)
.collect(Collectors.toSet())::contains)
.map(OrderType::valueOf).collect(Collectors.toSet());
}
}

View file

@ -18,14 +18,11 @@ package catering.inventory;
import static org.salespointframework.core.Currencies.EURO;
import java.util.Set;
import org.javamoney.moneta.Money;
import org.salespointframework.catalog.Product;
import org.salespointframework.inventory.UniqueInventoryItem;
import catering.catalog.Rentable;
import catering.order.OrderType;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;
@ -44,6 +41,7 @@ class RentableMutateForm extends InventoryMutateForm {
RentableMutateForm form = new RentableMutateForm();
form.setName(product.getName());
form.setQuantity(item.getQuantity());
form.setOrderTypes(orderTypesFromCategories(product.getCategories()));
form.setWholesalePrice(product.getWholesalePrice().getNumber().doubleValueExact());
form.setRetailPrice(product.getRetailPrice().getNumber().doubleValueExact());
return form;
@ -55,8 +53,7 @@ class RentableMutateForm extends InventoryMutateForm {
getName(),
Money.of(getRetailPrice(), EURO),
Money.of(getWholesalePrice(), EURO),
// FIXME: categories for creation of rentables are hardcoded
Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST));
getOrderTypes());
}
@Override

View file

@ -37,6 +37,14 @@
<input class="form-control" type="number" name="promotionPrice" th:value="${#numbers.formatDecimal(form.promotionPrice.orElse(null), 1, 2)}" th:errorclass="is-invalid" step="0.01" min="0"/>
<div th:if="${#fields.hasErrors('promotionPrice')}" class="invalid-feedback">Ungültiger Aktionspreis.</div>
</div>
<div class="mb-3">
<label class="form-label" for="orderTypes">Buchungstypen</label>
<select class="form-select" multiple th:field="*{orderTypes}" th:errorclass="is-invalid">
<option th:each="orderType : ${T(catering.order.OrderType).values()}" th:value="${orderType}" th:text="${orderType.toHumanReadable()}"/>
</select>
<div th:if="${#fields.hasErrors('orderTypes')}" class="invalid-feedback">Ungültiger Buchungstyp.</div>
<div class="form-text">Mehrfachauswahl ist mit Umschalt- bzw. Steuerungstaste möglich.</div>
</div>
<button class="btn btn-primary" type="submit" th:text="${actionIsAdd ? 'Hinzufügen' : 'Bearbeiten'}"></button>
<!-- KANN: Bild und Beschreibungstext -->
</form>

View file

@ -12,6 +12,7 @@
<th>Menge</th>
<th>Einkaufspreis</th>
<th>UVP</th>
<th>Buchungstypen</th>
<th>Gesamtwert</th>
<th></th>
</tr>
@ -23,6 +24,11 @@
<td th:text="${item.product.wholesalePrice}"></td>
<td th:if="${item.product.getClass().getSimpleName() == 'Consumable' && item.product.promotionPrice.isPresent()}"><del th:text="${item.product.retailPrice}"></del> <span th:text="${item.product.promotionPrice.get()}"></span></td>
<td th:if="${item.product.getClass().getSimpleName() != 'Consumable' || item.product.promotionPrice.isEmpty()}" th:text="${item.product.price}"></td>
<td style="width: 20%;">
<div class="d-inline-flex flex-wrap gap-1 flex-column flex-xxl-row">
<span class="badge bg-secondary" th:each="orderType : ${T(catering.inventory.InventoryMutateForm).orderTypesFromCategories(item.product.categories)}" th:text="${orderType.toHumanReadable()}"/>
</div>
</td>
<td th:text="${item.product.wholesalePrice.multiply(item.quantity.getAmount())}"></td>
<td>
<div class="d-inline-flex flex-wrap gap-1 flex-column flex-xxl-row">

View file

@ -30,6 +30,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Optional;
import java.util.Set;
import org.javamoney.moneta.Money;
import org.junit.jupiter.api.BeforeEach;
@ -48,6 +49,7 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import catering.catalog.Consumable;
import catering.order.OrderType;
@AutoConfigureMockMvc
@SpringBootTest
@ -90,6 +92,7 @@ class InventoryControllerIntegrationTests {
.queryParam("type", Consumable.class.getSimpleName())
.param("name", "MOCK Schnitzel Wiener Art (vegan)")
.param("quantity", "100")
.param("orderTypes", "MOBILE_BREAKFAST", "PARTY_SERVICE")
.param("wholesalePrice", "3.00")
.param("retailPrice", "7.50")
.param("promotionPrice", "6.66"))
@ -99,11 +102,17 @@ class InventoryControllerIntegrationTests {
assertThat(itemCountAfter).isEqualTo(itemCountBefore + 1);
assertThat(inventory.findAll().filter(ie -> ie.getProduct() instanceof Consumable).stream())
.extracting("product.name", "quantity", "product.wholesalePrice", "product.retailPrice",
"product.promotionPrice")
.contains(tuple("MOCK Schnitzel Wiener Art (vegan)", Quantity.of(100),
Money.of(3, EURO), Money.of(7.5, EURO), Optional.of(Money.of(6.66, EURO))));
// extracting is not possible here, as the category sets are not equal
assertThat(inventory.findAll().stream()
.filter(ie -> ie.getProduct().getName().equals("MOCK Schnitzel Wiener Art (vegan)")).findAny())
.get()
.usingRecursiveComparison()
.ignoringFields("inventoryItemIdentifier.inventoryItemId", "isNew", "product.id.productId",
"product.isNew")
.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)));
}
@Test
@ -142,6 +151,7 @@ class InventoryControllerIntegrationTests {
.queryParam("type", Consumable.class.getSimpleName())
.param("type", "CONSUMABLE")
.param("name", "MOCK edited")
.param("orderTypes", "PARTY_SERVICE", "EVENT_CATERING")
.param("quantity", "4711")
.param("wholesalePrice", "0.01")
.param("retailPrice", "0.03")
@ -157,6 +167,7 @@ class InventoryControllerIntegrationTests {
.extracting("name", "wholesalePrice", "retailPrice", "promotionPrice")
.containsExactly("MOCK edited", Money.of(0.01, EURO),
Money.of(0.03, EURO), Optional.of(Money.of(0.02, EURO)));
assertThat(editedProduct.getCategories()).containsExactlyInAnyOrder("PARTY_SERVICE", "EVENT_CATERING");
}
@Test