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

Feature/sales export tab #461

Merged
merged 4 commits into from
Dec 12, 2023
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
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
Loading