/*
* Copyright (C) 2011 4th Line GmbH, Switzerland
*
* 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.fourthline.konto.client.ledger.entry;
import com.google.web.bindery.event.shared.EventBus;
import com.google.gwt.place.shared.PlaceController;
import com.google.gwt.user.client.rpc.AsyncCallback;
import javax.inject.Inject;
import org.fourthline.konto.client.ledger.LedgerPlace;
import org.fourthline.konto.client.service.CurrencyServiceAsync;
import org.fourthline.konto.client.service.LedgerServiceAsync;
import org.fourthline.konto.client.ledger.entry.event.EntryEditAmountUpdated;
import org.fourthline.konto.client.ledger.entry.event.EntryEditSubmit;
import org.fourthline.konto.client.ledger.entry.view.ExchangeView;
import org.fourthline.konto.client.ledger.entry.view.SplitView;
import org.seamless.gwt.notify.client.Message;
import org.seamless.gwt.notify.client.ServerFailureNotifyEvent;
import org.seamless.gwt.notify.client.NotifyEvent;
import org.seamless.gwt.notify.client.ValidationErrorNotifyEvent;
import org.fourthline.konto.shared.LedgerCoordinates;
import org.fourthline.konto.shared.entity.Account;
import org.fourthline.konto.shared.DebitCreditHolder;
import org.fourthline.konto.shared.MonetaryAmount;
import org.fourthline.konto.shared.entity.MonetaryUnit;
import org.fourthline.konto.shared.entity.Split;
import org.seamless.gwt.validation.shared.ValidationError;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
/**
* @author Christian Bauer
*/
public class SplitPresenter extends DescriptionPresenter implements SplitView.Presenter {
final SplitView view;
final AccountSelectPresenter accountSelectPresenter;
final ExchangeView.Presenter exchangePresenter;
final PlaceController placeController;
Account currentAccount;
Date effectiveDate;
Split split;
boolean editEntry;
@Inject
public SplitPresenter(final SplitView view,
EventBus bus,
LedgerServiceAsync ledgerService,
CurrencyServiceAsync currencyService,
PlaceController placeController) {
super(bus, ledgerService);
this.view = view;
this.placeController = placeController;
exchangePresenter =
new ExchangePresenter(
view.getExchangeView(),
bus,
currencyService
);
accountSelectPresenter =
new AccountSelectPresenter(
view.getAccountSelectView(),
bus,
ledgerService
) {
@Override
protected void onAccountDeselect() {
super.onAccountDeselect();
updateExchange(null);
view.enableSwitch(false);
}
@Override
protected void onAccountSelection(Account selected) {
super.onAccountSelection(selected);
Long id = editEntry ? split.getAccountId() : split.getEntry().getAccountId();
if (selected != null && selected.getId().equals(id)) {
view.enableSwitch(true);
} else {
view.enableSwitch(false);
}
updateExchange(selected);
}
};
}
public AccountSelectPresenter getAccountSelectPresenter() {
return accountSelectPresenter;
}
public ExchangeView.Presenter getExchangePresenter() {
return exchangePresenter;
}
@Override
public SplitView getView() {
return view;
}
public Split getSplit() {
return split;
}
@Override
public void startWith(Account currentAccount, final Split split) {
this.currentAccount = currentAccount;
this.split = split;
// Are we on the "owning" account where this entry was created (or is it
// a new entry) or are we on the "other" side and just looking at the
// split of an entry made somewhere else. This is key to the
// logic of this presenter.
this.editEntry =
split.getEntry().getAccountId() == null
|| split.getEntry().getAccountId().equals(currentAccount.getId());
view.setPresenter(this);
view.setSplitSuggestionHandler(this);
view.setCurrentAccount(currentAccount);
view.setDescription(split.getDescription());
// Order sensitive inits here
initViewDebitCredit();
// Load the selected account (or not)
Long selectedAccountId = null;
if (editEntry && split.getAccountId() != null) {
selectedAccountId = split.getAccountId();
} else if (!editEntry && split.getEntry().getAccountId() != null) {
selectedAccountId = split.getEntry().getAccountId();
}
if (selectedAccountId != null) {
getLedgerService().getAccount(selectedAccountId, new AsyncCallback<Account>() {
@Override
public void onFailure(Throwable caught) {
bus.fireEvent(new ServerFailureNotifyEvent(caught));
}
@Override
public void onSuccess(Account result) {
accountSelectPresenter.startWith(
SplitPresenter.this.currentAccount.getId(),
result
);
updateExchange(result);
if (split.getEntry().getId() != null && split.getId() != null) {
view.enableSwitch(true);
}
}
});
} else {
accountSelectPresenter.startWith(currentAccount.getId(), null);
view.enableSwitch(false);
}
}
@Override
public void setEffectiveDate(Date date) {
exchangePresenter.updateForDay(date);
this.effectiveDate = date;
}
@Override
public boolean isNewDescription(String text) {
return split.getDescription() == null && text != null ||
!split.getDescription().equals(text);
}
@Override
public void suggest(Split suggestedSplit) {
boolean editEntrySuggested =
suggestedSplit.getEntry().getAccountId().equals(currentAccount.getId());
// Only "suggest" if the user hasn't entered anything
if (view.getDebit() == null && view.getCredit() == null) {
DebitCreditHolder.Accessor.setDebitOrCredit(
view,
editEntrySuggested
? suggestedSplit.getEntryAmount()
: suggestedSplit.getAmount()
);
}
// ... or selected anything
if (accountSelectPresenter.getSelected() == null) {
Long retrieveAccountId;
if (editEntrySuggested) {
retrieveAccountId = suggestedSplit.getAccountId();
} else {
retrieveAccountId = suggestedSplit.getEntry().getAccountId();
}
getLedgerService().getAccount(retrieveAccountId, new AsyncCallback<Account>() {
@Override
public void onFailure(Throwable caught) {
bus.fireEvent(new ServerFailureNotifyEvent(caught));
}
@Override
public void onSuccess(Account result) {
// Don't overwrite what the user might have typed meanwhile
if (accountSelectPresenter.getSelected() == null) {
accountSelectPresenter.startWith(currentAccount.getId(), result);
updateExchange(result);
}
}
});
}
}
@Override
public void debitUpdated() {
exchangePresenter.updateOriginalAmount(DebitCreditHolder.Accessor.getDebitOrCredit(view));
bus.fireEvent(new EntryEditAmountUpdated());
}
@Override
public void creditUpdated() {
exchangePresenter.updateOriginalAmount(DebitCreditHolder.Accessor.getDebitOrCredit(view));
bus.fireEvent(new EntryEditAmountUpdated());
}
@Override
public void immediateSubmit() {
bus.fireEvent(new EntryEditSubmit());
}
@Override
public void switchToOpposite() {
Account selectedAccount = accountSelectPresenter.getSelected();
if (selectedAccount == null) return;
if (split == null || split.getId() == null || split.getEntry() == null || split.getEntry().getId() == null) return;
placeController.goTo(
new LedgerPlace(
new LedgerCoordinates(
selectedAccount.getId(),
split.getEntry().getId(),
split.getId()
)
)
);
}
@Override
public List<ValidationError> flushView(int index) {
List<ValidationError> errors = new ArrayList();
if (split.getId() == null)
split.setEnteredOn(new Date());
if (view.getDescription() == null || view.getDescription().length() == 0) {
errors.add(new ValidationError(
Integer.toString(index),
Split.class.getName(),
Split.Property.description,
"Description is required."
));
} else {
split.setDescription(view.getDescription());
}
Account selectedAccount = accountSelectPresenter.getSelected();
if (selectedAccount == null) {
errors.add(new ValidationError(
Integer.toString(index),
Split.class.getName(),
Split.Property.accountId,
"Doble-entry ledger requires two accounts for each entry."
));
return errors; // Early exit, need to correct these errors first
} else {
if (editEntry) {
split.getEntry().setAccountId(currentAccount.getId());
split.setAccountId(selectedAccount.getId());
} else {
split.getEntry().setAccountId(selectedAccount.getId());
split.setAccountId(currentAccount.getId());
}
}
MonetaryAmount enteredAmount = DebitCreditHolder.Accessor.getDebitOrCredit(view);
if (enteredAmount == null || enteredAmount.signum() == 0) {
errors.add(new ValidationError(
Integer.toString(index),
Split.class.getName(),
Split.Property.amount,
"Debit or credit amount is required."
));
return errors; // Early exit, need to correct these errors first
}
MonetaryAmount otherAmount;
if (isCurrencyExchangeRequired(selectedAccount)) {
// Get the entered exchanged amount, ignoring the rate
otherAmount = exchangePresenter.getExchangedAmount();
if (otherAmount == null || otherAmount.signum() == 0) {
errors.add(new ValidationError(
Integer.toString(index),
Split.class.getName(),
Split.Property.amount,
"Exchanged amount is required."
));
return errors;
}
// If the entered amount is a credit, the other amount has to be a debit
if (enteredAmount.signum() > 0 && otherAmount.signum() > 0) {
otherAmount = otherAmount.negate();
}
} else {
// No exchange required, just balance the entered amount
otherAmount = enteredAmount.negate();
}
// Flip it if necessary (we are not looking at the account the entry belongs to)
if (editEntry) {
split.setEntryAmount(enteredAmount);
split.setAmount(otherAmount);
} else {
split.setEntryAmount(otherAmount);
split.setAmount(enteredAmount);
}
return errors;
}
@Override
public void clearValidationErrors() {
view.clearValidationErrorDescription();
view.clearValidationErrorAmount();
view.clearValidationErrorAccount();
exchangePresenter.clearValidationErrors();
}
@Override
public void showValidationErrors(List<ValidationError> errors) {
List<ValidationError> exchangeErrors = new ArrayList();
StringBuilder sb = new StringBuilder();
for (ValidationError error : errors) {
if (Split.Property.description.equals(error.getProperty())) {
view.showValidationErrorDescription(error);
sb.append(error.getMessage()).append(" ");
} else if (Split.Property.amount.equals(error.getProperty())) {
view.showValidationErrorAmount(error);
sb.append(error.getMessage()).append(" ");
exchangeErrors.add(error);
} else if (Split.Property.accountId.equals(error.getProperty())) {
view.showValidationErrorAccount(error);
sb.append(error.getMessage()).append(" ");
} else {
bus.fireEvent(new ValidationErrorNotifyEvent(error));
}
}
exchangePresenter.showValidationErrors(exchangeErrors);
if (sb.length() > 0) {
bus.fireEvent(new NotifyEvent(
new Message(
Level.WARNING,
"Invalid entry and/or split data", sb.toString()
)
));
}
}
protected void initViewDebitCredit() {
if (editEntry) {
DebitCreditHolder.Accessor.setDebitOrCredit(view, split.getEntryAmount());
} else {
DebitCreditHolder.Accessor.setDebitOrCredit(view, split.getAmount());
}
}
protected boolean isCurrencyExchangeRequired(Account targetAccount) {
return !targetAccount.getMonetaryUnitId().equals(currentAccount.getMonetaryUnitId());
}
protected void updateExchange(Account targetAccount) {
if (targetAccount != null && isCurrencyExchangeRequired(targetAccount)) {
Date forDay = effectiveDate != null ? effectiveDate : new Date();
MonetaryUnit fromUnit = currentAccount.getMonetaryUnit();
MonetaryUnit toUnit = targetAccount.getMonetaryUnit();
MonetaryAmount originalAmount = DebitCreditHolder.Accessor.getDebitOrCredit(view);
// Update the split's internal currencies and show the exchange form
if (editEntry) {
split.setEntryMonetaryUnit(currentAccount.getMonetaryUnit());
split.setMonetaryUnit(targetAccount.getMonetaryUnit());
exchangePresenter.startWith(
forDay,
fromUnit,
toUnit,
originalAmount,
split.getAmount()
);
} else {
split.setEntryMonetaryUnit(targetAccount.getMonetaryUnit());
split.setMonetaryUnit(currentAccount.getMonetaryUnit());
exchangePresenter.startWith(
forDay,
currentAccount.getMonetaryUnit(),
targetAccount.getMonetaryUnit(),
originalAmount,
split.getEntryAmount()
);
}
view.showExchangeView(true);
} else {
view.showExchangeView(false);
}
}
}