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

Issues with sales export (closes #464) #465

Merged
merged 5 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,38 +1,30 @@
package ch.wisv.events.admin.controller;

import ch.wisv.events.core.model.order.PaymentMethod;
import ch.wisv.events.core.admin.TreasurerData;
import ch.wisv.events.core.admin.SalesExportSubmission;
import ch.wisv.events.utils.LdapGroup;
import ch.wisv.events.admin.utils.AggregatedProduct;


import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import com.google.common.collect.Lists;

import ch.wisv.events.core.repository.OrderRepository;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.javatuples.Septet;

import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;


/**
Expand Down Expand Up @@ -87,55 +79,9 @@ public HttpEntity<? extends Object> csvExport(@ModelAttribute SalesExportSubmiss

List<TreasurerData> treasurerData = orderRepository.findallPaymentsByMonth(SalesExportSubmission.getMonth(), SalesExportSubmission.getYear(), paymentMethods, SalesExportSubmission.isFreeProductsIncluded());

ListIterator<TreasurerData> listIterator = treasurerData.listIterator(treasurerData.size());

// Loop through all orders in TreasurerData and add orders of the same product together

// Key: Product ID, Value: Septet with: event title, organized by, product title, total income, amount, vatRate, price
Map<Integer, Septet<String, Integer, String, Double, Integer, String, Double>> map = new HashMap<>();

while (listIterator.hasPrevious()) {
TreasurerData data = listIterator.previous();
Map<Integer, AggregatedProduct> map = aggregateOrders(treasurerData);

if (!map.containsKey(data.getProductId())) {
map.put(
data.getProductId(),
Septet.with(
data.getEventTitle(),
data.getOrganizedBy(),
data.getProductTitle(),
data.getPrice(),
data.getAmount(),
data.getVatRate(),
data.getPrice()));
} else {
Double totalIncome = map.get(data.getProductId()).getValue3();
Integer totalAmount = map.get(data.getProductId()).getValue4();
map.put(
data.getProductId(),
Septet.with(
data.getEventTitle(),
data.getOrganizedBy(),
data.getProductTitle(),
totalIncome + data.getPrice(),
totalAmount + data.getAmount(),
data.getVatRate(),
data.getPrice()));
}
}

// String csvContent = "Options:\n" + SalesExportSubmission.toString() + "\n\n";
// csvContent += "Event;Organized by;Product;Total income;Total amount;VAT rate\n";
String csvContent = "Event;Organized by;Product;Total income;Total amount;VAT rate;price\n";
for (Map.Entry<Integer, Septet<String, Integer, String, Double, Integer, String, Double>> entry : map.entrySet()) {
csvContent += entry.getValue().getValue0() // event title
+ ";" + LdapGroup.intToString(entry.getValue().getValue1()) // organized by
+ ";" + entry.getValue().getValue2() // product title
+ ";" + entry.getValue().getValue3() // total income
+ ";" + entry.getValue().getValue4() // total amount
+ ";" + entry.getValue().getValue5() // vat rate
+ ";" + entry.getValue().getValue6() + "\n"; // price
}
String csvContent = generateCsvContent(map);

InputStream bufferedInputStream = new ByteArrayInputStream(csvContent.getBytes(StandardCharsets.UTF_8));
InputStreamResource fileInputStream = new InputStreamResource(bufferedInputStream);
Expand All @@ -154,4 +100,73 @@ public HttpEntity<? extends Object> csvExport(@ModelAttribute SalesExportSubmiss
HttpStatus.OK
);
}

/**
* Go through all orders in a given month and add the total amount and income together in Aggregated product
*
* @param treasurerData containing all orders that have to be aggregated
*
* @return map with entry per product, with the product id as key
*/
public static Map<Integer, AggregatedProduct> aggregateOrders(List<TreasurerData> treasurerData) {

ListIterator<TreasurerData> listIterator = treasurerData.listIterator(treasurerData.size());

// Loop through all orders in treasurerData and add orders of the same product together in AggregatedProduct
// Key: Product ID, Value: Aggregated product
Map<Integer, AggregatedProduct> map = new HashMap<>();

while (listIterator.hasPrevious()) {
TreasurerData data = listIterator.previous();

if (!map.containsKey(data.getProductId())) {
AggregatedProduct product = new AggregatedProduct();

product.eventTitle = data.getEventTitle();
product.organizedBy = LdapGroup.intToString(data.getOrganizedBy());
product.productTitle = data.getProductTitle();
product.totalIncome = data.getPrice();
product.totalAmount = data.getAmount();
product.vatRate = data.getVatRate();
product.price = data.getPrice();

map.put(data.getProductId(),product);

} else {
AggregatedProduct product = map.get(data.getProductId());
product.totalIncome += data.getPrice()*data.getAmount();
product.totalAmount += data.getAmount();
map.put(data.getProductId(),product);
}
}

return map;
}




/**
* Format all aggregated products into string in csv format
*
* @param map containing all aggregated orders.
*
* @return string that should be written to csv file
*/
public static String generateCsvContent(Map<Integer, AggregatedProduct> map) {

// String csvContent = "Options:\n" + SalesExportSubmission.toString() + "\n\n";
// csvContent += "Event;Organized by;Product;Total income;Total amount;VAT rate\n";
StringBuilder csvContent = new StringBuilder("Event;Organized by;Product;Total income;Total amount;VAT rate;price\n");
for (Map.Entry<Integer, AggregatedProduct> entry : map.entrySet()) {
csvContent.append(entry.getValue().eventTitle)
.append(";").append(entry.getValue().organizedBy) // organized by
.append(";").append(entry.getValue().productTitle) // product title
.append(";").append(String.format("%.2f", entry.getValue().totalIncome)) // total income
.append(";").append(entry.getValue().totalAmount) // total amount
.append(";").append(entry.getValue().vatRate) // vat rate
.append(";").append(String.format("%.2f", entry.getValue().price)).append("\n"); // price
}
return csvContent.toString();
}
}
16 changes: 16 additions & 0 deletions src/main/java/ch/wisv/events/admin/utils/AggregatedProduct.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ch.wisv.events.admin.utils;


