Dépôt officiel du code source de l'ERP OpenConcerto
Blame | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.erp.core.supplychain.stock.report;
import org.openconcerto.erp.core.finance.accounting.report.GrandLivrePDF;
import org.openconcerto.sql.model.DBRoot;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLRowListRSH;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.Where;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.Tuple2;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
public class StockReportPDF {
private static final int LINE_HEIGHT = 10;
private static final float COL_1_SIZE = 80;
private static final float COL_2_SIZE = 300;
private static final float COL_3_SIZE = 30;
private static final float COL_4_SIZE = 40;
private static final float COL_5_SIZE = 50;
private static final float COL_1_X = 50;
private static final float COL_2_X = COL_1_X + COL_1_SIZE;
private static final float COL_3_X = COL_2_X + COL_2_SIZE;
private static final float COL_4_X = COL_3_X + COL_3_SIZE;
private static final float COL_5_X = COL_4_X + COL_4_SIZE;
private final PDDocument doc;
private final List<SQLRow> etatsStock = new ArrayList<>();
private final String companyName;
private final String title;
private int y = 0;
private PDPageContentStream content;
private final DecimalFormat decimalFormat = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(Locale.FRANCE));
private final DecimalFormat decimalFormatQuantity = new DecimalFormat("#,##0.##", DecimalFormatSymbols.getInstance(Locale.FRANCE));
private Map<SQLRow, BigDecimal> totaux = new HashMap<>();
public StockReportPDF(String companyName, String title) throws IOException {
this.companyName = companyName;
this.title = title;
this.doc = new PDDocument();
final PDPage page = new PDPage(PDRectangle.A4);
this.doc.addPage(page);
this.content = new PDPageContentStream(this.doc, page);
this.y = drawHeader(true, true);
}
private int drawHeader(boolean full, boolean columns) throws IOException {
this.y = 795;
this.content.beginText();
this.content.setFont(PDType1Font.HELVETICA_BOLD, 14);
this.content.newLineAtOffset(40, this.y);
this.content.showText("INVENTAIRE " + StringUtils.cleanPDFString(this.companyName.toUpperCase()));
this.content.endText();
this.y -= 17;
if (full) {
this.content.beginText();
this.content.setFont(PDType1Font.HELVETICA, 12);
this.content.newLineAtOffset(40, this.y);
this.content.showText(StringUtils.cleanPDFString(this.title));
this.content.endText();
this.y -= 20;
}
this.content.setFont(PDType1Font.HELVETICA_BOLD, 7);
if (columns) {
this.content.beginText();
this.content.newLineAtOffset(COL_1_X + 5, this.y);
this.content.showText("CODE");
this.content.endText();
this.content.beginText();
this.content.newLineAtOffset(COL_2_X, this.y);
this.content.showText("ARTICLE");
this.content.endText();
drawRightAlign(this.content, COL_3_X, this.y, COL_3_SIZE, "QUANTITE");
drawRightAlign(this.content, COL_4_X, this.y, COL_4_SIZE, "PRIX U.");
drawRightAlign(this.content, COL_5_X, this.y, COL_5_SIZE, "TOTAL");
}
this.y -= 4;
this.content.setStrokingColor(Color.BLACK);
this.content.setLineWidth(1f);
this.content.moveTo(COL_1_X - 4, this.y);
this.content.lineTo(COL_5_X + COL_5_SIZE + 4, this.y);
this.content.stroke();
return this.y;
}
private static void drawRightAlign(PDPageContentStream content, float x, float y, float width, String text) throws IOException {
content.beginText();
final float w = PDType1Font.HELVETICA.getStringWidth(StringUtils.cleanPDFString(text)) / 1000.0f * 7f;
content.newLineAtOffset(x + width - w, y);
content.showText(StringUtils.cleanPDFString(text));
content.endText();
}
private void drawFamilleName(String name) throws IOException {
this.y -= LINE_HEIGHT;
this.content.setFont(PDType1Font.HELVETICA_BOLD, 7);
this.content.beginText();
this.content.newLineAtOffset(COL_1_X, this.y);
this.content.showText(StringUtils.cleanPDFString(name));
this.content.endText();
}
/**
*
* @param root
* @param mapDeclinaison map de ID_ARTICLE_DECLINAISON_... vers Tuple2<Nom, Ordre>
*/
private void fillDeclinaisons(DBRoot root, Map<String, Map<Integer, Tuple2<String, BigDecimal>>> mapDeclinaisons) {
List<String> tablesDeclinaisons = new ArrayList<>();
List<String> declinaisonsFieldNames = new ArrayList<>();
SQLTable tableArticle = root.getTable("ARTICLE");
for (SQLField f : tableArticle.getFields()) {
if (f.getName().startsWith("ID_ARTICLE_DECLINAISON_")) {
declinaisonsFieldNames.add(f.getName());
tablesDeclinaisons.add(f.getName().substring("ID_".length()));
}
}
for (String table : tablesDeclinaisons) {
SQLTable t = tableArticle.getTable(table);
final SQLSelect selDecl = new SQLSelect();
selDecl.addSelect(t.getKey());
selDecl.addSelect(t.getField("NOM"));
selDecl.addSelect(t.getField("ORDRE"));
Map<Integer, Tuple2<String, BigDecimal>> m = new HashMap<>();
mapDeclinaisons.put("ID_" + table, m);
for (SQLRow row : SQLRowListRSH.execute(selDecl)) {
m.put(row.getID(), Tuple2.create(row.getString("NOM"), row.getBigDecimal("ORDRE")));
}
}
}
private void drawLine(String code, String article, BigDecimal qte, BigDecimal prixUnitaire, BigDecimal total) throws IOException {
String s = StringUtils.splitString(article, 70);
final List<String> parts = StringUtils.fastSplitTrimmed(s, '\n');
this.y -= LINE_HEIGHT;
if (this.y - LINE_HEIGHT * parts.size() < 50) {
final PDPage page = new PDPage(PDRectangle.A4);
this.doc.addPage(page);
if (this.content != null) {
this.content.close();
}
this.content = new PDPageContentStream(this.doc, page);
this.y = drawHeader(false, true);
this.y -= LINE_HEIGHT;
this.y -= LINE_HEIGHT;
}
this.content.setFont(PDType1Font.HELVETICA, 7);
if (code != null) {
this.content.beginText();
this.content.newLineAtOffset(COL_1_X, this.y);
this.content.showText(StringUtils.cleanPDFString(code));
this.content.endText();
}
drawRightAlign(this.content, COL_3_X, this.y, COL_3_SIZE, this.decimalFormatQuantity.format(qte));
drawRightAlign(this.content, COL_4_X, this.y, COL_4_SIZE, this.decimalFormat.format(prixUnitaire));
drawRightAlign(this.content, COL_5_X, this.y, COL_5_SIZE, this.decimalFormat.format(total));
for (String part : parts) {
this.content.beginText();
this.content.newLineAtOffset(COL_2_X, this.y);
this.content.showText(StringUtils.cleanPDFString(part));
this.content.endText();
this.y -= LINE_HEIGHT;
}
this.y += LINE_HEIGHT;
this.content.setLineWidth(0.5f);
this.content.setStrokingColor(Color.LIGHT_GRAY);
this.content.moveTo(COL_1_X - 4, this.y - 2f);
this.content.lineTo(COL_5_X + 4 + COL_5_SIZE, this.y - 2f);
this.content.stroke();
}
public void addStockDepot(SQLRow rowEtatStock) throws IOException {
this.etatsStock.add(rowEtatStock);
final DBRoot root = rowEtatStock.getTable().getDBRoot();
// Nouvelle page si besoin
if (this.etatsStock.size() > 1) {
final PDPage page = new PDPage(PDRectangle.A4);
this.doc.addPage(page);
if (this.content != null) {
this.content.close();
}
this.content = new PDPageContentStream(this.doc, page);
this.y = drawHeader(false, true);
}
final SQLTable tDepot = root.getTable("DEPOT_STOCK");
final SQLTable tFamilleArtice = root.getTable("FAMILLE_ARTICLE");
final SQLRow rDepot = tDepot.getRow(rowEtatStock.getInt("ID_DEPOT_STOCK"));
this.y -= LINE_HEIGHT * 2;
// Nom du déport
this.content.setFont(PDType1Font.HELVETICA, 10);
this.content.beginText();
this.content.newLineAtOffset(COL_1_X, this.y);
this.content.showText(StringUtils.cleanPDFString("DÉPÔT : " + rDepot.getString("NOM")));
this.content.endText();
this.y -= LINE_HEIGHT;
// Depot Element
final SQLTable tEtatStockElement = root.getTable("ETAT_STOCK_ELEMENT");
final SQLRowValues rElements = new SQLRowValues(tEtatStockElement);
final SQLRowValues putRowValues = rElements.putRowValues("ID_ARTICLE");
putRowValues.putNulls("ID_FAMILLE_ARTICLE");
for (SQLField field : putRowValues.getTable().getContentFields()) {
if (field.getName().startsWith("ID_ARTICLE_DECLINAISON_")) {
putRowValues.put(field.getName(), null);
}
}
rElements.putNulls("PA", "T_PA", "QTE", "CODE", "NOM");
final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(rElements);
final List<SQLRowValues> elements = fetcher
.fetch(new Where(tEtatStockElement.getField("ID_ETAT_STOCK"), "=", rowEtatStock.getID()).and(new Where(tEtatStockElement.getField("QTE"), ">", BigDecimal.ZERO)));
Collections.sort(elements, new Comparator<SQLRowValues>() {
@Override
public int compare(SQLRowValues o1, SQLRowValues o2) {
final BigDecimal qte1 = o1.getBigDecimal("PA");
final BigDecimal qte2 = o2.getBigDecimal("PA");
if (qte1.signum() == 0 && qte2.signum() == 0 || qte1.signum() != 0 && qte2.signum() != 0) {
return o1.getString("NOM").compareTo(o2.getString("NOM"));
} else if (qte1.signum() == 0) {
return 1;
} else {
return -1;
}
}
});
final Map<Number, List<SQLRowValues>> mapFamilleElement = new HashMap<>();
for (final SQLRowValues e : elements) {
final Number idFamille = e.getForeign("ID_ARTICLE").getForeignIDNumber("ID_FAMILLE_ARTICLE");
List<SQLRowValues> l = mapFamilleElement.get(idFamille);
if (l == null) {
l = new ArrayList<>();
mapFamilleElement.put(idFamille, l);
}
l.add(e);
}
// On recupere tous les familles
final SQLRowValues rFamille = new SQLRowValues(root.getTable("FAMILLE_ARTICLE"));
rFamille.putNulls("NOM", "ID_FAMILLE_ARTICLE_PERE");
List<SQLRowValues> familles = SQLRowValuesListFetcher.create(rFamille).fetch();
System.out.println("StockReportPDF.addStockDepot()" + familles.size() + " familles");
SQLRowValuesTree tree = new SQLRowValuesTree(familles, "ID_FAMILLE_ARTICLE_PERE");
tree.dump(System.out);
// On récupère les declinaisons
Map<String, Map<Integer, Tuple2<String, BigDecimal>>> mapDeclinaison = new HashMap<>();
fillDeclinaisons(tDepot.getDBRoot(), mapDeclinaison);
//// On groupe par Famille
// D'abord les articles sans familles
BigDecimal t = BigDecimal.ZERO;
for (final SQLRowValues e : elements) {
final Number idFamille = e.getForeign("ID_ARTICLE").getForeignIDNumber("ID_FAMILLE_ARTICLE");
if (idFamille.intValue() == tFamilleArtice.getUndefinedID()) {
drawEtatStockElement(e, mapDeclinaison);
t = t.add(e.getBigDecimal("T_PA"));
}
}
this.totaux.put(rowEtatStock, t);
// Les familles selons l'arbre
List<SQLRowValuesTreeNode> roots = tree.getChildren();
final Comparator<SQLRowValuesTreeNode> comparatorFamille = new Comparator<SQLRowValuesTreeNode>() {
@Override
public int compare(SQLRowValuesTreeNode o1, SQLRowValuesTreeNode o2) {
return o1.getSQLRowValues().getString("NOM").compareToIgnoreCase(o2.getSQLRowValues().getString("NOM"));
}
};
Collections.sort(roots, comparatorFamille);
for (SQLRowValuesTreeNode f : roots) {
drawFamilleNode(mapFamilleElement, f, mapDeclinaison);
}
}
private void drawFamilleNode(Map<Number, List<SQLRowValues>> mapFamilleElement, SQLRowValuesTreeNode f, Map<String, Map<Integer, Tuple2<String, BigDecimal>>> mapDeclinaison) throws IOException {
if (f.getChildrenCount() > 0) {
drawFamilleName(f.getSQLRowValues().getString("NOM").toUpperCase());
}
// EtatStockElement
List<SQLRowValues> list = mapFamilleElement.get(f.getSQLRowValues().getIDNumber());
if (list != null) {
for (SQLRowValues e : list) {
drawEtatStockElement(e, mapDeclinaison);
}
}
// Sous familles
for (SQLRowValuesTreeNode n : f.getChildren()) {
drawFamilleNode(mapFamilleElement, n, mapDeclinaison);
}
}
private void drawEtatStockElement(SQLRowValues e, Map<String, Map<Integer, Tuple2<String, BigDecimal>>> mapDeclinaison) throws IOException {
String code = e.getString("CODE");
String article = e.getString("NOM");
StringBuilder detailsDeclinaison = new StringBuilder();
final SQLRowAccessor nonEmptyForeignArticle = e.getNonEmptyForeign("ID_ARTICLE");
if (nonEmptyForeignArticle != null) {
for (String fieldDecl : mapDeclinaison.keySet()) {
if (nonEmptyForeignArticle.getObject(fieldDecl) != null) {
final Number nonEmptyForeignDecl = nonEmptyForeignArticle.getNonEmptyForeignIDNumber(fieldDecl);
if (nonEmptyForeignDecl != null) {
final String declinaisonName = mapDeclinaison.get(fieldDecl).get(nonEmptyForeignDecl).get0();
if (declinaisonName.trim().length() > 0) {
detailsDeclinaison.append(fieldDecl.substring("ID_ARTICLE_DECLINAISON_".length()));
detailsDeclinaison.append(" : ");
detailsDeclinaison.append(declinaisonName);
detailsDeclinaison.append(" ");
}
}
}
}
}
String detailsDeclString = detailsDeclinaison.toString().trim();
if (detailsDeclString.length() > 0) {
article += "\n" + detailsDeclString;
}
BigDecimal qte = e.getBigDecimal("QTE");
BigDecimal prixUnitaire = e.getBigDecimal("PA");
BigDecimal total = e.getBigDecimal("T_PA");
drawLine(code, article, qte, prixUnitaire, total);
}
public void savePDF(File f) throws IOException {
final PDPage page = new PDPage(PDRectangle.A4);
this.doc.addPage(page);
if (this.content != null) {
this.content.close();
}
this.content = new PDPageContentStream(this.doc, page);
// Totaux
drawHeader(false, false);
this.y -= LINE_HEIGHT;
this.content.setFont(PDType1Font.HELVETICA, 10);
for (SQLRow r : this.etatsStock) {
BigDecimal total = this.totaux.get(r);
final SQLTable tDepot = r.getTable().getTable("DEPOT_STOCK");
final SQLRow rDepot = tDepot.getRow(r.getInt("ID_DEPOT_STOCK"));
this.y -= LINE_HEIGHT * 2;
// Nom du déport
this.content.setFont(PDType1Font.HELVETICA, 10);
this.content.beginText();
this.content.newLineAtOffset(COL_1_X, this.y);
this.content.showText(StringUtils.cleanPDFString("TOTAL DÉPÔT : " + rDepot.getString("NOM") + " : " + this.decimalFormat.format(total) + " EUR"));
this.content.endText();
}
this.content.close();
// Header avec numéro de page
final InputStream sImage = GrandLivrePDF.class.getResourceAsStream("OpenConcerto_2000px.png");
final ByteArrayOutputStream bOut = new ByteArrayOutputStream();
final byte[] buf = new byte[8192];
int length;
while ((length = sImage.read(buf)) > 0) {
bOut.write(buf, 0, length);
}
sImage.close();
bOut.close();
final float ratio = 20;
final int pageCount = this.doc.getNumberOfPages();
for (int index = 0; index < pageCount; index++) {
final PDPage currentPage = this.doc.getPage(index);
this.content = new PDPageContentStream(this.doc, currentPage, PDPageContentStream.AppendMode.APPEND, true, true);
if (index == 0 || index == pageCount - 1) {
// Logo sur 1ere page
final PDImageXObject pdImage = PDImageXObject.createFromByteArray(this.doc, bOut.toByteArray(), "openconcerto.png");
final float h = pdImage.getHeight() / ratio;
this.content.drawImage(pdImage, 40, 10, pdImage.getWidth() / ratio, h);
}
this.content.beginText();
this.content.setFont(PDType1Font.HELVETICA, 10);
this.content.newLineAtOffset(500, 20);
this.content.showText("Page " + (index + 1) + " / " + pageCount);
this.content.endText();
this.content.close();
}
this.doc.save(f);
}
}