OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

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);
    }

}