/**
* AggregatedProduct class. Is used to store all information about a single product
* when adding the sales over a month together in the Sales Export tab
*/
public class AggregatedProduct {
public String eventTitle;
public String organizedBy;
public String productTitle;
public Double totalIncome;
public Integer totalAmount;
public String vatRate;
public Double price;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import lombok.Setter;
import java.util.*;
import java.time.LocalDate;
import java.time.LocalDate;
import com.google.common.collect.Lists;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.util.List;
import java.util.Optional;
import java.util.Collection;
import java.time.LocalDate;

import ch.wisv.events.core.admin.TreasurerData;
import org.springframework.data.jpa.repository.JpaRepository;
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/templates/admin/salesexport/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ <h6 class="card-header">Month</h6>
type="number"
class="form-control"
th:field="*{month}"
max="12"
min="1"
/></label>
</div>
</div>
Expand Down
18 changes: 18 additions & 0 deletions src/test/java/ch/wisv/events/ControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,24 @@ protected Order createOrder(Customer customer, List<Product> products, OrderStat
return order;
}

protected Order createOrderSingleProduct(Customer customer, Product product, Long amount, OrderStatus status, String createdBy) {
Order order = new Order();
order.setOwner(customer);
order.setStatus(status);
order.setCreatedBy(createdBy);
order.setPaymentMethod(PaymentMethod.IDEAL);


OrderProduct orderProduct = new OrderProduct(product, product.getCost(), amount);
orderProductRepository.saveAndFlush(orderProduct);
order.addOrderProduct(orderProduct);


orderRepository.saveAndFlush(order);

return order;
}

protected OrderProduct createOrderProduct(Product product) {
OrderProduct orderProduct = new OrderProduct();
orderProduct.setProduct(product);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,24 @@
import ch.wisv.events.core.model.order.Order;
import ch.wisv.events.core.model.order.OrderStatus;
import ch.wisv.events.core.model.product.Product;
import ch.wisv.events.core.admin.TreasurerData;
import ch.wisv.events.admin.utils.AggregatedProduct;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.time.LocalDate;
import java.util.Map;
import java.util.TreeMap;
import org.json.simple.JSONObject;


import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Triple;

import java.util.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
Expand All @@ -35,6 +34,126 @@
@ActiveProfiles("test")
public class DashboardSalesExportControllerTest extends ControllerTest {

@Test
public void testIndex() throws Exception {
mockMvc.perform(get("/administrator/salesexport"))
.andExpect(status().is2xxSuccessful())
.andExpect(view().name("admin/salesexport/index"));
}

@Test
public void testAggregateOrders() throws Exception {
Event event = this.createEvent();
Customer customer = this.createCustomer();

Product product1 = this.createProduct();
product1.setCost(0.10d);
event.addProduct(product1);

Order order1 = this.createOrderSingleProduct(customer, product1, 2L, OrderStatus.PENDING, "test");
orderService.updateOrderStatus(order1, OrderStatus.PAID);
Order order2 = this.createOrderSingleProduct(customer, product1, 1L, OrderStatus.PENDING, "test");
orderService.updateOrderStatus(order2, OrderStatus.PAID);

Product product2 = this.createProduct();
product2.setCost(13.13d);
event.addProduct(product2);

Order order3 = this.createOrderSingleProduct(customer, product2, 1L, OrderStatus.PENDING, "test");
orderService.updateOrderStatus(order3, OrderStatus.PAID);
// this last order should not be in the aggregated product, because OrderStatus is not set to PAID
this.createOrderSingleProduct(customer, product2, 1L, OrderStatus.PENDING, "test");

List<Integer> paymentMethods = new ArrayList<>();
paymentMethods.add(order1.getPaymentMethod().toInt());
List<TreasurerData> treasurerData = orderRepository.findallPaymentsByMonth(LocalDate.now().getMonthValue(), LocalDate.now().getYear(), paymentMethods, false);

Map<Integer, AggregatedProduct> map = DashboardSalesExportController.aggregateOrders(treasurerData);


assertTrue(map.containsKey(product1.getId()));
AggregatedProduct aggrProduct1 = map.get(product1.getId());
assertEquals(event.toString(), aggrProduct1.eventTitle);
assertEquals(event.getOrganizedBy().getName(), aggrProduct1.organizedBy);
assertEquals(product1.getTitle(), aggrProduct1.productTitle);
assertEquals((Double) (3*(product1.getCost())), aggrProduct1.totalIncome);
assertEquals((Integer) 3, aggrProduct1.totalAmount);
assertEquals(product1.getVatRate().name(), aggrProduct1.vatRate);
assertEquals(product1.getCost(), aggrProduct1.price);

AggregatedProduct aggrProduct2 = map.get(product2.getId());
assertEquals(product2.getCost(), aggrProduct2.totalIncome);
assertEquals((Integer) 1, aggrProduct2.totalAmount);


}

@Test
public void testGenerateCsvContent() throws Exception {
// Mostly test if amounts are correctly rounded to 2 decimals

Event event = this.createEvent();

Product product1 = this.createProduct();
product1.setCost(0.10d);
event.addProduct(product1);

AggregatedProduct aggregatedProduct1 = new AggregatedProduct();
aggregatedProduct1.eventTitle = event.getTitle();
aggregatedProduct1.organizedBy = event.getOrganizedBy().getName();
aggregatedProduct1.productTitle = product1.getTitle();
aggregatedProduct1.vatRate = product1.getVatRate().name();
aggregatedProduct1.price = product1.getCost();
aggregatedProduct1.totalIncome = product1.getCost();
aggregatedProduct1.totalAmount = 1;

aggregatedProduct1.totalAmount += 2;
aggregatedProduct1.totalIncome += 2*product1.getCost();

Product product2 = this.createProduct();
product2.setCost(0.30d);
event.addProduct(product1);

AggregatedProduct aggregatedProduct2 = new AggregatedProduct();
aggregatedProduct2.eventTitle = event.getTitle();
aggregatedProduct2.organizedBy = event.getOrganizedBy().getName();
aggregatedProduct2.productTitle = product2.getTitle();
aggregatedProduct2.vatRate = product2.getVatRate().name();
aggregatedProduct2.price = product2.getCost();
aggregatedProduct2.totalIncome = 128.1;
aggregatedProduct2.totalAmount = 123;

aggregatedProduct2.totalAmount += 0;
aggregatedProduct2.totalIncome += 0.2;

Map<Integer, AggregatedProduct> map = new HashMap<>();

map.put(product1.getId(), aggregatedProduct1);
map.put(product2.getId(), aggregatedProduct2);

String csvContent = DashboardSalesExportController.generateCsvContent(map);

String expectedLine1 = event.getTitle()
+ ";" + event.getOrganizedBy().getName()
+ ";" + product1.getTitle()
+ ";" + "0.30" // total income
+ ";" + "3" // total amount
+ ";" + product1.getVatRate().name()
+ ";" + "0.10";

String expectedLine2 = event.getTitle()
+ ";" + event.getOrganizedBy().getName()
+ ";" + product2.getTitle()
+ ";" + "128.30" // total income
+ ";" + "123" // total amount
+ ";" + product2.getVatRate().name()
+ ";" + "0.30";

assertTrue(csvContent.contains(expectedLine1));
assertTrue(csvContent.contains(expectedLine2));

}

@Test
public void testCsvExport() throws Exception {
Product product = this.createProduct();
Expand Down
Loading