/* * Copyright (C) 2014 GG-Net GmbH - Oliver Günther * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package eu.ggnet.dwoss.misc.op.listings; import java.awt.Color; import java.io.*; import java.net.URL; import java.text.DecimalFormat; import java.util.*; import java.util.stream.Collectors; import javax.ejb.Stateless; import javax.enterprise.inject.Instance; import javax.inject.Inject; import javax.persistence.EntityManager; import net.sf.jasperreports.engine.*; import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource; import net.sf.jasperreports.engine.util.JRSaver; import org.apache.commons.lang.StringUtils; import org.apache.commons.mail.*; import org.slf4j.*; import eu.ggnet.dwoss.configuration.GlobalConfig; import eu.ggnet.dwoss.mandator.api.service.*; import eu.ggnet.dwoss.mandator.api.service.FtpConfiguration.UploadCommand; import eu.ggnet.dwoss.mandator.api.value.Mandator; import eu.ggnet.dwoss.mandator.api.value.partial.ListingMailConfiguration; import eu.ggnet.dwoss.progress.MonitorFactory; import eu.ggnet.dwoss.progress.SubMonitor; import eu.ggnet.dwoss.rules.*; import eu.ggnet.dwoss.stock.assist.Stocks; import eu.ggnet.dwoss.stock.eao.StockUnitEao; import eu.ggnet.dwoss.stock.entity.StockUnit; import eu.ggnet.dwoss.uniqueunit.assist.UniqueUnits; import eu.ggnet.dwoss.uniqueunit.eao.UniqueUnitEao; import eu.ggnet.dwoss.uniqueunit.entity.*; import eu.ggnet.dwoss.uniqueunit.format.UniqueUnitFormater; import eu.ggnet.dwoss.util.*; import eu.ggnet.lucidcalc.*; import eu.ggnet.lucidcalc.CFormat.Representation; import static eu.ggnet.lucidcalc.CFormat.HorizontalAlignment.*; import static eu.ggnet.lucidcalc.CFormat.VerticalAlignment.*; /** * Operation for all Saleslistings. * <p/> * @author oliver.guenther */ @Stateless public class SalesListingProducerOperation implements SalesListingProducer { public static class UniqueUnitComparator implements Comparator<UniqueUnit> { @Override public int compare(UniqueUnit o1, UniqueUnit o2) { if ( o1 == null && o2 == null ) return 0; if ( o1 == null ) return 1; if ( o2 == null ) return -1; Product p1 = o1.getProduct(); Product p2 = o2.getProduct(); // Safetynet till product @NotNull if ( p1 == null || p2 == null ) return o1.getRefurbishId().compareTo(o2.getRefurbishId()); if ( p1.getGroup() != p2.getGroup() ) return p1.getGroup().compareTo(p2.getGroup()); if ( p1.getTradeName() != p2.getTradeName() ) return p1.getTradeName().compareTo(p2.getTradeName()); if ( !p1.getName().equals(p2.getName()) ) return p1.getName().compareTo(p2.getName()); if ( !p1.getPartNo().equals(p2.getPartNo()) ) return p1.getPartNo().compareTo(p2.getPartNo()); return o1.getRefurbishId().compareTo(o2.getRefurbishId()); } }; private final static Logger L = LoggerFactory.getLogger(SalesListingProducerOperation.class); private final static CFormat LFT = new CFormat(LEFT); private final static CFormat EURO = new CFormat(CFormat.Representation.CURRENCY_EURO); @Inject private ImageFinder imageFinder; @Inject @Stocks private EntityManager stockEm; @Inject @UniqueUnits private EntityManager uuEm; @Inject private Mandator mandator; @Inject private MonitorFactory monitorFactory; @Inject private Instance<ListingService> listingService; @Override public FileJacket generateAllSalesListing() { SubMonitor m = monitorFactory.newSubMonitor("All List", 5); m.message("loading Units"); m.start(); List<StockUnit> stockUnits = new StockUnitEao(stockEm).findByNoLogicTransaction(); List<UniqueUnit> uniqueUnits = new UniqueUnitEao(uuEm).findByIds(toUniqueUnitIds(stockUnits)); m.worked(3, "preparing Units"); List<Object[]> retailers = new ArrayList<>(stockUnits.size()); List<Object[]> customers = new ArrayList<>(stockUnits.size()); for (Map.Entry<UniqueUnit, StockUnit> entry : toSortedMap(uniqueUnits, stockUnits, new UniqueUnitComparator()).entrySet()) { UniqueUnit uu = entry.getKey(); StockUnit su = entry.getValue(); Product p = uu.getProduct(); Date firstPriced = null; for (PriceHistory priceHistory : uu.getPriceHistory()) { if ( firstPriced == null || firstPriced.after(priceHistory.getDate()) ) firstPriced = priceHistory.getDate(); } String source = "Automatisch"; if ( p != null && p.getFlags().contains(Product.Flag.PRICE_FIXED) ) source = "Manuell (Artikel)"; else if ( uu.getFlags().contains(UniqueUnit.Flag.PRICE_FIXED) ) source = "Manuell (Gerät)"; Object[] row = { uu.getRefurbishId(), (p == null ? null : p.getPartNo()), (p == null ? null : p.getGroup().getNote()), (p == null ? null : p.getTradeName().getName()), (p == null ? null : p.getName()), (p == null ? null : p.getDescription()), uu.getWarranty().getName(), uu.getWarrentyValid(), UniqueUnitFormater.toSingleLineAccessories(uu), uu.getCondition().getNote(), UniqueUnitFormater.toSingleLineComment(uu), uu.getPrice(PriceType.RETAILER), uu.getPrice(PriceType.CUSTOMER), (!uu.hasPrice(PriceType.CUSTOMER) ? null : MathUtil.roundedApply(uu.getPrice(PriceType.CUSTOMER), GlobalConfig.TAX, 0)), (su.getStock() == null ? su.getTransaction() : su.getStock().getName()), uu.getMfgDate(), uu.getInputDate(), firstPriced, source }; if ( uu.getSalesChannel() == SalesChannel.CUSTOMER && uu.hasPrice(PriceType.CUSTOMER) ) customers.add(row); else if ( uu.getSalesChannel() == SalesChannel.RETAILER && (uu.hasPrice(PriceType.CUSTOMER) || uu.hasPrice(PriceType.RETAILER)) ) retailers.add(row); } m.worked(1, "creating File, Endkundengeräte: " + customers.size() + ", Händlergeräte: " + retailers.size()); STable consumerTable = new STable(); consumerTable.setTableFormat(new CFormat(CENTER, TOP, new CBorder(Color.GRAY, CBorder.LineStyle.THIN), true)); consumerTable.setHeadlineFormat(new CFormat(CFormat.FontStyle.BOLD, Color.BLACK, Color.LIGHT_GRAY, CENTER, MIDDLE)); consumerTable.setRowHeight(1000); consumerTable.add(new STableColumn("SopoNr", 12)); consumerTable.add(new STableColumn("ArtikelNr", 15)); consumerTable.add(new STableColumn("Warengruppe", 18)); consumerTable.add(new STableColumn("Hersteller", 15)); consumerTable.add(new STableColumn("Bezeichnung", 30)); consumerTable.add(new STableColumn("Beschreibung", 60, LFT)); consumerTable.add(new STableColumn("Garantie", 18, LFT)); consumerTable.add(new STableColumn("Garantie bis", 18, new CFormat(Representation.SHORT_DATE))); consumerTable.add(new STableColumn("Zubehör", 30, LFT)); consumerTable.add(new STableColumn("optische Bewertung", 25)); consumerTable.add(new STableColumn("Bemerkung", 50, LFT)); consumerTable.add(new STableColumn("Händler", 15, EURO)); consumerTable.add(new STableColumn("Endkunde", 15, EURO)); consumerTable.add(new STableColumn("E.inc.Mwst", 15, EURO)); consumerTable.add(new STableColumn("Lager", 18)); consumerTable.add(new STableColumn("Mfg Datum", 18, new CFormat(Representation.SHORT_DATE))); consumerTable.add(new STableColumn("Aufnahme Datum", 18, new CFormat(Representation.SHORT_DATE))); consumerTable.add(new STableColumn("Erstmalig Bepreist", 18, new CFormat(Representation.SHORT_DATE))); consumerTable.add(new STableColumn("Preis Quelle", 18)); consumerTable.setModel(new STableModelList(customers)); STable retailerTable = new STable(consumerTable); retailerTable.setModel(new STableModelList(retailers)); CCalcDocument cdoc = new TempCalcDocument(); cdoc.add(new CSheet("Endkunde", consumerTable)); cdoc.add(new CSheet("Händler", retailerTable)); FileJacket fj = new FileJacket("All", ".xls", LucidCalc.createWriter(LucidCalc.Backend.XLS).write(cdoc)); m.finish(); return fj; } /** * Returns the next Image Id. * <p/> * @return the next Image Id. */ @Override public int nextImageId() { return imageFinder.nextImageId(); } private List<Integer> toUniqueUnitIds(List<StockUnit> stockUnits) { List<Integer> uuids = new ArrayList<>(stockUnits.size()); for (StockUnit stockUnit : stockUnits) { uuids.add(stockUnit.getUniqueUnitId()); } return uuids; } private SortedMap<UniqueUnit, StockUnit> toSortedMap(List<UniqueUnit> uniqueUnits, List<StockUnit> stockUnits, Comparator<UniqueUnit> comparator) { Map<Integer, UniqueUnit> uuIdMs = new HashMap<>(uniqueUnits.size()); for (UniqueUnit uniqueUnit : uniqueUnits) { uuIdMs.put(uniqueUnit.getId(), uniqueUnit); } SortedMap<UniqueUnit, StockUnit> uusu = new TreeMap<>(comparator); for (StockUnit stockUnit : stockUnits) { uusu.put(uuIdMs.get(stockUnit.getUniqueUnitId()), stockUnit); } return uusu; } /** * * @param name the fileName in the jar, but without jrxml * @return */ private String compileReportToTempFile(final String name) { // Optimize, only do it on updates. String reportFile = TempUtil.getDirectory("jasper") + "/" + name + ".jasper"; URL url = Objects.requireNonNull(getClass().getResource(name + ".jrxml"), "The Resource " + getClass().getPackage() + "/" + name + ".jrxml not found."); try (InputStream is = url.openStream()) { JRSaver.saveObject(JasperCompileManager.compileReport(is), reportFile); return reportFile; } catch (IOException | JRException ex) { throw new RuntimeException(ex); } } @Override public List<FileJacket> generateListings(ListingActionConfiguration config) throws UserInfoException { Map<TradeName, Collection<FileJacket>> result = new HashMap<>(); switch (config.getType()) { case XLS: result = generateXlsListings(config.getChannel()); break; case PDF: result = generatePdfListings(config.getChannel()); break; } List<FileJacket> jackets = result.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); switch (config.getLocation()) { case LOCAL: return jackets; case MAIL: prepareAndSend(jackets); break; case FTP: prepareAndUpload(result); break; } return null; } /** * Generates XLS files for units in a specific sales channel. * The lists are seperated by brand. * <p> * @param channel the saleschannel * @return XLS files for units in a specific sales channel. */ private Map<TradeName, Collection<FileJacket>> generateXlsListings(SalesChannel channel) { SubMonitor m = monitorFactory.newSubMonitor("Listen für " + channel.getName() + " erstellen", 100); m.start(); List<StockUnit> stockUnits = new StockUnitEao(stockEm).findByNoLogicTransactionAndPresentStock(); List<UniqueUnit> uniqueUnits = new UniqueUnitEao(uuEm).findByIds(toUniqueUnitIds(stockUnits)); Map<TradeName, List<UniqueUnit>> units = uniqueUnits.stream().collect(Collectors.groupingBy(uu -> uu.getProduct().getTradeName())); m.worked(2, "prüfe und filtere Geräte"); Map<TradeName, Collection<FileJacket>> files = new HashMap<>(); for (TradeName k : units.keySet()) { List<UniqueUnit> uus = units.get(k); Collections.sort(uus, new UniqueUnitComparator()); List<Object[]> rows = new ArrayList<>(); for (UniqueUnit get : uus) { UniqueUnit uu = get; Product p = uu.getProduct(); // Cases to filter out. if ( uu.getSalesChannel() != channel ) continue; if ( !uu.hasPrice((channel == SalesChannel.CUSTOMER ? PriceType.CUSTOMER : PriceType.RETAILER)) ) continue; Object[] row = { uu.getRefurbishId(), p.getPartNo(), p.getGroup().getNote(), p.getTradeName().getName(), p.getName(), p.getDescription(), uu.getWarranty().getName(), uu.getWarrentyValid(), UniqueUnitFormater.toSingleLineAccessories(uu), uu.getCondition().getNote(), UniqueUnitFormater.toSingleLineComment(uu), uu.getPrice(PriceType.RETAILER), uu.getPrice(PriceType.CUSTOMER), (!uu.hasPrice(PriceType.CUSTOMER) ? null : MathUtil.roundedApply(uu.getPrice(PriceType.CUSTOMER), GlobalConfig.TAX, 0)),}; rows.add(row); } if ( rows.isEmpty() ) continue; m.worked(5, "creating File, Geräte: " + rows.size()); STable unitTable = new STable(); unitTable.setTableFormat(new CFormat(CENTER, TOP, new CBorder(Color.GRAY, CBorder.LineStyle.THIN), true)); unitTable.setHeadlineFormat(new CFormat(CFormat.FontStyle.BOLD, Color.BLACK, Color.LIGHT_GRAY, CENTER, MIDDLE)); unitTable.setRowHeight(1000); unitTable.add(new STableColumn("SopoNr", 12)); unitTable.add(new STableColumn("ArtikelNr", 15)); unitTable.add(new STableColumn("Warengruppe", 18)); unitTable.add(new STableColumn("Hersteller", 15)); unitTable.add(new STableColumn("Bezeichnung", 30)); unitTable.add(new STableColumn("Beschreibung", 60, LFT)); unitTable.add(new STableColumn("Garantie", 18, LFT)); unitTable.add(new STableColumn("Garantie bis", 18, new CFormat(Representation.SHORT_DATE))); unitTable.add(new STableColumn("Zubehör", 30, LFT)); unitTable.add(new STableColumn("optische Bewertung", 25)); unitTable.add(new STableColumn("Bemerkung", 50, LFT)); unitTable.add(new STableColumn("Händler", 15, EURO)); unitTable.add(new STableColumn("Endkunde", 15, EURO)); unitTable.add(new STableColumn("E.inc.Mwst", 15, EURO)); unitTable.setModel(new STableModelList(rows)); CCalcDocument cdoc = new TempCalcDocument(); cdoc.add(new CSheet("Sonderposten", unitTable)); files.put(k, Arrays.asList(new FileJacket(k.getName() + " Liste", ".xls", LucidCalc.createWriter(LucidCalc.Backend.XLS).write(cdoc)))); } m.finish(); return files; } /** * Generates PDF files for units in a specific sales channel. * The lists are seperated by brand. * <p> * @param channel the saleschannel * @return PDF files for units in a specific sales channel. */ private Map<TradeName, Collection<FileJacket>> generatePdfListings(SalesChannel channel) throws UserInfoException { SubMonitor m = monitorFactory.newSubMonitor("Endkundenlisten erstellen", 10); m.message("lade Gerätedaten"); m.start(); List<StockUnit> stockUnits = new StockUnitEao(stockEm).findByNoLogicTransaction(); List<UniqueUnit> uniqueUnits = new UniqueUnitEao(uuEm).findByIds(toUniqueUnitIds(stockUnits)); PriceType priceType = (channel == SalesChannel.CUSTOMER ? PriceType.CUSTOMER : PriceType.RETAILER); m.worked(2, "prüfe und filtere Geräte"); SortedMap<UniqueUnit, StockUnit> uusus = toSortedMap(uniqueUnits, stockUnits, new UniqueUnitComparator()); for (Iterator<Map.Entry<UniqueUnit, StockUnit>> it = uusus.entrySet().iterator(); it.hasNext();) { Map.Entry<UniqueUnit, StockUnit> entry = it.next(); UniqueUnit uu = entry.getKey(); StockUnit su = entry.getValue(); if ( uu == null ) throw new NullPointerException(su + " has no UniqueUnit, Database Error"); if ( uu.getSalesChannel() != channel || !uu.hasPrice(priceType) || su.isInTransaction() ) { it.remove(); } } L.info("Selected {} Units for the Lists", uusus.size()); m.worked(1, "sortiere und bereite Geräte vor"); Map<Product, Set<UniqueUnit>> stackedUnits = new HashMap<>(); for (Map.Entry<UniqueUnit, StockUnit> entry : uusus.entrySet()) { Product p = entry.getKey().getProduct(); if ( !stackedUnits.containsKey(p) ) stackedUnits.put(p, new HashSet<>()); stackedUnits.get(p).add(entry.getKey()); } List<StackedLine> stackedLines = new ArrayList<>(stackedUnits.size()); DecimalFormat df = (DecimalFormat)DecimalFormat.getInstance(Locale.GERMAN); df.applyPattern("#,###,##0.00"); for (Map.Entry<Product, Set<UniqueUnit>> entry : stackedUnits.entrySet()) { Product p = entry.getKey(); StackedLine line = new StackedLine(); line.setBrand(p.getTradeName()); line.setGroup(p.getGroup()); line.setCommodityGroupName(p.getGroup().getNote()); line.setDescription(p.getDescription()); line.setManufacturerName(p.getTradeName().getName()); line.setManufacturerPartNo(p.getPartNo()); line.setName(p.getName()); line.setImageUrl(imageFinder.findImageUrl(p.getImageId())); boolean priceChanged = false; double customerPrice = 0; for (UniqueUnit uu : entry.getValue()) { StackedLineUnit elem = new StackedLineUnit(); elem.setAccessories(UniqueUnitFormater.toSingleLineAccessories(uu)); elem.setComment(UniqueUnitFormater.toSingleLineComment(uu)); elem.setConditionLevelDescription(uu.getCondition().getNote()); elem.setMfgDate(uu.getMfgDate()); elem.setRefurbishedId(uu.getRefurbishId()); elem.setSerial(uu.getSerial()); elem.setWarranty(uu.getWarranty().getName()); if ( uu.getWarranty().equals(Warranty.WARRANTY_TILL_DATE) ) elem.setWarrentyTill(uu.getWarrentyValid()); double uuPrice = uu.getPrice(priceType); elem.setCustomerPrice(uuPrice); elem.setRoundedTaxedCustomerPrice(MathUtil.roundedApply(uuPrice, GlobalConfig.TAX, 0.02)); // For the "ab € XXX" handler if ( customerPrice == 0 ) { customerPrice = uuPrice; } else if ( customerPrice > uuPrice ) { customerPrice = uuPrice; priceChanged = true; } else if ( customerPrice < uuPrice ) { priceChanged = true; } elem.normaize(); line.add(elem); } line.setAmount(line.getUnits().size()); line.setCustomerPriceLabel((priceChanged ? "ab €" : "€") + df.format(MathUtil.roundedApply(customerPrice, GlobalConfig.TAX, 0.02))); line.normaize(); stackedLines.add(line); } L.info("Created {} Lines for the Lists", stackedLines.size()); m.worked(1, "erzeuge listen"); Set<ListingConfiguration> configs = new HashSet<>(); if ( listingService.isAmbiguous() || listingService.isUnsatisfied() ) { for (TradeName brand : TradeName.values()) { for (ProductGroup value : ProductGroup.values()) { configs.add(ListingConfiguration.builder() .filePrefix("Geräteliste ") .name(brand.getName() + " " + value.getName()) .brand(brand) .groups(EnumSet.of(value)) .headLeft("Beispieltext Links\nZeile 2") .headCenter("Beispieltext Mitte\nZeile 2") .headRight("Beispieltext Rechts\nZeile 2") .footer("Fusszeilentext") .build()); } } } else { configs.addAll(listingService.get().listingConfigurations()); } m.setWorkRemaining(configs.size() + 1); Map<TradeName, Collection<FileJacket>> jackets = new HashMap<>(); for (ListingConfiguration config : configs) { m.worked(1, "erstelle Liste " + config.getName()); if ( StringUtils.isBlank(config.getJasperTemplateFile()) ) config.setJasperTemplateFile(compileReportToTempFile("CustomerSalesListing")); if ( StringUtils.isBlank(config.getJasperTempleteUnitsFile()) ) config.setJasperTempleteUnitsFile(compileReportToTempFile("CustomerSalesListingUnits")); FileJacket fj = createListing(config, stackedLines); if ( fj != null ) { if ( !jackets.containsKey(config.getBrand()) ) jackets.put(config.getBrand(), new HashSet<>()); jackets.get(config.getBrand()).add(fj); } } m.finish(); return jackets; } /** * Create a filejacket from a collection of lines that are filtered by configuration parameters. * Lines are filtered by brand and group. * <p> * @param config configuration for filtering and file creation * @param all lines to be considered * @return a filejacket from a collection of lines that are filtered by configuration parameters. */ private FileJacket createListing(ListingConfiguration config, Collection<StackedLine> all) { try { SortedSet<StackedLine> filtered = all.stream() .filter(line -> (config.getAllBrands().contains(line.getBrand()) && config.getGroups().contains(line.getGroup()))) .collect(Collectors.toCollection(TreeSet::new)); if ( filtered.isEmpty() ) return null; L.info("Creating listing {} with {} lines", config.getName(), filtered.size()); JRDataSource datasource = new JRBeanCollectionDataSource(filtered); JasperPrint jasperPrint = JasperFillManager.fillReport(config.getJasperTemplateFile(), config.toReportParamters(), datasource); byte[] pdfContend = JasperExportManager.exportReportToPdf(jasperPrint); return new FileJacket(config.getFilePrefix() + config.getName(), ".pdf", pdfContend); } catch (JRException ex) { throw new RuntimeException(ex); } } /** * Prepare and send filejackets to the specified email address. * <p> * @param fileJackets files to be send */ private void prepareAndSend(List<FileJacket> fileJackets) { SubMonitor m = monitorFactory.newSubMonitor("Transfer"); m.message("sending Mail"); m.start(); try { ListingMailConfiguration config = listingService.get().listingMailConfiguration(); MultiPartEmail email = mandator.prepareDirectMail(); email.setFrom(config.getFromAddress()); email.addTo(config.getToAddress()); email.setSubject(config.getSubject()); email.setMsg(config.toMessage()); for (FileJacket fj : fileJackets) { email.attach( new javax.mail.util.ByteArrayDataSource(fj.getContent(), "application/xls"), fj.getHead() + fj.getSuffix(), "Die Händlerliste für die Marke "); } email.send(); m.finish(); } catch (EmailException e) { throw new RuntimeException(e); } } /** * Prepares and uploads filejackets to a specified location. * <p> * @param files files to be uploaded * @throws UserInfoException */ private void prepareAndUpload(Map<TradeName, Collection<FileJacket>> files) throws UserInfoException { FtpConfiguration ftpConfig = listingService.get().listingFtpConfiguration(files); try { FtpTransfer.upload(ftpConfig.getConfig(), monitorFactory.newSubMonitor("Bereite FTP tranfer vor"), ftpConfig.getUpdloadCommands().toArray(new UploadCommand[0])); } catch (IOException ex) { throw new UserInfoException("Fileupload konnte nicht durchgeführt werden.", "Fileupload configuration = " + ftpConfig); } L.info("UploadCommands = {}", ftpConfig.getUpdloadCommands()); } }