Skip to content

Commit

Permalink
Created Sales export tab, which can export sales per month to csv fil…
Browse files Browse the repository at this point in the history
…e (replaces the Treasurer tab)
  • Loading branch information
JulesFleuren committed Nov 6, 2023
1 parent 3bfbdee commit e5c6087
Show file tree
Hide file tree
Showing 12 changed files with 478 additions and 13 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'com.tngtech.java:junit-dataprovider:1.5.0'

implementation 'org.javatuples:javatuples:1.2'
}

jacocoTestReport {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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 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;


/**
* DashboardSalesexportController class.
*/
@Controller
@RequestMapping("/administrator/salesexport")
@PreAuthorize("hasRole('ADMIN')")
public class DashboardSalesexportController extends DashboardController {

/** TreasurerRepository */
private final OrderRepository orderRepository;

/**
* DashboardSalesexportController constructor.
*
* @param orderRepository of type OrderRepository
*/
@Autowired
public DashboardSalesexportController(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}

/**
* Index of vendor [GET "/"].
*
* @param model String model
*
* @return path to Thymeleaf template location
*/
@GetMapping
public String index(Model model) {
// model.addAttribute("productMap", this.generateProductMap());
model.addAttribute("SalesexportSubmission", new SalesexportSubmission());

return "admin/salesexport/index";
}



/**
* Exports sales of month to csv
*
*/
@GetMapping(value="/csv", produces="text/csv")
public HttpEntity<? extends Object> csvExport(@ModelAttribute SalesexportSubmission SalesexportSubmission, Model model) {
model.addAttribute("SalesexportSubmission", SalesexportSubmission);

// Convert payment methods to integers
List<Integer> paymentMethods = new ArrayList<>();
SalesexportSubmission.getIncludedPaymentMethods().forEach( (m) -> paymentMethods.add(m.toInt()) );

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();

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
}

InputStream bufferedInputStream = new ByteArrayInputStream(csvContent.getBytes(StandardCharsets.UTF_8));
InputStreamResource fileInputStream = new InputStreamResource(bufferedInputStream);

String filename = "Sales_overview_"+SalesexportSubmission.getYear()+"-"+SalesexportSubmission.getMonth()+"_export.csv";

// setting HTTP headers
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
// defining the custom Content-Type
headers.set(HttpHeaders.CONTENT_TYPE, "text/csv");

return new ResponseEntity<>(
fileInputStream,
headers,
HttpStatus.OK
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import java.util.*;

import ch.wisv.events.core.repository.OrderRepository;
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 org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -88,11 +86,11 @@ private Map<LocalDate, Map<String, Triple<Double, Integer, String>>> generatePro
date = LocalDate.of(date.getYear(), date.getMonthValue(), 1);

Map<String, Triple<Double, Integer, String>> list = map.getOrDefault(date, new HashMap<>());
if (!list.containsKey(data.getTitle())) {
list.put(data.getTitle(), new ImmutableTriple<>(data.getPrice(), data.getAmount(), data.getVatRate()));
if (!list.containsKey(data.getProductTitle())) {
list.put(data.getProductTitle(), new ImmutableTriple<>(data.getPrice(), data.getAmount(), data.getVatRate()));
} else {
list.put(data.getTitle(),
new ImmutableTriple<>(data.getPrice(), list.get(data.getTitle()).getMiddle()+data.getAmount(), data.getVatRate())
list.put(data.getProductTitle(),
new ImmutableTriple<>(data.getPrice(), list.get(data.getProductTitle()).getMiddle()+data.getAmount(), data.getVatRate())
);
}

Expand Down
70 changes: 70 additions & 0 deletions src/main/java/ch/wisv/events/core/admin/SalesexportSubmission.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package ch.wisv.events.core.admin;

import ch.wisv.events.core.model.order.PaymentMethod;

import lombok.Getter;
import lombok.Setter;
import java.util.*;
import java.time.LocalDate;
import java.time.LocalDate;
import com.google.common.collect.Lists;


/**
* Class that contains the settings for the salesexport query,
* which can be specified in on the form on the Sales Export tab.
*/
@Getter
@Setter
public class SalesexportSubmission {

/**
* Year of query
*/
private int year;

/**
* Month of query.
*/
private int month;

/**
* Payment methods that should be contained in query.
*/
private List<PaymentMethod> includedPaymentMethods;

private boolean freeProductsIncluded;

/**
* Default constructor.
*/
public SalesexportSubmission() {

// default: previous month
if (LocalDate.now().getMonthValue() == 1) {
this.year = LocalDate.now().getYear()-1;
this.month = 12;
}
else {
this.year = LocalDate.now().getYear();
this.month = LocalDate.now().getMonthValue()-1;
}

this.includedPaymentMethods = Lists.newArrayList(PaymentMethod.IDEAL, PaymentMethod.SOFORT);

this.freeProductsIncluded = false;

}

/**
* Return all options in a string.
*/
public String toString() {
String settings = "Year: " + this.year + ", Month: " + this.month + ", Free products included: " + this.freeProductsIncluded;
for (PaymentMethod paymentMethod : this.includedPaymentMethods) {
settings += ", Payment method: " + paymentMethod.getName();
}
return settings;
}
}

7 changes: 5 additions & 2 deletions src/main/java/ch/wisv/events/core/admin/TreasurerData.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import java.time.LocalDateTime;

public interface TreasurerData {
String getTitle();
int getProductId();
String getEventTitle();
int getOrganizedBy();
String getProductTitle();
double getPrice();
int getAmount();
LocalDateTime getPaidAt();
String getVatRate();
LocalDateTime getPaidAt();
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,12 @@ public double calculateCostIncludingTransaction(double cost) {
return Math.round(e.evaluate() * 100.d) / 100.d;
}

/**
* Return corresponding integer
*
* @return int
*/
public int toInt() {
return this.ordinal();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import java.util.List;
import java.util.Optional;

import ch.wisv.events.core.admin.TreasurerData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import ch.wisv.events.core.model.order.OrderStatus;
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;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

/**
* OrderRepository interface.
Expand Down Expand Up @@ -82,7 +85,7 @@ public interface OrderRepository extends JpaRepository<Order, Integer> {
List<Order> findAllByOrderProducts(OrderProduct orderProduct);

@Query(value =
"SELECT B.TITLE AS title,B.PRICE AS price,B.AMOUNT AS amount,B.VAT_RATE AS vatRate, O.PAID_AT AS paidAt " +
"SELECT B.TITLE AS productTitle,B.PRICE AS price,B.AMOUNT AS amount,B.VAT_RATE AS vatRate, O.PAID_AT AS paidAt " +
"FROM " +
"( SELECT * " +
"FROM " +
Expand All @@ -102,4 +105,54 @@ public interface OrderRepository extends JpaRepository<Order, Integer> {
"AND (O.PAYMENT_METHOD = 2 " +
"OR O.PAYMENT_METHOD = 3)", nativeQuery = true)
List<TreasurerData> findallPayments();


/**
* Query for all orders with status PAID.
*
* Also joins the product and event tables to get the information
* about the products (and corresponding event) that were bought in the order
*
* @param month of type Integer. Filter query on month that the order was paid in (1-12)
* @param year of type Integer. Filter query on year that the order was paid in
* @param paymentMethods of type Collection<Integer>. Payment methods to include in the query
* @param includeFreeProducts of type boolean. Whether to include products with price 0 in the query
*
* @return List of TreasurerData. Contains the following fields:
* - productId
* - eventTitle
* - organizedBy
* - productTitle
* - price
* - amount
* - vatRate
*
*/
@Query(value ="""
SELECT
P.ID AS productId,
E.TITLE AS eventTitle,
E.ORGANIZED_BY AS organizedBy,
P.TITLE AS productTitle,
B.OP_PRICE AS price,
B.OP_AMOUNT AS amount,
P.VAT_RATE AS vatRate
FROM (
SELECT OP.PRICE AS OP_PRICE, OP.AMOUNT AS OP_AMOUNT, OP.PRODUCT_ID
FROM (
SELECT OOP.ORDER_PRODUCTS_ID AS OPID
FROM ORDERS O
INNER JOIN ORDERS_ORDER_PRODUCTS OOP ON O.ID = OOP.ORDER_ID
WHERE O.STATUS = 5
AND EXTRACT(YEAR FROM O.PAID_AT) = :year
AND EXTRACT(MONTH FROM O.PAID_AT) = :month
AND O.PAYMENT_METHOD IN :paymentMethods
) A
INNER JOIN ORDER_PRODUCT OP ON A.OPID = OP.ID
WHERE (:includeFreeProducts OR OP.PRICE > 0)
) B
INNER JOIN PRODUCT P ON B.PRODUCT_ID = P.ID
INNER JOIN EVENT_PRODUCTS EP ON P.ID = EP.PRODUCTS_ID
INNER JOIN EVENT E ON EP.EVENT_ID = E.ID""", nativeQuery = true)
List<TreasurerData> findallPaymentsByMonth(@Param("month") Integer month, @Param("year") Integer year, @Param("paymentMethods") Collection<Integer> paymentMethods, @Param("includeFreeProducts") boolean includeFreeProducts);
}
11 changes: 11 additions & 0 deletions src/main/java/ch/wisv/events/utils/LdapGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,15 @@ public String getName() {
return name;
}

/**
* Method intToString returns the name of the i-th LdapGroup.
*
* @param i of type int
*
* @return String
*/
public static String intToString(int i) {
return LdapGroup.values()[i].getName();
}

}
Loading

0 comments on commit e5c6087

Please sign in to comment.