/* * 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.user.client.rpc.AsyncCallback; import javax.inject.Inject; import com.google.inject.Provider; import org.seamless.gwt.component.client.AbstractEventListeningPresenter; import org.fourthline.konto.client.service.LedgerServiceAsync; import org.fourthline.konto.client.ledger.entry.event.EntryEditAmountUpdated; import org.fourthline.konto.client.ledger.entry.event.EntryEditCanceled; import org.fourthline.konto.client.ledger.entry.event.EntryEditStarted; import org.fourthline.konto.client.ledger.entry.event.EntryEditSubmit; import org.fourthline.konto.client.ledger.entry.event.EntryModified; import org.fourthline.konto.client.ledger.entry.event.EntryRemoved; import org.fourthline.konto.client.ledger.entry.view.EntrySummaryView; import org.fourthline.konto.client.ledger.entry.view.EntryView; 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.client.settings.GlobalSettings; import org.fourthline.konto.client.settings.event.GlobalSettingsRefreshedEvent; import org.fourthline.konto.shared.LedgerEntry; import org.fourthline.konto.shared.entity.Account; import org.fourthline.konto.shared.DebitCreditHolder; import org.fourthline.konto.shared.entity.Entry; import org.fourthline.konto.shared.MonetaryAmount; import org.fourthline.konto.shared.entity.Split; import org.seamless.gwt.validation.shared.Validatable; import org.seamless.gwt.validation.shared.ValidationError; import org.seamless.gwt.validation.shared.ValidationException; import org.fourthline.konto.shared.entity.settings.GlobalOption; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; /** * @author Christian Bauer */ public class EntryPresenter extends AbstractEventListeningPresenter implements EntryView.Presenter, GlobalSettingsRefreshedEvent.Handler, EntryEditSubmit.Handler, EntryEditAmountUpdated.Handler { final EntryView view; final EventBus bus; final LedgerServiceAsync service; GlobalSettings globalSettings; Account currentAccount; Entry entry; // Sub-presenters final Provider<SplitView.Presenter> splitPresenterProvider; final Provider<EntrySummaryView.Presenter> entrySummaryPresenterProvider; final List<SplitView.Presenter> splitPresenters = new ArrayList(); EntrySummaryView.Presenter entrySummaryPresenter; @Inject public EntryPresenter(EntryView view, Provider<SplitView.Presenter> splitPresenterProvider, Provider<EntrySummaryView.Presenter> entrySummaryPresenterProvider, EventBus bus, LedgerServiceAsync service, GlobalSettings globalSettings) { this.view = view; this.splitPresenterProvider = splitPresenterProvider; this.entrySummaryPresenterProvider = entrySummaryPresenterProvider; this.bus = bus; this.service = service; addRegistration(bus.addHandler(GlobalSettingsRefreshedEvent.TYPE, this)); addRegistration(bus.addHandler(EntryEditSubmit.TYPE, this)); addRegistration(bus.addHandler(EntryEditAmountUpdated.TYPE, this)); onSettingsRefreshed(globalSettings); } public EntrySummaryView.Presenter getEntrySummaryPresenter() { return entrySummaryPresenter; } public List<SplitView.Presenter> getSplitPresenters() { return splitPresenters; } public Entry getEntry() { return entry; } @Override public void onSettingsRefreshed(GlobalSettings gs) { this.globalSettings = gs; view.setDateFormat(gs.getValue(GlobalOption.OPT_DATE_FORMAT)); } @Override public EntryView getView() { return view; } @Override public void startWith(Account currentAccount, LedgerEntry ledgerEntry, Date lastLedgerEntryDate) { this.currentAccount = currentAccount; Split split = null; if (ledgerEntry == null) { // New entry and one new split (each entry has at least one split) entry = new Entry(); entry.setEffectiveOn(lastLedgerEntryDate); entry.setAccountId(currentAccount.getId()); Split newSplit = new Split(currentAccount.getMonetaryUnit(), currentAccount.getMonetaryUnit()); newSplit.setEntry(entry); entry.getSplits().add(newSplit); } else if (ledgerEntry instanceof Entry) { entry = (Entry) ledgerEntry; } else { split = (Split) ledgerEntry; entry = split.getEntry(); } view.setPresenter(this); view.reset(); view.getEffectiveOnProperty().set(entry.getEffectiveOn()); // Are we looking at an account where this entry was made or the opposite side? if (split == null) { // The "onwing" side, we are looking at the same account on which this // entry was made or we are making a new entry on this account, show all the // splits of this entry for (Split s : entry.getSplits()) { addSplitPresenter(s); } // Allow addition of new splits view.showSplitAdd(); // If there is more than one split we have to update the summary line updateEntrySummaryDebitCredit(); bus.fireEvent(new EntryEditStarted(entry, splitPresenters.size() > 1)); } else { // We are looking at the "other" side, this is just a split of potentially // many splits of this entry. The entry was made on a different account addSplitPresenter(split); // Deny adding new splits view.hideSplitAdd(); bus.fireEvent(new EntryEditStarted(split, splitPresenters.size() > 1)); } if (entry.getId() == null) { view.focus(globalSettings.getValue(GlobalOption.OPT_NEW_ENTRY_SELECT_DAY)); } else { focusSplit(true); } } protected void addSplitPresenter(Split split) { SplitView.Presenter splitPresenter = splitPresenterProvider.get(); splitPresenter.startWith(currentAccount, split); if (view.getEffectiveOnProperty().get() != null) splitPresenter.setEffectiveDate(view.getEffectiveOnProperty().get()); splitPresenters.add(splitPresenter); view.addSplitView(splitPresenter.getView()); if (splitPresenters.size() > 1) { // Second (or more) split added, need to show delete button view.showSplitDelete(); // .. update the entry description with the first split's description if (entry.getDescription() == null) entry.setDescription( splitPresenters.get(0).getView().getDescription().length() > 0 ? splitPresenters.get(0).getView().getDescription() : null ); // ... show the entry summary createEntrySummaryPresenter(); // .. and update the summary line's amounts updateEntrySummaryDebitCredit(); } } protected void removeSplitPresenter(int index) { splitPresenters.remove(index); view.removeSplitView(index); if (splitPresenters.size() < 2) { // Only one left, can't remove that, hide delete button view.hideSplitDelete(); // .. but keep the description of the summary if (entrySummaryPresenter.getView().getDescription().length() > 0) { splitPresenters.get(0).getView().setDescription( entrySummaryPresenter.getView().getDescription() ); } // .. and remove summary removeEntrySummaryPresenter(); } } protected void createEntrySummaryPresenter() { if (entrySummaryPresenter == null) { entrySummaryPresenter = entrySummaryPresenterProvider.get(); entrySummaryPresenter.startWith(currentAccount, entry); view.showEntrySummaryView(entrySummaryPresenter.getView()); } } protected void removeEntrySummaryPresenter() { entrySummaryPresenter = null; view.removeEntrySummaryView(); } @Override public void dateEntered(Date date) { for (SplitView.Presenter splitPresenter : splitPresenters) { splitPresenter.setEffectiveDate(date); } } @Override public void addSplit() { Split split = new Split(currentAccount.getMonetaryUnit(), currentAccount.getMonetaryUnit()); split.setEntry(entry); entry.getSplits().add(split); addSplitPresenter(split); bus.fireEvent(new EntryEditStarted(entry, splitPresenters.size() > 1, true)); focusSplit(splitPresenters.size() > 2); } @Override public void removeSplit(int index) { if (splitPresenters.size() == 1) return; // Can't delete last one, SplitView.Presenter splitPresenter = splitPresenters.get(index); Split split = splitPresenter.getSplit(); split.getEntry().getSplits().remove(split); if (split.getId() != null) { split.getEntry().getOrphanedSplits().add(split); } removeSplitPresenter(index); if (splitPresenters.size() == 1) { bus.fireEvent(new EntryEditStarted(entry, false)); } focusSplit(true); } @Override public void onEntryEditSubmit(EntryEditSubmit event) { saveEntry(); } @Override public void onEntryEditAmountUpdated(EntryEditAmountUpdated event) { updateEntrySummaryDebitCredit(); } @Override public void saveEntry() { clearValidationErrors(); // Client-side validation including view/model data binding List<ValidationError> errors = flushView(); if (errors.size() > 0) { bus.fireEvent(new NotifyEvent( new Message(Level.WARNING, "Can't save entry", "Please correct your input.") )); showValidationErrors(errors); return; } service.store(entry, new AsyncCallback<Void>() { @Override public void onFailure(Throwable caught) { if (caught instanceof ValidationException) { ValidationException ex = (ValidationException) caught; // This is probably a FK violation if (!ex.hasErrors()) { bus.fireEvent(new NotifyEvent( new Message( Level.WARNING, "Can't save entry, errors on server", ex.getMessage() ) )); } showValidationErrors(ex.getErrors()); } else { bus.fireEvent(new ServerFailureNotifyEvent(caught)); } } @Override public void onSuccess(Void result) { bus.fireEvent(new EntryModified(entry)); } }); } @Override public void deleteEntry() { service.remove(entry, new AsyncCallback<Void>() { @Override public void onFailure(Throwable caught) { bus.fireEvent(new ServerFailureNotifyEvent(caught)); } @Override public void onSuccess(Void result) { bus.fireEvent(new EntryRemoved(entry)); } }); } @Override public void cancel() { bus.fireEvent(new EntryEditCanceled()); } public void clearValidationErrors() { view.getEffectiveOnProperty().clearValidationError(); for (SplitView.Presenter splitPresenter : splitPresenters) { splitPresenter.clearValidationErrors(); } if (entrySummaryPresenter != null) { entrySummaryPresenter.clearValidationErrors(); } } public void showValidationErrors(List<ValidationError> errors) { // Seperate the Split errors from the Entry errors (many entries) Map<Integer, List<ValidationError>> splitErrors = new HashMap(); List<ValidationError> entrySummaryErrors = new ArrayList(); for (ValidationError error : errors) { // Find out which index (error id) is given, so we can pick the right sub-presenter // for displaying the error if (Split.class.getName().equals(error.getEntity()) && error.getId() != null) { int splitIndex = Integer.valueOf(error.getId()); List<ValidationError> splitErrorList = splitErrors.get(splitIndex); if (splitErrorList == null) { splitErrorList = new ArrayList(); splitErrors.put(splitIndex, splitErrorList); } splitErrorList.add(error); } else if (Entry.class.getName().equals(error.getEntity()) && Entry.Property.effectiveOn.equals(error.getProperty())) { view.getEffectiveOnProperty().showValidationError(error); bus.fireEvent(new NotifyEvent( new Message(Level.WARNING, "Invalid date", error.getMessage()) )); } else if (Entry.class.getName().equals(error.getEntity()) && Entry.Property.description.equals(error.getProperty())) { entrySummaryErrors.add(error); } else { bus.fireEvent(new ValidationErrorNotifyEvent(error)); } } // Show errors for each sub-presenter if (entrySummaryPresenter != null) { entrySummaryPresenter.showValidationErrors(entrySummaryErrors); } for (Map.Entry<Integer, List<ValidationError>> me : splitErrors.entrySet()) { if (splitPresenters.size() >= me.getKey()) { SplitView.Presenter splitPresenter = splitPresenters.get(me.getKey()); if (splitPresenter != null) splitPresenter.showValidationErrors(me.getValue()); } } } protected void focusSplit(boolean lastSplit) { if (splitPresenters.size() == 0) return; splitPresenters.get( lastSplit ? splitPresenters.size() - 1 : 0 ).getView().focus(); } protected List<ValidationError> flushView() { List<ValidationError> errors = new ArrayList(); Date effectiveOn = view.getEffectiveOnProperty().get(); if (effectiveOn == null) effectiveOn = new Date(); if (effectiveOn.getTime() < currentAccount.getEffectiveOn().getTime()) { errors.add(new ValidationError( Entry.class.getName(), Entry.Property.effectiveOn, "Date of entry can't be before account's starting date." )); } else { entry.setEffectiveOn(effectiveOn); } entry.setEnteredOn(new Date()); // Flush sub-presenters and give them their index so they can add validation errors for (int i = 0; i < splitPresenters.size(); i++) { SplitView.Presenter splitPresenter = splitPresenters.get(i); errors.addAll(splitPresenter.flushView(i)); } if (entrySummaryPresenter != null) { errors.addAll(entrySummaryPresenter.flushView()); } // If this entry has a single split, transfer the description if (entry.getSplits().size() == 1) { entry.setDescription(entry.getSplits().get(0).getDescription()); } // Flushing was OK (that included data binding), now model integrity validation if (errors.size() == 0) { errors.addAll(entry.validate(Validatable.GROUP_CLIENT)); } return errors; } protected void updateEntrySummaryDebitCredit() { if (entrySummaryPresenter == null) return; MonetaryAmount sum = new MonetaryAmount(currentAccount.getMonetaryUnit()); for (SplitView.Presenter splitPresenter : splitPresenters) { sum = sum.add(DebitCreditHolder.Accessor.getDebitOrCredit(splitPresenter.getView())); } DebitCreditHolder.Accessor.setDebitOrCredit(entrySummaryPresenter, sum); } }