Skip to content

Commit

Permalink
Feature/sales export tab (#461)
Browse files Browse the repository at this point in the history
* Added VAT rate column to treasurer tab

* change to layout of treasurer tab

* Created Sales export tab, which can export sales per month to csv file (replaces the Treasurer tab)

* Rename Salesexport to SalesExport

---------

Co-authored-by: root <[email protected]>
  • Loading branch information
JoepdeJong and JulesFleuren authored Dec 12, 2023
1 parent 40983e9 commit d81dd86
Show file tree
Hide file tree
Showing 12 changed files with 555 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
69 changes: 69 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,69 @@
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 @@ -62,4 +62,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 d81dd86

Please sign in to comment.