package name.abuchen.portfolio.ui.wizards.datatransfer; import java.io.File; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ColumnPixelData; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.jface.viewers.ComboViewer; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StyledCellLabelProvider; import org.eclipse.jface.viewers.StyledString; import org.eclipse.jface.viewers.StyledString.Styler; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.jface.wizard.IWizardPage; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.TextStyle; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Table; import name.abuchen.portfolio.datatransfer.Extractor; import name.abuchen.portfolio.datatransfer.ImportAction; import name.abuchen.portfolio.datatransfer.actions.CheckCurrenciesAction; import name.abuchen.portfolio.datatransfer.actions.CheckValidTypesAction; import name.abuchen.portfolio.datatransfer.actions.DetectDuplicatesAction; import name.abuchen.portfolio.model.Account; import name.abuchen.portfolio.model.AccountTransaction; import name.abuchen.portfolio.model.AccountTransferEntry; import name.abuchen.portfolio.model.Annotated; import name.abuchen.portfolio.model.BuySellEntry; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.model.Portfolio; import name.abuchen.portfolio.model.PortfolioTransaction; import name.abuchen.portfolio.model.PortfolioTransferEntry; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.money.Values; import name.abuchen.portfolio.ui.AbstractClientJob; import name.abuchen.portfolio.ui.Images; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.PortfolioPlugin; import name.abuchen.portfolio.ui.util.FormDataFactory; import name.abuchen.portfolio.ui.util.LabelOnly; import name.abuchen.portfolio.ui.wizards.AbstractWizardPage; public class ReviewExtractedItemsPage extends AbstractWizardPage implements ImportAction.Context { /* package */static final String PAGE_ID = "reviewitems"; //$NON-NLS-1$ private static final String IMPORT_TARGET = "import-target"; //$NON-NLS-1$ private static final String IMPORT_TARGET_PORTFOLIO = IMPORT_TARGET + "-portfolio-"; //$NON-NLS-1$ private static final String IMPORT_TARGET_ACCOUNT = IMPORT_TARGET + "-account-"; //$NON-NLS-1$ private TableViewer tableViewer; private TableViewer errorTableViewer; private Label lblPrimaryPortfolio; private ComboViewer primaryPortfolio; private Label lblSecondaryPortfolio; private ComboViewer secondaryPortfolio; private Label lblPrimaryAccount; private ComboViewer primaryAccount; private Label lblSecondaryAccount; private ComboViewer secondaryAccount; private Button cbConvertToDelivery; private final Client client; private final Extractor extractor; private final IPreferenceStore preferences; private List<File> files; private List<ExtractedEntry> allEntries = new ArrayList<ExtractedEntry>(); public ReviewExtractedItemsPage(Client client, Extractor extractor, IPreferenceStore preferences, List<File> files) { super(PAGE_ID); this.client = client; this.extractor = extractor; this.preferences = preferences; this.files = files; setTitle(extractor.getLabel()); setDescription(Messages.PDFImportWizardDescription); } public List<ExtractedEntry> getEntries() { return allEntries; } @Override public Portfolio getPortfolio() { return (Portfolio) ((IStructuredSelection) primaryPortfolio.getSelection()).getFirstElement(); } @Override public Portfolio getSecondaryPortfolio() { return (Portfolio) ((IStructuredSelection) secondaryPortfolio.getSelection()).getFirstElement(); } @Override public Account getAccount() { return (Account) ((IStructuredSelection) primaryAccount.getSelection()).getFirstElement(); } @Override public Account getSecondaryAccount() { return (Account) ((IStructuredSelection) secondaryAccount.getSelection()).getFirstElement(); } public boolean doConvertToDelivery() { return cbConvertToDelivery.getSelection(); } @Override public IWizardPage getNextPage() { return null; } @Override public void createControl(Composite parent) { Composite container = new Composite(parent, SWT.NULL); setControl(container); container.setLayout(new FormLayout()); Composite targetContainer = new Composite(container, SWT.NONE); GridLayoutFactory.fillDefaults().numColumns(4).applyTo(targetContainer); lblPrimaryAccount = new Label(targetContainer, SWT.NONE); lblPrimaryAccount.setText(Messages.ColumnAccount); Combo cmbAccount = new Combo(targetContainer, SWT.READ_ONLY); primaryAccount = new ComboViewer(cmbAccount); primaryAccount.setContentProvider(ArrayContentProvider.getInstance()); primaryAccount.setInput(client.getActiveAccounts()); primaryAccount.addSelectionChangedListener(e -> checkEntriesAndRefresh(allEntries)); lblSecondaryAccount = new Label(targetContainer, SWT.NONE); lblSecondaryAccount.setText(Messages.LabelTransferTo); lblSecondaryAccount.setVisible(false); Combo cmbAccountTarget = new Combo(targetContainer, SWT.READ_ONLY); secondaryAccount = new ComboViewer(cmbAccountTarget); secondaryAccount.setContentProvider(ArrayContentProvider.getInstance()); secondaryAccount.setInput(client.getActiveAccounts()); secondaryAccount.getControl().setVisible(false); lblPrimaryPortfolio = new Label(targetContainer, SWT.NONE); lblPrimaryPortfolio.setText(Messages.ColumnPortfolio); Combo cmbPortfolio = new Combo(targetContainer, SWT.READ_ONLY); primaryPortfolio = new ComboViewer(cmbPortfolio); primaryPortfolio.setContentProvider(ArrayContentProvider.getInstance()); primaryPortfolio.setInput(client.getActivePortfolios()); primaryPortfolio.addSelectionChangedListener(e -> checkEntriesAndRefresh(allEntries)); lblSecondaryPortfolio = new Label(targetContainer, SWT.NONE); lblSecondaryPortfolio.setText(Messages.LabelTransferTo); lblSecondaryPortfolio.setVisible(false); Combo cmbPortfolioTarget = new Combo(targetContainer, SWT.READ_ONLY); secondaryPortfolio = new ComboViewer(cmbPortfolioTarget); secondaryPortfolio.setContentProvider(ArrayContentProvider.getInstance()); secondaryPortfolio.setInput(client.getActivePortfolios()); secondaryPortfolio.getControl().setVisible(false); preselectDropDowns(); cbConvertToDelivery = new Button(container, SWT.CHECK); cbConvertToDelivery.setText(Messages.LabelConvertBuySellIntoDeliveryTransactions); Composite compositeTable = new Composite(container, SWT.NONE); Composite errorTable = new Composite(container, SWT.NONE); // // form layout // FormDataFactory.startingWith(targetContainer) // .top(new FormAttachment(0, 0)).left(new FormAttachment(0, 0)).right(new FormAttachment(100, 0)) .thenBelow(cbConvertToDelivery) // .thenBelow(compositeTable).right(targetContainer).bottom(new FormAttachment(70, 0)) // .thenBelow(errorTable).right(targetContainer).bottom(new FormAttachment(100, 0)); // // table & columns // TableColumnLayout layout = new TableColumnLayout(); compositeTable.setLayout(layout); tableViewer = new TableViewer(compositeTable, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); tableViewer.setContentProvider(ArrayContentProvider.getInstance()); Table table = tableViewer.getTable(); table.setHeaderVisible(true); table.setLinesVisible(true); addColumns(tableViewer, layout); attachContextMenu(table); layout = new TableColumnLayout(); errorTable.setLayout(layout); errorTableViewer = new TableViewer(errorTable, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); errorTableViewer.setContentProvider(ArrayContentProvider.getInstance()); table = errorTableViewer.getTable(); table.setHeaderVisible(true); table.setLinesVisible(true); addColumnsExceptionTable(errorTableViewer, layout); } private void preselectDropDowns() { // idea: generally one type of document (i.e. from the same bank) will // be imported into the same account List<Account> activeAccounts = client.getActiveAccounts(); if (!activeAccounts.isEmpty()) { String uuid = preferences.getString(IMPORT_TARGET_ACCOUNT + extractor.getClass().getSimpleName()); // do not trigger selection listener (-> do not user #setSelection) primaryAccount.getCombo().select(IntStream.range(0, activeAccounts.size()) .filter(i -> activeAccounts.get(i).getUUID().equals(uuid)).findAny().orElse(0)); secondaryAccount.getCombo().select(0); } List<Portfolio> activePortfolios = client.getActivePortfolios(); if (!activePortfolios.isEmpty()) { String uuid = preferences.getString(IMPORT_TARGET_PORTFOLIO + extractor.getClass().getSimpleName()); // do not trigger selection listener (-> do not user #setSelection) primaryPortfolio.getCombo().select(IntStream.range(0, activePortfolios.size()) .filter(i -> activePortfolios.get(i).getUUID().equals(uuid)).findAny().orElse(0)); secondaryPortfolio.getCombo().select(0); } } private void addColumnsExceptionTable(TableViewer viewer, TableColumnLayout layout) { TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE); column.getColumn().setText(Messages.ColumnErrorMessages); column.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { Exception e = (Exception) element; String text = e.getMessage(); return text == null || text.isEmpty() ? e.getClass().getName() : text; } }); layout.setColumnData(column.getColumn(), new ColumnWeightData(100, true)); } private void addColumns(TableViewer viewer, TableColumnLayout layout) { TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE); column.getColumn().setText(Messages.ColumnStatus); column.setLabelProvider(new FormattedLabelProvider() { @Override public Image getImage(ExtractedEntry element) { Images image = null; switch (element.getMaxCode()) { case WARNING: image = Images.WARNING; break; case ERROR: image = Images.ERROR; break; case OK: default: } return image != null ? image.image() : null; } @Override public String getText(ExtractedEntry entry) { return ""; //$NON-NLS-1$ } }); layout.setColumnData(column.getColumn(), new ColumnPixelData(22, true)); column = new TableViewerColumn(viewer, SWT.NONE); column.getColumn().setText(Messages.ColumnDate); column.setLabelProvider(new FormattedLabelProvider() { @Override public String getText(ExtractedEntry entry) { LocalDate date = entry.getItem().getDate(); return date != null ? Values.Date.format(date) : null; } }); layout.setColumnData(column.getColumn(), new ColumnPixelData(80, true)); column = new TableViewerColumn(viewer, SWT.NONE); column.getColumn().setText(Messages.ColumnTransactionType); column.setLabelProvider(new FormattedLabelProvider() { @Override public String getText(ExtractedEntry entry) { return entry.getItem().getTypeInformation(); } @Override public Image getImage(ExtractedEntry entry) { Annotated subject = entry.getItem().getSubject(); if (subject instanceof AccountTransaction) return Images.ACCOUNT.image(); else if (subject instanceof PortfolioTransaction) return Images.PORTFOLIO.image(); else if (subject instanceof Security) return Images.SECURITY.image(); else if (subject instanceof BuySellEntry) return Images.PORTFOLIO.image(); else if (subject instanceof AccountTransferEntry) return Images.ACCOUNT.image(); else if (subject instanceof PortfolioTransferEntry) return Images.PORTFOLIO.image(); else return null; } }); layout.setColumnData(column.getColumn(), new ColumnPixelData(100, true)); column = new TableViewerColumn(viewer, SWT.RIGHT); column.getColumn().setText(Messages.ColumnAmount); column.setLabelProvider(new FormattedLabelProvider() { @Override public String getText(ExtractedEntry entry) { Money amount = entry.getItem().getAmount(); return amount != null ? Values.Money.format(amount) : null; } }); layout.setColumnData(column.getColumn(), new ColumnPixelData(80, true)); column = new TableViewerColumn(viewer, SWT.RIGHT); column.getColumn().setText(Messages.ColumnShares); column.setLabelProvider(new FormattedLabelProvider() { @Override public String getText(ExtractedEntry entry) { return Values.Share.formatNonZero(entry.getItem().getShares()); } }); layout.setColumnData(column.getColumn(), new ColumnPixelData(80, true)); column = new TableViewerColumn(viewer, SWT.NONE); column.getColumn().setText(Messages.ColumnSecurity); column.setLabelProvider(new FormattedLabelProvider() { @Override public String getText(ExtractedEntry entry) { Security security = entry.getItem().getSecurity(); return security != null ? security.getName() : null; } }); layout.setColumnData(column.getColumn(), new ColumnPixelData(250, true)); } private void attachContextMenu(final Table table) { MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$ menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(manager -> showContextMenu(manager)); final Menu contextMenu = menuMgr.createContextMenu(table.getShell()); table.setMenu(contextMenu); table.addDisposeListener(e -> { if (contextMenu != null && !contextMenu.isDisposed()) contextMenu.dispose(); }); } private void showContextMenu(IMenuManager manager) { IStructuredSelection selection = (IStructuredSelection) tableViewer.getSelection(); boolean atLeastOneImported = false; boolean atLeastOneNotImported = false; for (Object element : selection.toList()) { ExtractedEntry entry = (ExtractedEntry) element; // an entry will be imported if it is marked as to be // imported *and* not a duplicate atLeastOneImported = atLeastOneImported || entry.isImported(); // an entry will not be imported if it marked as not to be // imported *or* if it is marked as duplicate atLeastOneNotImported = atLeastOneNotImported || !entry.isImported(); } // provide a hint to the user why the entry is struck out if (selection.size() == 1) { ExtractedEntry entry = (ExtractedEntry) selection.getFirstElement(); entry.getStatus() // .filter(s -> s.getCode() != ImportAction.Status.Code.OK) // .forEach(s -> { Images image = s.getCode() == ImportAction.Status.Code.WARNING ? // Images.WARNING : Images.ERROR; manager.add(new LabelOnly(s.getMessage(), image.descriptor())); }); } if (atLeastOneImported) { manager.add(new Action(Messages.LabelDoNotImport) { @Override public void run() { for (Object element : ((IStructuredSelection) tableViewer.getSelection()).toList()) ((ExtractedEntry) element).setImported(false); tableViewer.refresh(); } }); } if (atLeastOneNotImported) { manager.add(new Action(Messages.LabelDoImport) { @Override public void run() { for (Object element : ((IStructuredSelection) tableViewer.getSelection()).toList()) ((ExtractedEntry) element).setImported(true); tableViewer.refresh(); } }); } } @Override public void beforePage() { setTitle(extractor.getLabel()); // clear all entries (if embedded into multi-page wizard) allEntries.clear(); tableViewer.setInput(allEntries); errorTableViewer.setInput(Collections.emptyList()); try { new AbstractClientJob(client, extractor.getLabel()) { @Override protected IStatus run(IProgressMonitor monitor) { monitor.beginTask(Messages.PDFImportWizardMsgExtracting, files.size()); final List<Exception> errors = new ArrayList<Exception>(); List<ExtractedEntry> entries = extractor // .extract(files, errors).stream() // .map(i -> new ExtractedEntry(i)) // .collect(Collectors.toList()); // Logging them is not a bad idea if the whole method fails PortfolioPlugin.log(errors); Display.getDefault().asyncExec(() -> setResults(entries, errors)); return Status.OK_STATUS; } }.schedule(); } catch (Exception e) { throw new UnsupportedOperationException(e); } } @Override public void afterPage() { preferences.setValue(IMPORT_TARGET_ACCOUNT + extractor.getClass().getSimpleName(), getAccount().getUUID()); preferences.setValue(IMPORT_TARGET_PORTFOLIO + extractor.getClass().getSimpleName(), getPortfolio().getUUID()); } private void setResults(List<ExtractedEntry> entries, List<Exception> errors) { checkEntries(entries); allEntries.addAll(entries); tableViewer.setInput(allEntries); errorTableViewer.setInput(errors); for (ExtractedEntry entry : entries) { if (entry.getItem() instanceof Extractor.AccountTransferItem) { lblSecondaryAccount.setVisible(true); secondaryAccount.getControl().setVisible(true); } else if (entry.getItem() instanceof Extractor.PortfolioTransferItem) { lblSecondaryPortfolio.setVisible(true); secondaryPortfolio.getControl().setVisible(true); } } } private void checkEntriesAndRefresh(List<ExtractedEntry> entries) { checkEntries(entries); tableViewer.refresh(); } private void checkEntries(List<ExtractedEntry> entries) { List<ImportAction> actions = new ArrayList<>(); actions.add(new CheckValidTypesAction()); actions.add(new DetectDuplicatesAction()); actions.add(new CheckCurrenciesAction()); for (ExtractedEntry entry : entries) { entry.clearStatus(); for (ImportAction action : actions) entry.addStatus(entry.getItem().apply(action, this)); } } static class FormattedLabelProvider extends StyledCellLabelProvider { private static Styler strikeoutStyler = new Styler() { @Override public void applyStyles(TextStyle textStyle) { textStyle.strikeout = true; } }; public String getText(ExtractedEntry element) { return null; } public Image getImage(ExtractedEntry element) { return null; } @Override public void update(ViewerCell cell) { ExtractedEntry entry = (ExtractedEntry) cell.getElement(); String text = getText(entry); if (text == null) text = ""; //$NON-NLS-1$ boolean strikeout = !entry.isImported(); StyledString styledString = new StyledString(text, strikeout ? strikeoutStyler : null); cell.setText(styledString.toString()); cell.setStyleRanges(styledString.getStyleRanges()); cell.setImage(getImage(entry)); super.update(cell); } } }