package org.internna.ossmoney.mvc;
import java.util.Map;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.HashMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Calendar;
import java.util.Currency;
import java.util.Collection;
import java.math.BigDecimal;
import javax.servlet.http.HttpServletRequest;
import org.internna.ossmoney.model.Category;
import org.internna.ossmoney.util.DateUtils;
import org.internna.ossmoney.cache.CacheStore;
import org.internna.ossmoney.model.Subcategory;
import org.internna.ossmoney.model.budget.Budget;
import org.internna.ossmoney.model.support.Interval;
import org.internna.ossmoney.model.AccountTransaction;
import org.internna.ossmoney.model.security.UserDetails;
import org.internna.ossmoney.model.support.InflowOutflow;
import org.internna.ossmoney.model.support.NameValuePair;
import org.springframework.ui.ModelMap;
import org.springframework.util.CollectionUtils;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/financial/reporting")
public final class ReportingController extends AbstractTransactionController {
@Autowired private CacheStore cache;
@Autowired private MessageSource messageSource;
@Autowired private WidgetController widgetController;
@RequestMapping
public String index() {
return "reporting/index";
}
protected void setCache(CacheStore cache) {
this.cache = cache;
}
@RequestMapping("/inflows-outflows/{intervals}")
public String monthlyInflowsOutflowsReport(@PathVariable final String intervals, final HttpServletRequest request, final ModelMap modelMap) {
Interval interval = getInterval(intervals);
modelMap.addAttribute("months", DateUtils.dates(interval.getNumberOfMonths() - 1));
List<AccountTransaction> transactions = getTransactions(interval, null, modelMap);
modelMap.addAttribute("data", calculateMonthlyInflowsOutflows(interval, transactions));
return "reporting/monthly-inflows-outflows";
}
protected final Collection<InflowOutflow> calculateMonthlyInflowsOutflows(final Interval interval, final List<AccountTransaction> transactions) {
Map<String, InflowOutflow> data = new HashMap<String, InflowOutflow>();
if (!CollectionUtils.isEmpty(transactions)) {
for (AccountTransaction transaction : transactions) {
Locale locale = transaction.getAccount().getLocale();
String currency = transaction.getAccount().getCurrency();
InflowOutflow inflowOutflow = getOrCreateInflowOutflow(interval, currency, locale, data);
process(transaction, inflowOutflow);
}
}
return data.values();
}
protected final InflowOutflow getOrCreateInflowOutflow(final Interval interval, final String currency, final Locale locale, final Map<String, InflowOutflow> inflowsOutflows) {
InflowOutflow inflowOutflow = inflowsOutflows.get(currency);
if (inflowOutflow == null) {
inflowOutflow = new InflowOutflow(interval, currency, locale, messageSource);
inflowsOutflows.put(currency, inflowOutflow);
}
return inflowOutflow;
}
protected final void process(final AccountTransaction transaction, final InflowOutflow inflowOutflow) {
boolean income = transaction.getSubcategory().isIncome();
Map<Category, Map<Subcategory, BigDecimal>> data = income ? inflowOutflow.getInflow(transaction.getOperationDate()) : inflowOutflow.getOutflow(transaction.getOperationDate());
Map<Subcategory, BigDecimal> categoryData = getOrCreateCategoryData(transaction, data);
BigDecimal amount = getOrCreateSubcategoryAmount(transaction, categoryData);
amount = amount.abs().add(transaction.getAmount().abs());
categoryData.put(transaction.getSubcategory(), amount);
}
protected final Map<Subcategory, BigDecimal> getOrCreateCategoryData(final AccountTransaction transaction, final Map<Category, Map<Subcategory, BigDecimal>> data) {
Subcategory subcategory = transaction.getSubcategory();
Category category = subcategory.getParentCategory();
Map<Subcategory, BigDecimal> categoryData = data.get(category);
if (categoryData == null) {
categoryData = new HashMap<Subcategory, BigDecimal>();
data.put(category, categoryData);
}
return categoryData;
}
protected BigDecimal getOrCreateSubcategoryAmount(final AccountTransaction transaction, final Map<Subcategory, BigDecimal> categoryData) {
Subcategory subcategory = transaction.getSubcategory();
BigDecimal amount = categoryData.get(subcategory);
if (amount == null) {
amount = BigDecimal.ZERO;
categoryData.put(subcategory, amount);
}
return amount;
}
@RequestMapping("/categories")
public String expensesByCategoryOverTime(final ModelMap modelMap) {
UserDetails user = getCurrentUser();
modelMap.addAttribute("subcategories", new TreeSet<Subcategory>(Subcategory.findExpenseCategories(user)));
return "reporting/categories";
}
@RequestMapping("/categories-chart/{id}/{intervals}")
public String expensesByCategoryOverTimeChartData(@PathVariable final Long id, @PathVariable final String intervals, final ModelMap modelMap) {
UserDetails user = getCurrentUser();
Interval interval = getInterval(intervals);
Subcategory subcat = Subcategory.findSubcategory(id);
Map<String, BigDecimal> maxValues = cache.getMaxCategoryData(user, subcat, intervals);
Map<String, Map<Date, NameValuePair<Date, BigDecimal>>> budgetData = cache.getAlloted(user, subcat, intervals);
Map<String, Map<Date, NameValuePair<Date, BigDecimal>>> currencyData = cache.getCategoryData(user, subcat, intervals);
if (maxValues == null) {
maxValues = new HashMap<String, BigDecimal>();
budgetData = new HashMap<String, Map<Date, NameValuePair<Date, BigDecimal>>>();
currencyData = new HashMap<String, Map<Date, NameValuePair<Date, BigDecimal>>>();
for (AccountTransaction transaction : getTransactions(interval, subcat, modelMap)) {
Locale locale = transaction.getAccount().getLocale();
String isoCode = Currency.getInstance(locale).getCurrencyCode();
if (!currencyData.containsKey(isoCode)) {
maxValues.put(isoCode, BigDecimal.ZERO);
currencyData.put(isoCode, createDateValueMap(interval));
budgetData.put(isoCode, budget(user, subcat, locale, interval));
}
Date date = DateUtils.getMonthEndDate(transaction.getOperationDate());
if (!currencyData.get(isoCode).containsKey(date)) {
date = DateUtils.end(Calendar.getInstance()).getTime();
}
NameValuePair<Date, BigDecimal> stored = currencyData.get(isoCode).get(date);
stored.setValue(stored.getValue().add(transaction.getAmount().abs()));
maxValues.put(isoCode, maxValues.get(isoCode).max(stored.getValue()));
}
cache.storeAlloted(user, subcat, intervals, budgetData);
cache.storeCategoryData(user, subcat, intervals, currencyData);
cache.storeMaxCategoryData(user, subcat, intervals, maxValues);
}
modelMap.addAttribute("max", maxValues);
modelMap.addAttribute("category", subcat);
modelMap.addAttribute("currencies", currencyData.keySet());
for (String code : currencyData.keySet()) {
modelMap.addAttribute("data" + code, new TreeSet<NameValuePair<Date, BigDecimal>>(currencyData.get(code).values()));
modelMap.addAttribute("budget" + code, new TreeSet<NameValuePair<Date, BigDecimal>>(budgetData.get(code).values()));
}
if (widgetController != null) {
widgetController.incomeVsExpensesOverTime(interval.getNumberOfMonths(), modelMap);
}
return "reporting/categories-chart";
}
private Map<Date, NameValuePair<Date, BigDecimal>> createDateValueMap(Interval interval) {
Map<Date, NameValuePair<Date, BigDecimal>> data = new TreeMap<Date, NameValuePair<Date, BigDecimal>>();
for (Date date : DateUtils.dates(interval.getNumberOfMonths())) {
data.put(date, new NameValuePair<Date, BigDecimal>(date, BigDecimal.ZERO));
}
return data;
}
protected Map<Date, NameValuePair<Date, BigDecimal>> budget(UserDetails user, Subcategory category, Locale locale, Interval interval) {
Map<Date, NameValuePair<Date, BigDecimal>> budgetData = new HashMap<Date, NameValuePair<Date, BigDecimal>>();
Budget budget = user.getBudget();
for (Date date : DateUtils.dates(interval.getNumberOfMonths())) {
budgetData.put(date, new NameValuePair<Date, BigDecimal>(date, budget.getAlloted(date, locale, category)));
}
return budgetData;
}
}