/*
* This file is part of LibrePlan
*
* Copyright (C) 2012 WirelessGalicia, S.L.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.libreplan.web.expensesheet;
import org.joda.time.LocalDate;
import org.libreplan.business.common.exceptions.InstanceNotFoundException;
import org.libreplan.business.common.exceptions.ValidationException;
import org.libreplan.business.expensesheet.entities.ExpenseSheet;
import org.libreplan.business.expensesheet.entities.ExpenseSheetLine;
import org.libreplan.business.orders.entities.Order;
import org.libreplan.business.orders.entities.OrderElement;
import org.libreplan.business.resources.entities.Resource;
import org.libreplan.business.users.entities.UserRole;
import org.libreplan.web.common.BaseCRUDController;
import org.libreplan.web.common.IndexController;
import org.libreplan.web.common.Level;
import org.libreplan.web.common.Util;
import org.libreplan.web.common.components.bandboxsearch.BandboxSearch;
import org.libreplan.web.common.entrypoints.IURLHandlerRegistry;
import org.libreplan.web.common.entrypoints.MatrixParameters;
import org.libreplan.web.security.SecurityUtils;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.CheckEvent;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zkplus.spring.SpringUtil;
import org.zkoss.zul.Button;
import org.zkoss.zul.Constraint;
import org.zkoss.zul.Datebox;
import org.zkoss.zul.Decimalbox;
import org.zkoss.zul.Grid;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.Row;
import org.zkoss.zul.RowRenderer;
import org.zkoss.zul.Textbox;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import static org.libreplan.web.I18nHelper._;
/**
* Controller for CRUD actions over a {@link ExpenseSheet}.
*
* @author Susana Montes Pedreira <smontes@wirelessgalicia.com>
*/
public class ExpenseSheetCRUDController
extends BaseCRUDController<ExpenseSheet>
implements IExpenseSheetCRUDController {
private static final String NOT_EMPTY = "cannot be empty";
private IExpenseSheetModel expenseSheetModel;
/**
* Components editWindow
*/
private Datebox dateboxExpenseDate;
private Grid gridExpenseLines;
private BandboxSearch bandboxTasks;
private Decimalbox dboxValue;
private BandboxSearch bandboxResource;
private Textbox tbConcept;
private ExpenseSheetLineRenderer expenseSheetLineRenderer = new ExpenseSheetLineRenderer();
private IURLHandlerRegistry URLHandlerRegistry;
private boolean fromUserDashboard = false;
private boolean cancel = false;
public ExpenseSheetCRUDController() {
if ( expenseSheetModel == null ) {
expenseSheetModel = (IExpenseSheetModel) SpringUtil.getBean("expenseSheetModel");
}
if ( URLHandlerRegistry == null ) {
URLHandlerRegistry = (IURLHandlerRegistry) SpringUtil.getBean("URLHandlerRegistry");
}
}
@Override
public void doAfterCompose(Component comp) throws Exception {
super.doAfterCompose(comp);
checkUserHasProperRoleOrSendForbiddenCode();
URLHandlerRegistry.getRedirectorFor(IExpenseSheetCRUDController.class).register(this, page);
}
private void checkUserHasProperRoleOrSendForbiddenCode() {
HttpServletRequest request = (HttpServletRequest) Executions.getCurrent().getNativeRequest();
Map<String, String> matrixParams = MatrixParameters.extract(request);
// If it doesn't come from a entry point
if (matrixParams.isEmpty()) {
if (!SecurityUtils.isSuperuserOrUserInRoles(UserRole.ROLE_EXPENSES)) {
Util.sendForbiddenStatusCodeInHttpServletResponse();
}
}
}
@Override
public void save() throws ValidationException {
expenseSheetModel.confirmSave();
}
@Override
protected void beforeSaving() throws ValidationException {
super.beforeSaving();
expenseSheetModel.generateExpenseSheetLineCodesIfIsNecessary();
}
private void loadComponentsEditWindow() {
tbConcept = (Textbox) editWindow.getFellowIfAny("tbConcept");
dateboxExpenseDate = (Datebox) editWindow.getFellowIfAny("dateboxExpenseDate");
dboxValue = (Decimalbox) editWindow.getFellowIfAny("dboxValue");
gridExpenseLines = (Grid) editWindow.getFellowIfAny("gridExpenseLines");
bandboxResource = (BandboxSearch) editWindow.getFellowIfAny("bandboxResource");
bandboxTasks = (BandboxSearch) editWindow.getFellowIfAny("bandboxTasks");
}
/**
* Operations in the list window.
*/
public List<ExpenseSheet> getExpenseSheets() {
return expenseSheetModel.getExpenseSheets();
}
/**
* Operations in the create window.
*/
public ExpenseSheet getExpenseSheet() {
return expenseSheetModel.getExpenseSheet();
}
public SortedSet<ExpenseSheetLine> getExpenseSheetLines() {
return expenseSheetModel.getExpenseSheetLines();
}
/**
* Adds a new {@link ExpenseSheetLine} to the list of rows.
*/
public void addExpenseSheetLine() {
if (validateNewLine()) {
expenseSheetModel.addExpenseSheetLine();
reloadExpenseSheetLines();
reloadComponentsNewLine();
}
}
private void reloadComponentsNewLine() {
Util.reloadBindings(bandboxTasks);
Util.reloadBindings(bandboxResource);
Util.reloadBindings(tbConcept);
Util.reloadBindings(dboxValue);
}
private boolean validateNewLine() {
if (expenseSheetModel.getNewExpenseSheetLine().getDate() == null) {
throw new WrongValueException(this.dateboxExpenseDate, _(NOT_EMPTY));
}
if (expenseSheetModel.getNewExpenseSheetLine().getOrderElement() == null) {
throw new WrongValueException(this.bandboxTasks, _(NOT_EMPTY));
}
BigDecimal value = expenseSheetModel.getNewExpenseSheetLine().getValue();
if (value == null || value.compareTo(BigDecimal.ZERO) < 0) {
throw new WrongValueException(this.dboxValue, _("cannot be empty or less than zero"));
}
return true;
}
public void confirmRemove(ExpenseSheetLine expenseSheetLine) {
int status = Messagebox.show(
_("Confirm deleting {0}. Are you sure?", getExpenseSheetLineName(expenseSheetLine)),
_("Delete"), Messagebox.OK | Messagebox.CANCEL, Messagebox.QUESTION);
if (Messagebox.OK == status)
removeExpenseSheetLine(expenseSheetLine);
}
private void removeExpenseSheetLine(ExpenseSheetLine expenseSheetLine) {
expenseSheetModel.removeExpenseSheetLine(expenseSheetLine);
reloadExpenseSheetLines();
}
private String getExpenseSheetLineName(ExpenseSheetLine expenseSheetLine) {
if (expenseSheetLine != null) {
LocalDate date = expenseSheetLine.getDate();
OrderElement task = expenseSheetLine.getOrderElement();
if (date != null && task != null) {
return _("expense line of the ") + task.getName() + " - " + date;
}
}
return _("item");
}
private void reloadExpenseSheetLines() {
if (gridExpenseLines != null) {
Util.reloadBindings(gridExpenseLines);
}
}
public void onCheckGenerateCode(Event e) {
CheckEvent ce = (CheckEvent) e;
if (ce.isChecked()) {
// We have to auto-generate the code for new objects
try {
expenseSheetModel.setCodeAutogenerated(ce.isChecked());
} catch (ConcurrentModificationException err) {
messagesForUser.showMessage(Level.ERROR, err.getMessage());
}
}
Util.reloadBindings(editWindow);
reloadExpenseSheetLines();
}
public ExpenseSheetLine getNewExpenseSheetLine() {
return expenseSheetModel.getNewExpenseSheetLine();
}
public List<Order> getOrders() {
return expenseSheetModel.getOrders();
}
public List<OrderElement> getTasks() {
return expenseSheetModel.getTasks();
}
public void setProject(Order project) {
expenseSheetModel.setSelectedProject(project);
Util.reloadBindings(bandboxTasks);
}
public Order getProject() {
return expenseSheetModel.getSelectedProject();
}
public ExpenseSheetLineRenderer getExpenseSheetLineRenderer() {
return expenseSheetLineRenderer;
}
/**
* RowRenderer for a @{ExpenseSheetLine} element.
*/
private class ExpenseSheetLineRenderer implements RowRenderer {
@Override
public void render(Row row, Object data, int i) {
ExpenseSheetLine expenseSheetLine = (ExpenseSheetLine) data;
row.setValue(expenseSheetLine);
appendOrderElementInLines(row);
appendValueInLines(row);
appendConceptInLines(row);
appendDateInLines(row);
appendResourceInLines(row);
appendCode(row);
appendDeleteButton(row);
}
private void appendConceptInLines(Row row) {
final ExpenseSheetLine expenseSheetLine = row.getValue();
final Textbox txtConcept = new Textbox();
txtConcept.setWidth("160px");
Util.bind(
txtConcept,
() -> expenseSheetLine != null ? expenseSheetLine.getConcept() : "",
value -> {
if (expenseSheetLine != null)
expenseSheetLine.setConcept(value);
});
row.appendChild(txtConcept);
}
private void appendDateInLines(Row row) {
final ExpenseSheetLine expenseSheetLine = row.getValue();
final Datebox dateboxExpense = new Datebox();
Util.bind(
dateboxExpense,
() -> expenseSheetLine != null && expenseSheetLine.getDate() != null
? expenseSheetLine.getDate().toDateTimeAtStartOfDay().toDate()
: null,
value -> {
if (expenseSheetLine != null) {
LocalDate newDate = null;
if (value != null)
newDate = LocalDate.fromDateFields(value);
expenseSheetModel.keepSortedExpenseSheetLines(expenseSheetLine, newDate);
reloadExpenseSheetLines();
}
});
dateboxExpense.setConstraint("no empty:" + _(NOT_EMPTY));
row.appendChild(dateboxExpense);
}
private void appendResourceInLines(Row row) {
final ExpenseSheetLine expenseSheetLine = row.getValue();
final BandboxSearch bandboxSearch = BandboxSearch.create("ResourceInExpenseSheetBandboxFinder");
bandboxSearch.setSelectedElement(expenseSheetLine.getResource());
bandboxSearch.setSclass("bandbox-workreport-task");
bandboxSearch.setListboxWidth("450px");
EventListener eventListenerUpdateResource = event -> {
Listitem selectedItem = bandboxSearch.getSelectedItem();
setResourceInESL(selectedItem, expenseSheetLine);
};
bandboxSearch.setListboxEventListener(Events.ON_SELECT, eventListenerUpdateResource);
bandboxSearch.setListboxEventListener(Events.ON_OK, eventListenerUpdateResource);
bandboxSearch.setBandboxEventListener(Events.ON_CHANGING, eventListenerUpdateResource);
row.appendChild(bandboxSearch);
}
private void appendCode(final Row row) {
final ExpenseSheetLine line = row.getValue();
final Textbox code = new Textbox();
code.setWidth("90%");
code.setDisabled(getExpenseSheet().isCodeAutogenerated());
code.applyProperties();
if (line.getCode() != null) {
code.setValue(line.getCode());
}
code.addEventListener("onChange", event -> {
final ExpenseSheetLine line1 = row.getValue();
line1.setCode(code.getValue());
});
code.setConstraint(checkConstraintLineCodes(line));
row.appendChild(code);
}
private void appendDeleteButton(final Row row) {
Button delete = new Button("", "/common/img/ico_borrar1.png");
delete.setHoverImage("/common/img/ico_borrar.png");
delete.setSclass("icono");
delete.setTooltiptext(_("Delete"));
delete.addEventListener(Events.ON_CLICK, event -> confirmRemove(row.getValue()));
row.appendChild(delete);
}
private void appendValueInLines(Row row) {
final ExpenseSheetLine expenseSheetLine = row.getValue();
final Decimalbox dbValue = new Decimalbox();
dbValue.setScale(2);
Util.bind(
dbValue,
() -> expenseSheetLine != null ? expenseSheetLine.getValue() : BigDecimal.ZERO.setScale(2),
value -> {
if (expenseSheetLine != null)
expenseSheetLine.setValue(value);
});
dbValue.setConstraint(checkConstraintExpenseValue());
dbValue.setFormat(Util.getMoneyFormat());
row.appendChild(dbValue);
}
/**
* Append a Bandbox @{link OrderElement} to row.
*
* @param row
*/
private void appendOrderElementInLines(Row row) {
final ExpenseSheetLine expenseSheetLine = row.getValue();
final BandboxSearch bandboxSearch = BandboxSearch.create("OrderElementInExpenseSheetBandboxFinder");
bandboxSearch.setSelectedElement(expenseSheetLine.getOrderElement());
bandboxSearch.setSclass("bandbox-workreport-task");
bandboxSearch.setListboxWidth("450px");
EventListener eventListenerUpdateOrderElement = event -> {
Listitem selectedItem = bandboxSearch.getSelectedItem();
setOrderElementInESL(selectedItem, expenseSheetLine);
};
bandboxSearch.setListboxEventListener(Events.ON_SELECT, eventListenerUpdateOrderElement);
bandboxSearch.setListboxEventListener(Events.ON_OK, eventListenerUpdateOrderElement);
bandboxSearch.setBandboxEventListener(Events.ON_CHANGING, eventListenerUpdateOrderElement);
bandboxSearch.setBandboxConstraint("no empty:" + _(NOT_EMPTY));
row.appendChild(bandboxSearch);
}
private Constraint checkConstraintLineCodes(final ExpenseSheetLine line) {
return (comp, value) -> {
if (!getExpenseSheet().isCodeAutogenerated()) {
String code = (String) value;
if (code == null || code.isEmpty()) {
throw new WrongValueException(comp, _("Code cannot be empty."));
} else {
String oldCode = line.getCode();
line.setCode(code);
if (!getExpenseSheet().isNonRepeatedExpenseSheetLinesCodesConstraint()) {
line.setCode(oldCode);
throw new WrongValueException(comp, _("The code must be unique."));
}
}
}
};
}
private void setOrderElementInESL(Listitem selectedItem, ExpenseSheetLine line) {
OrderElement orderElement = selectedItem == null ? null : (OrderElement) selectedItem.getValue();
line.setOrderElement(orderElement);
}
private void setResourceInESL(Listitem selectedItem, ExpenseSheetLine expenseSheetLine) {
Resource resource = selectedItem == null ? null : (Resource) selectedItem.getValue();
expenseSheetLine.setResource(resource);
}
}
public Constraint checkConstraintExpenseValue() {
return (comp, value) -> {
BigDecimal expenseValue = (BigDecimal) value;
if (expenseValue == null || expenseValue.compareTo(BigDecimal.ZERO) < 0) {
throw new WrongValueException(comp, _("must be greater or equal than 0"));
}
};
}
public Constraint checkConstraintExpendeCode() {
return (comp, value) -> {
if (!getExpenseSheet().isCodeAutogenerated()) {
String code = (String) value;
if (code == null || code.isEmpty()) {
throw new WrongValueException(comp, _("The code cannot be empty and it must be unique."));
} else if (!getExpenseSheet().isUniqueCodeConstraint()) {
throw new WrongValueException(
comp, _("it already exists another expense sheet with the same code."));
}
}
};
}
public Date getExpenseSheetLineDate() {
if (expenseSheetModel.getNewExpenseSheetLine() != null) {
return (expenseSheetModel.getNewExpenseSheetLine().getDate() != null)
? expenseSheetModel.getNewExpenseSheetLine().getDate().toDateTimeAtStartOfDay().toDate()
: null;
}
return null;
}
public void setExpenseSheetLineDate(Date date) {
if (expenseSheetModel.getNewExpenseSheetLine() != null) {
LocalDate localDate = null;
if (date != null) {
localDate = LocalDate.fromDateFields(date);
}
expenseSheetModel.getNewExpenseSheetLine().setDate(localDate);
}
}
@Override
protected void initCreate() {
initCreate(false);
}
@Override
protected void initEdit(ExpenseSheet expenseSheet) {
expenseSheetModel.prepareToEdit(expenseSheet);
loadComponentsEditWindow();
}
@Override
protected ExpenseSheet getEntityBeingEdited() {
return expenseSheetModel.getExpenseSheet();
}
@Override
public void delete(ExpenseSheet expenseSheet) throws InstanceNotFoundException {
expenseSheetModel.removeExpenseSheet(expenseSheet);
}
@Override
protected String getEntityType() {
return _("Expense Sheet");
}
@Override
protected String getPluralEntityType() {
return _("Expense Sheets");
}
public String getCurrencySymbol() {
return Util.getCurrencySymbol();
}
public String getMoneyFormat() {
return Util.getMoneyFormat();
}
@Override
public void goToCreatePersonalExpenseSheet() {
if (!SecurityUtils.isUserInRole(UserRole.ROLE_BOUND_USER)) {
Util.sendForbiddenStatusCodeInHttpServletResponse();
}
state = CRUDControllerState.CREATE;
initCreate(true);
showEditWindow();
fromUserDashboard = true;
}
private void initCreate(boolean personal) {
expenseSheetModel.initCreate(personal);
loadComponentsEditWindow();
}
public String getResource() {
Resource resource = expenseSheetModel.getResource();
return resource == null ? "" : resource.getShortDescription();
}
@Override
public void goToEditPersonalExpenseSheet(ExpenseSheet expenseSheet) {
if ( !SecurityUtils.isUserInRole(UserRole.ROLE_BOUND_USER) ||
!expenseSheetModel.isPersonalAndBelongsToCurrentUser(expenseSheet) )
Util.sendForbiddenStatusCodeInHttpServletResponse();
goToEditForm(expenseSheet);
fromUserDashboard = true;
}
@Override
protected void showListWindow() {
if (fromUserDashboard) {
String url = IndexController.USER_DASHBOARD_URL;
if (!cancel)
url += "?expense_sheet_saved=" + expenseSheetModel.getExpenseSheet().getCode();
Executions.getCurrent().sendRedirect(url);
} else {
super.showListWindow();
}
}
@Override
protected void cancel() {
cancel = true;
}
public String getType() {
return getType(expenseSheetModel.getExpenseSheet());
}
private String getType(ExpenseSheet expenseSheet) {
return expenseSheet != null && expenseSheet.isPersonal() ? _("Personal") : _("Regular");
}
public RowRenderer getExpenseSheetsRenderer() {
return (row, data, i) -> {
final ExpenseSheet expenseSheet = (ExpenseSheet) data;
row.setValue(expenseSheet);
Util.appendLabel(row, expenseSheet.getFirstExpense().toString());
Util.appendLabel(row, expenseSheet.getLastExpense().toString());
Util.appendLabel(row, Util.addCurrencySymbol(expenseSheet.getTotal()));
Util.appendLabel(row, expenseSheet.getCode());
Util.appendLabel(row, expenseSheet.getDescription());
Util.appendLabel(row, getType(expenseSheet));
Util.appendOperationsAndOnClickEvent(
row, event -> goToEditForm(expenseSheet), event -> confirmDelete(expenseSheet));
};
}
}