OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | Blame | Compare with Previous | 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.sales.pos.model;

import org.openconcerto.erp.config.ComptaPropsConfiguration;
import org.openconcerto.erp.core.common.ui.TotalCalculator;
import org.openconcerto.erp.core.customerrelationship.customer.element.CompteClientTransactionSQLELement;
import org.openconcerto.erp.core.finance.accounting.element.ComptePCESQLElement;
import org.openconcerto.erp.core.finance.accounting.element.JournalSQLElement;
import org.openconcerto.erp.core.finance.tax.model.TaxeCache;
import org.openconcerto.erp.core.sales.pos.POSConfiguration;
import org.openconcerto.erp.core.sales.pos.io.DefaultTicketPrinter;
import org.openconcerto.erp.core.sales.pos.io.Printable;
import org.openconcerto.erp.core.sales.pos.io.TicketPrinter;
import org.openconcerto.erp.core.sales.pos.model.RegisterFiles.HashMode;
import org.openconcerto.erp.core.sales.pos.ui.TicketCellRenderer;
import org.openconcerto.erp.generationEcritures.GenerationEcritures;
import org.openconcerto.erp.generationEcritures.GenerationMvtVirement;
import org.openconcerto.erp.preferences.DefaultNXProps;
import org.openconcerto.sql.Configuration;
import org.openconcerto.sql.element.SQLElementDirectory;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowAccessor;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.utils.DecimalUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.XMLDateFormat;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;

public class Ticket implements Printable {

    private static final XMLDateFormat DATE_FMT = new XMLDateFormat();

    // Propre a ticket
    private final List<Paiement> paiements = new ArrayList<>();
    private final List<TicketItem> items = new ArrayList<>();
    private Calendar creationCal;
    private Client client = Client.NONE;
    private final int number;
    private final String previousHash;
    private boolean additionnalCopyRequested = false;
    private String codePostal = "";
    // Propre à la caisse
    private final int caisseNumber;

    private static final SQLTable tableArticle = Configuration.getInstance().getRoot().findTable("ARTICLE");

    public static Ticket getTicketFromCode(final String code, final RegisterFiles registerFiles) {
        try {
            final ReceiptCode receiptCode = new ReceiptCode(code);
            final Path receiptFile = registerFiles.getReceiptFile(receiptCode);
            if (receiptFile != null && Files.exists(receiptFile)) {
                return parseFile(receiptFile.toFile());
            } else {
                // old location
                return parseFile(receiptCode.getFile());
            }
        } catch (final Exception e) {
            return null;
        }
    }

    public String getCodePostal() {
        return this.codePostal;
    }

    public void setCodePostal(String codePostal) {
        this.codePostal = codePostal;

    }

    public void setClient(final Client client) {
        this.client = client;
    }

    public Client getClient() {
        return this.client;
    }

    public static Ticket parseFile(final File file) throws IOException {
        return parseFile(file, HashMode.REQUIRED);
    }

    public static Ticket parseFile(final File file, final HashMode hashMode) throws IOException {
        try {
            // XML Reading

            final Document document = RegisterFiles.parse(file.toPath(), hashMode);
            final Element root = document.getRootElement();
            final ReceiptCode receiptCode = new ReceiptCode(root.getAttributeValue("code"));
            final String creationDateAttr = root.getAttributeValue("creationDate");
            final Calendar c = (Calendar) receiptCode.getDay().clone();
            if (creationDateAttr == null) {
                final String h = root.getAttributeValue("hour");
                final String m = root.getAttributeValue("minute");
                c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(h));
                c.set(Calendar.MINUTE, Integer.parseInt(m));
            } else {
                c.setTime((Date) DATE_FMT.parseObject(creationDateAttr));
            }
            final String client = root.getAttributeValue("clientID", "1");
            final Ticket t = new Ticket(receiptCode, c, root.getAttributeValue("previousHash"));
            t.setClient(new Client(Integer.parseInt(client), "", BigDecimal.ZERO));

            t.setCodePostal(root.getAttributeValue("codePostal", ""));

            // article
            final List<Element> children = root.getChildren("article");
            for (final Element element : children) {
                final BigDecimal qte = new BigDecimal(element.getAttributeValue("qte"));
                final BigDecimal prix_unitaire_cents_ht = new BigDecimal(element.getAttributeValue("prixHT"));
                final int idTaxe = Integer.parseInt(element.getAttributeValue("idTaxe"));
                final BigDecimal prix_unitaire_cents = new BigDecimal(element.getAttributeValue("prix"));
                final String categorie = element.getAttributeValue("categorie");
                final String name = element.getValue();
                final String codebarre = element.getAttributeValue("codebarre");
                final String codeArt = element.getAttributeValue("code");
                final String salesUnit = element.getAttributeValue("unit");
                final String declinaison = element.getAttributeValue("declinaison");
                final Categorie cat = new Categorie(categorie);

                final BigDecimal discount_pct = new BigDecimal(element.getAttributeValue("discount_pct", "0.00"));
                final BigDecimal ecotaxe = new BigDecimal(element.getAttributeValue("ecotaxe", "0.00"));

                final String valueID = element.getAttributeValue("id");

                final int id = valueID == null || valueID.trim().length() == 0 ? tableArticle.getUndefinedID() : Integer.parseInt(valueID);
                final Article art = new Article(cat, name, id);
                art.setPriceWithTax(prix_unitaire_cents);
                art.setCode(codeArt);
                art.setPriceWithoutTax(prix_unitaire_cents_ht);
                art.setIdTaxe(idTaxe);
                art.setBarCode(codebarre);
                art.setSalesUnit(salesUnit);
                art.setDeclinaison(declinaison);

                art.setDiscountPct(discount_pct);
                art.setEcoTaxe(ecotaxe);

                final TicketItem line = new TicketItem(art, qte);
                t.items.add(line);

            }
            // paiement
            final List<Element> payChildren = root.getChildren("paiement");
            for (final Element element : payChildren) {

                final String type = element.getAttributeValue("type");
                final int montant_cents = Integer.parseInt(element.getAttributeValue("montant"));
                if (montant_cents != 0) {
                    int tp = Paiement.ESPECES;
                    if (type.equals("CB")) {
                        tp = Paiement.CB;
                    } else if (type.equals("CHEQUE")) {
                        tp = Paiement.CHEQUE;
                    } else if (type.equals("ESPECES")) {
                        tp = Paiement.ESPECES;
                    } else if (type.equals("SOLDE")) {
                        tp = Paiement.SOLDE;
                    }
                    final Paiement p = new Paiement(tp);
                    p.setMontantInCents(montant_cents);
                    t.paiements.add(p);
                }
            }

            return t;
        } catch (IOException e) {
            throw e;
        } catch (Exception e) {
            throw new IOException("Couldn't parse " + file, e);
        }
    }

    // TODO receiptCode should be immutable and the day part should be the current
    // accounting day
    // not the day of the present time. E.g. for work day beginning at 15:00, even
    // at 3:00 the
    // receipt code should contain the previous day.
    // The creationCal should only be set once the ticket is done (i.e. no further
    // modifications
    // should be allowed)

    // create new ticket, i.e. null creationDate since it's not done
    public Ticket(final int caisse, final int number, final String previousHash) {
        this(new ReceiptCode(caisse, Calendar.getInstance(), number), null, previousHash);
    }

    // re-create existing ticket
    public Ticket(final ReceiptCode code, final Calendar creationDate, final String previousHash) {
        this.caisseNumber = code.getCaisseNb();
        this.number = code.getDayIndex();
        this.previousHash = previousHash;
        this.setCreationCal(creationDate);
    }

    public final String getPreviousHash() {
        return this.previousHash;
    }

    public void clear() {
        this.client = Client.NONE;
        this.codePostal = "";
        this.paiements.clear();

    }

    public final ReceiptCode getReceiptCode() {
        // TODO replace our fields by one ReceiptCode
        return new ReceiptCode(this.getCaisseNumber(), this.getCreationCal(), this.getNumber());
    }

    public String getCode() {
        return getReceiptCode().getCode();
    }

    /**
     * Numero du ticket fait ce jour, compteur remis à 1 chaque jour
     * 
     * @return the index of this ticket, inside its day.
     */
    public int getNumber() {
        return this.number;
    }

    /**
     * Numero de la caisse, de 1 à n
     */
    final int getCaisseNumber() {
        return this.caisseNumber;
    }

    public String save(final RegisterFiles files, final SQLElementDirectory dir) throws IOException, SQLException {
        final String fileHash = files.save(this);
        // FIXME handle failure
        this.handleSolde(dir);
        return fileHash;
    }

    final byte[] saveToFile(final Path f) throws IOException {
        final Calendar c = getCreationCal();

        final Element topLevel = new Element("ticket");
        topLevel.setAttribute(new Attribute("code", this.getCode()));
        if (this.getPreviousHash() != null)
            topLevel.setAttribute("previousHash", this.getPreviousHash());
        topLevel.setAttribute("creationDate", DATE_FMT.format(c.getTime()));
        topLevel.setAttribute("clientID", String.valueOf(this.client.getId()));
        topLevel.setAttribute("codePostal", this.codePostal);
        // Articles
        for (final TicketItem item : this.items) {
            final Element e = new Element("article");
            e.setAttribute("qte", String.valueOf(item.getQty()));
            // Prix unitaire
            final Article article = item.getArticle();

            e.setAttribute("prix", String.valueOf(article.getPriceWithTax(item.getQty(), false)));
            e.setAttribute("prixHT", String.valueOf(article.getPriceWithoutTax(item.getQty(), false)));

            e.setAttribute("idTaxe", String.valueOf(article.getIdTaxe()));
            e.setAttribute("categorie", article.getCategorie().getName());
            e.setAttribute("codebarre", article.getBarCode());
            e.setAttribute("code", article.getCode());
            e.setAttribute("id", String.valueOf(article.getId()));
            e.setAttribute("ecotaxe", String.valueOf(article.getEcoTaxe()));
            e.setAttribute("discount_pct", String.valueOf(article.getDiscountPct()));
            if (article.getSalesUnit() != null) {
                e.setAttribute("unit", article.getSalesUnit());
            }
            if (article.getDeclinaison() != null) {
                e.setAttribute("declinaison", article.getDeclinaison());
            }
            e.setText(article.getName());
            topLevel.addContent(e);
        }
        // Paiements
        for (final Paiement paiement : this.paiements) {
            final int montantInCents = paiement.getMontantInCents();
            if (montantInCents != 0) {
                final Element e = new Element("paiement");
                String type = "";
                if (paiement.getType() == Paiement.CB) {
                    type = "CB";
                } else if (paiement.getType() == Paiement.CHEQUE) {
                    type = "CHEQUE";
                } else if (paiement.getType() == Paiement.ESPECES) {
                    type = "ESPECES";
                } else if (paiement.getType() == Paiement.SOLDE) {
                    type = "SOLDE";
                }
                e.setAttribute("type", type);
                e.setAttribute("montant", String.valueOf(montantInCents));
                topLevel.addContent(e);
            }
        }
        return RegisterFiles.save(new Document(topLevel), f);
    }

    private final void handleSolde(final SQLElementDirectory dir) throws SQLException {
        final SQLTable table = dir.getElement(CompteClientTransactionSQLELement.class).getTable();
        final SQLTable tablePrefCompte = table.getDBRoot().findTable("PREFS_COMPTE");
        final SQLRow rowPrefsCompte = tablePrefCompte.getRow(2);
        final Calendar c = getCreationCal();
        SQLUtils.executeAtomic(table.getDBSystemRoot().getDataSource(), new SQLUtils.SQLFactory<Object>() {
            @Override
            public Object create() throws SQLException {
                for (final Paiement paiement : Ticket.this.paiements) {
                    final int montantInCents = paiement.getMontantInCents();
                    if (montantInCents > 0 && paiement.getType() == Paiement.SOLDE) {
                        final SQLRowValues rowValsTransact = new SQLRowValues(table);
                        rowValsTransact.put("ID_CLIENT", Ticket.this.client.getId());

                        rowValsTransact.put("DATE", c.getTime());
                        final BigDecimal amountTransaction = new BigDecimal(montantInCents).movePointLeft(2);
                        rowValsTransact.put("MONTANT", amountTransaction.negate());

                        SQLRow rowTransact = rowValsTransact.commit();
                        final GenerationEcritures ecr = new GenerationEcritures();
                        final int idMvt = ecr.getNewMouvement(table.getName(), rowTransact.getID(), 1, "Transact. " + Ticket.this.client.getFullName() + " Ticket " + getCode());
                        rowTransact = rowTransact.createEmptyUpdateRow().put("ID_MOUVEMENT", idMvt).commit();

                        // mise à jour du solde
                        final SQLTable tableClient = table.getForeignTable("ID_CLIENT");
                        final SQLRow row = tableClient.getRow(Ticket.this.client.getId());
                        final BigDecimal solde = row.getBigDecimal("SOLDE_COMPTE");
                        final BigDecimal nouveauSolde = solde.subtract(amountTransaction);
                        row.createEmptyUpdateRow().put("SOLDE_COMPTE", nouveauSolde).commit();

                        // Créeation des réglements et écritures

                        int idCompteAvanceClient = rowPrefsCompte.getInt("ID_COMPTE_PCE_AVANCE_CLIENT");
                        int idCompteClient = row.getInt("ID_COMPTE_PCE");
                        if (idCompteAvanceClient <= 1) {
                            idCompteAvanceClient = ComptePCESQLElement.getIdComptePceDefault("AvanceClients");
                        }

                        // compte Clients
                        if (idCompteClient <= 1) {
                            idCompteClient = rowPrefsCompte.getInt("ID_COMPTE_PCE_CLIENT");
                            if (idCompteClient <= 1) {
                                idCompteClient = ComptePCESQLElement.getIdComptePceDefault("Clients");
                            }
                        }
                        new GenerationMvtVirement(idCompteAvanceClient, idCompteClient, 0, montantInCents, "Utilisation compte client", c.getTime(), JournalSQLElement.VENTES, "Ticket N°" + getCode())
                                .genereMouvement();
                    }
                }
                return null;
            }
        });
    }

    @Override
    public void print(final TicketPrinter prt, final int ticketWidth) {
        final int maxWidth = ticketWidth;
        final int MAX_PRICE_WIDTH = 8;
        final int MAX_QTE_WIDTH = 5;
        prt.clearBuffer("receipt " + this.getCode());
        final List<TicketLine> headers = POSConfiguration.getInstance().getHeaderLines();
        for (final TicketLine line : headers) {
            prt.addToBuffer(line);
        }

        // Date
        prt.addToBuffer("");
        final SimpleDateFormat df = new SimpleDateFormat("EEEE d MMMM yyyy à HH:mm", Locale.FRENCH);
        prt.addToBuffer(DefaultTicketPrinter.formatCenter(maxWidth, "Le " + df.format(getCreationDate())));
        prt.addToBuffer("");
        List<TicketItem> itemsToPrint = new ArrayList<>(this.items);
        Collections.sort(itemsToPrint, new Comparator<TicketItem>() {

            @Override
            public int compare(TicketItem o1, TicketItem o2) {
                final Article p1 = o1.getArticle();
                final Article p2 = o2.getArticle();
                final Categorie c1 = p1.getCategorie();
                final Categorie c2 = p2.getCategorie();
                if (c1.equals(c2)) {
                    return p1.getName().compareTo(p2.getName());
                }
                // Unknown first
                if (c1.isUnknown()) {
                    return -1;
                }
                if (c2.isUnknown()) {
                    return 1;
                }
                // Sort by name
                return c1.getName().compareTo(c2.getName());
            }
        });
        Categorie currentCategorie = null;
        for (final TicketItem item : this.items) {
            final Article article = item.getArticle();
            System.err.println("Ticket.print()" + item);
            if (currentCategorie == null || !currentCategorie.getName().equals(article.getCategorie().getName())) {
                // Print category name, except for unknown
                currentCategorie = article.getCategorie();
                if (!currentCategorie.isUnknown()) {
                    prt.addToBuffer(currentCategorie.getName(), TicketPrinter.BOLD);
                }
            }
            final BigDecimal nb = item.getQty();
            final Float tauxFromId = TaxeCache.getCache().getTauxFromId(article.getIdTaxe());
            final BigDecimal tauxTVA = BigDecimal.valueOf(tauxFromId).movePointLeft(2).add(BigDecimal.ONE);

            final BigDecimal unitPrice = article.getPriceWithoutTax(nb, false).multiply(tauxTVA, DecimalUtils.HIGH_PRECISION);
            final BigDecimal multiply = article.getPriceWithoutTax(nb, false).multiply(nb, DecimalUtils.HIGH_PRECISION).multiply(tauxTVA, DecimalUtils.HIGH_PRECISION);

            String qtyString = DefaultTicketPrinter.formatRight(MAX_QTE_WIDTH, String.valueOf(nb));
            final String priceUnformated = TicketCellRenderer.centsToString(multiply.movePointRight(2).setScale(0, RoundingMode.HALF_UP).intValue());
            final String priceString = DefaultTicketPrinter.formatRight(MAX_PRICE_WIDTH, priceUnformated);
            String unitPriceString = "";
            if (article.getSalesUnit() == null) {
                if (nb.intValue() != 1) {
                    unitPriceString = DefaultTicketPrinter.formatRight(MAX_PRICE_WIDTH, TicketCellRenderer.centsToString(unitPrice.movePointRight(2).setScale(0, RoundingMode.HALF_UP).intValue()));
                }
            } else {
                unitPriceString = DefaultTicketPrinter.formatRight(MAX_PRICE_WIDTH, "");
                if (nb.signum() >= 0) {
                    qtyString = DefaultTicketPrinter.formatRight(MAX_QTE_WIDTH, "1");
                } else {
                    qtyString = DefaultTicketPrinter.formatRight(MAX_QTE_WIDTH, "-1");
                }
            }
            if (article.getCode() != null && !article.getCode().isEmpty() && !article.getCode().equalsIgnoreCase(article.getName())) {
                // 2 lines
                final String codeString = DefaultTicketPrinter.formatLeft(maxWidth - 2 - MAX_PRICE_WIDTH - MAX_QTE_WIDTH - 1 - unitPriceString.length(), article.getCode());
                prt.addToBuffer(qtyString + " " + codeString + " " + unitPriceString + " " + priceString);
                final String nameString = DefaultTicketPrinter.formatLeft(maxWidth - MAX_QTE_WIDTH - 1, article.getName());
                prt.addToBuffer("      " + nameString);
            } else {
                // 1 line
                final String nameString = DefaultTicketPrinter.formatLeft(maxWidth - 2 - MAX_PRICE_WIDTH - MAX_QTE_WIDTH - 1 - unitPriceString.length(), article.getName());
                prt.addToBuffer(qtyString + " " + nameString + " " + unitPriceString + " " + priceString);
            }
            if (article.getDeclinaison() != null) {
                prt.addToBuffer("      " + article.getDeclinaison());
            }

            if (article.getSalesUnit() != null) {
                prt.addToBuffer(
                        "      (" + nb.abs() + " " + article.getSalesUnit() + " x " + TicketCellRenderer.centsToString(unitPrice.movePointRight(2).setScale(0, RoundingMode.HALF_UP).intValue()) + ")");
            }

            if (article.getEcoTaxe().compareTo(BigDecimal.ZERO) != 0) {
                final String nameString = "       * dont eco-part.:";
                BigDecimal eco = article.getEcoTaxe().movePointRight(2).setScale(0, RoundingMode.HALF_UP);
                final String ecoString = DefaultTicketPrinter.formatRight(MAX_PRICE_WIDTH, TicketCellRenderer.centsToString(eco.intValue()));
                prt.addToBuffer(nameString + ecoString);
            }

            if (article.getDiscountPct().compareTo(BigDecimal.ZERO) != 0) {
                final String nameString = DefaultTicketPrinter.formatLeft(maxWidth - 2 - MAX_PRICE_WIDTH - 1, " * Remise");
                BigDecimal discount = multiply.movePointRight(2).multiply(article.getDiscountPct()).negate().setScale(0, RoundingMode.HALF_UP);
                final String discountString = DefaultTicketPrinter.formatRight(MAX_PRICE_WIDTH, TicketCellRenderer.centsToString(discount.intValue()));
                prt.addToBuffer(" " + nameString + " " + " " + discountString);
            }

        }

        final StringBuilder spacer = new StringBuilder();
        for (int i = 0; i <= MAX_QTE_WIDTH; i++) {
            spacer.append(' ');
        }
        for (int i = 0; i < maxWidth - MAX_QTE_WIDTH - 1; i++) {
            spacer.append('=');
        }
        prt.addToBuffer(spacer.toString());

        final TotalCalculator calc = getTotalCalculator();
        final int totalTTCInCents = calc.getTotalTTC().movePointRight(2).setScale(0, RoundingMode.HALF_UP).intValue();

        prt.addToBuffer(DefaultTicketPrinter.formatRight(maxWidth - MAX_PRICE_WIDTH, "MONTANT TOTAL TTC (Euros) : ")
                + DefaultTicketPrinter.formatRight(MAX_PRICE_WIDTH, TicketCellRenderer.centsToString(totalTTCInCents)), TicketPrinter.BOLD);

        final Map<SQLRowAccessor, Tuple2<BigDecimal, BigDecimal>> mapHtTVARowTaux = calc.getMapHtTVARowTaux();
        for (final SQLRowAccessor row : mapHtTVARowTaux.keySet()) {
            final Tuple2<BigDecimal, BigDecimal> htTVA = mapHtTVARowTaux.get(row);
            final float tvaTaux = TaxeCache.getCache().getTauxFromId(row.getID());
            final BigDecimal montantTVA = htTVA.get1();
            if (montantTVA != null && montantTVA.signum() != 0) {
                prt.addToBuffer(
                        DefaultTicketPrinter.formatRight(maxWidth - MAX_PRICE_WIDTH, "Dont TVA " + tvaTaux + "% : ")
                                + DefaultTicketPrinter.formatRight(MAX_PRICE_WIDTH, TicketCellRenderer.centsToString(montantTVA.movePointRight(2).setScale(0, RoundingMode.HALF_UP).intValue())),
                        TicketPrinter.NORMAL);
            }
        }
        prt.addToBuffer("");
        //
        for (final Paiement paiement : this.paiements) {

            String type = "";
            final int montantInCents = paiement.getMontantInCents();
            if (montantInCents != 0) {
                if (montantInCents > 0) {
                    type = "Paiement ";
                } else {
                    type = "Remboursement ";
                }
                if (paiement.getType() == Paiement.CB) {
                    type += "CB";
                } else if (paiement.getType() == Paiement.CHEQUE) {
                    type += "par chèque";
                } else if (paiement.getType() == Paiement.ESPECES) {
                    type += "en espèces";
                } else if (paiement.getType() == Paiement.SOLDE) {
                    type += "depuis solde";
                }
                if (montantInCents > 0) {
                    type += " de " + TicketCellRenderer.centsToString(montantInCents);
                } else {
                    type += " de " + TicketCellRenderer.centsToString(-montantInCents);
                }
                if (Math.abs(montantInCents) > 100) {
                    type += " euros";
                } else {
                    type += " euro";
                }
                prt.addToBuffer(type);
            }
        }
        // Montant Rendu
        if (getTotalInCents() < getPaidTotal()) {
            final int montantInCents = getPaidTotal() - getTotalInCents();
            String type = "Rendu : " + TicketCellRenderer.centsToString(montantInCents);
            if (Math.abs(montantInCents) > 100) {
                type += " euros";
            } else {
                type += " euro";
            }
            prt.addToBuffer(type);
        }
        prt.addToBuffer("");
        // Footer
        final List<TicketLine> footers = POSConfiguration.getInstance().getFooterLines();
        for (final TicketLine line : footers) {
            prt.addToBuffer(line);
        }
        prt.addToBuffer("");
        prt.addToBuffer(getCode(), TicketPrinter.BARCODE);
        prt.addToBuffer("");
        prt.addToBuffer("Ticket créé par l'ERP OpenConcerto.");

        try {
            prt.printBuffer();
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    public Date getCreationDate() {
        return this.getCreationCal().getTime();
    }

    public Calendar getCreationCal() {
        return this.creationCal;
    }

    public void setCreationCal(final Calendar cal) {
        // FIXME date mismatch when ticket created one day and saved the next one
        this.creationCal = cal == null ? null : (Calendar) cal.clone();
    }

    public void addPaiement(final Paiement p1) {
        this.paiements.add(p1);

    }

    public boolean isAdditionnalCopyRequested() {
        return this.additionnalCopyRequested;
    }

    public void addArticle(final Article a) {
        System.err.println("Ticket.addArticle()" + a);
        boolean alreadyExist = false;
        if (a.isAdditionalCopyRequested()) {
            this.additionnalCopyRequested = true;
        }
        if (a.getSalesUnit() == null) {
            for (final TicketItem line : this.items) {
                if (line.getArticle().equals(a)) {
                    alreadyExist = true;
                    break;
                }
            }
        }
        if (!alreadyExist) {
            final TicketItem line = new TicketItem(new Article(a), BigDecimal.ONE);
            this.items.add(line);
        }

    }

    public void incrementArticle(final Article a) {
        System.err.println("Ticket.incrementArticle()" + a.getName());
        TicketItem ticketLine;
        boolean alreadyExist = false;
        if (a.getSalesUnit() == null) {
            for (final TicketItem line : this.items) {
                if (line.getArticle().equals(a)) {
                    alreadyExist = true;
                    ticketLine = line;
                    ticketLine.setQty(line.getQty().add(BigDecimal.ONE));
                    break;
                }
            }
        }
        if (!alreadyExist) {
            ticketLine = new TicketItem(a, BigDecimal.ONE);
            this.items.add(ticketLine);
        }
    }

    public void addItem(TicketItem item) {
        this.items.add(item);
    }

    public List<Paiement> getPaiements() {
        return this.paiements;
    }

    public int getTotalInCents() {
        final TotalCalculator calc = getTotalCalculator();
        final BigDecimal totalTTC = calc.getTotalTTC();
        return totalTTC.movePointRight(2).setScale(0, RoundingMode.HALF_UP).intValue();
    }

    public TotalCalculator getTotalCalculator() {
        final SQLTable tableElt = ((ComptaPropsConfiguration) Configuration.getInstance()).getRootSociete().findTable("SAISIE_VENTE_FACTURE_ELEMENT");
        final TotalCalculator calc = new TotalCalculator("T_PA_HT", "T_PV_HT", null, null);
        final String val = DefaultNXProps.getInstance().getStringProperty("ArticleService");
        final Boolean bServiceActive = Boolean.valueOf(val);
        calc.setServiceActive(bServiceActive != null && bServiceActive);
        final int size = this.items.size();

        Set<Integer> ids = new HashSet<>();
        ArticleCache cache = ArticleCache.getInstance();
        for (int i = 0; i < size; i++) {
            final TicketItem line = this.items.get(i);
            final Article art = line.getArticle();
            int id = art.getId();
            if (!cache.isInCache(id)) {
                ids.add(id);
            }
        }
        if (!ids.isEmpty()) {
            cache.preloadCacheArticleMap(new ArrayList<>(ids));
        }

        for (int i = 0; i < size; i++) {
            final TicketItem line = this.items.get(i);
            final BigDecimal count = line.getQty();
            final Article art = line.getArticle();
            final SQLRowValues rowVals = new SQLRowValues(tableElt);

            rowVals.put("T_PV_HT", art.getPriceWithoutTax(count, true).multiply(count));

            if (art.getSalesUnit() != null) {
                rowVals.put("QTE_UNITAIRE", count);
                rowVals.put("QTE", BigDecimal.ONE);
            } else {
                rowVals.put("QTE", count.intValue());
            }
            rowVals.put("ID_TAXE", art.getIdTaxe());
            System.out.println("Ticket.getTotalCalculator()" + rowVals);
            calc.addLine(rowVals, cache.getArticleRowValuesFromCache(art.getId()), i, false);

        }
        calc.checkResult();
        return calc;
    }

    public List<TicketItem> getItems() {
        return this.items;
    }

    public void clearArticle(final Article article) {
        TicketItem toRemove = null;
        for (final TicketItem line : this.items) {
            if (line.getArticle().equals(article)) {
                toRemove = line;
                break;
            }
        }
        if (toRemove != null) {
            this.items.remove(toRemove);
        }
    }

    public void removeTicketItem(TicketItem item) {
        TicketItem toRemove = null;
        for (final TicketItem line : this.items) {
            if (line.equals(item)) {
                toRemove = line;
                break;
            }
        }
        if (toRemove != null) {
            this.items.remove(toRemove);
        }

    }

    public void setArticleCount(final Article article, final BigDecimal count) {
        // TODO Allow only if annulation?
        // if (count <= 0) {
        // this.clearArticle(article);
        // return;
        // }
        TicketItem toModify = null;
        for (final TicketItem line : this.items) {
            if (line.getArticle().equals(article)) {
                toModify = line;
                break;
            }
        }
        if (toModify != null) {
            toModify.setQty(count);
        }

    }

    public BigDecimal getItemCount(final Article article) {
        for (final TicketItem line : this.items) {
            if (line.getArticle().equals(article)) {
                return line.getQty();
            }
        }
        return BigDecimal.ZERO;
    }

    public int getPaidTotal() {
        int paid = 0;
        for (final Paiement p : this.paiements) {
            paid += p.getMontantInCents();
        }
        return paid;
    }

    public void removeArticle(final Article a) {
        TicketItem lineToDelete = null;
        for (final TicketItem line : this.items) {
            if (line.getArticle().equals(a)) {
                final BigDecimal count = line.getQty().add(BigDecimal.ONE);
                if (count.signum() <= 0) {
                    lineToDelete = line;
                }
                line.setQty(count);
                break;
            }
        }
        if (lineToDelete != null) {
            this.items.remove(lineToDelete);
        }

    }

    @Override
    public String toString() {
        return "Ticket " + getCode();
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        } else if (obj instanceof Ticket) {
            final Ticket t = (Ticket) obj;
            return t.getCode().equals(getCode());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return getCode().hashCode();
    }

    public void deleteTicket(final SQLElementDirectory dir) throws IOException {
        final SQLTable table = dir.getElement(CompteClientTransactionSQLELement.class).getTable();

        for (final Paiement paiement : this.paiements) {
            final int montantInCents = paiement.getMontantInCents();
            if (montantInCents > 0 && paiement.getType() == Paiement.SOLDE) {
                try {
                    SQLUtils.executeAtomic(table.getDBSystemRoot().getDataSource(), new SQLUtils.SQLFactory<Object>() {
                        @Override
                        public Object create() throws SQLException {
                            final SQLTable tablePrefCompte = table.getDBRoot().findTable("PREFS_COMPTE");
                            final SQLRow rowPrefsCompte = tablePrefCompte.getRow(2);
                            final SQLRowValues rowValsTransact = new SQLRowValues(table);
                            rowValsTransact.put("ID_CLIENT", Ticket.this.client.getId());

                            rowValsTransact.put("DATE", getCreationCal().getTime());
                            final BigDecimal amountTransaction = new BigDecimal(montantInCents).movePointLeft(2);
                            rowValsTransact.put("MONTANT", amountTransaction);

                            SQLRow rowTransact = rowValsTransact.commit();
                            final GenerationEcritures ecr = new GenerationEcritures();
                            final int idMvt = ecr.getNewMouvement(table.getName(), rowTransact.getID(), 1, "Annule Transact. " + Ticket.this.client.getFullName() + " Ticket " + getCode());
                            rowTransact = rowTransact.createEmptyUpdateRow().put("ID_MOUVEMENT", idMvt).commit();

                            // mise à jour du solde
                            final SQLTable tableClient = table.getForeignTable("ID_CLIENT");
                            final SQLRow row = tableClient.getRow(Ticket.this.client.getId());
                            final BigDecimal solde = row.getBigDecimal("SOLDE_COMPTE");
                            final BigDecimal nouveauSolde = solde.add(amountTransaction);
                            row.createEmptyUpdateRow().put("SOLDE_COMPTE", nouveauSolde).commit();

                            // Créeation des réglements et écritures

                            int idCompteAvanceClient = rowPrefsCompte.getInt("ID_COMPTE_PCE_AVANCE_CLIENT");
                            int idCompteClient = row.getInt("ID_COMPTE_PCE");
                            try {
                                if (idCompteAvanceClient <= 1) {
                                    idCompteAvanceClient = ComptePCESQLElement.getIdComptePceDefault("AvanceClients");
                                }

                                // compte Clients
                                if (idCompteClient <= 1) {
                                    idCompteClient = rowPrefsCompte.getInt("ID_COMPTE_PCE_CLIENT");
                                    if (idCompteClient <= 1) {
                                        idCompteClient = ComptePCESQLElement.getIdComptePceDefault("Clients");
                                    }
                                }
                            } catch (final Exception e) {
                                e.printStackTrace();
                                throw new SQLException(e);
                            }
                            new GenerationMvtVirement(idCompteAvanceClient, idCompteClient, montantInCents, 0L, "Annulation transaction compte client", getCreationCal().getTime(),
                                    JournalSQLElement.VENTES, "Ticket N°" + getCode()).genereMouvement();
                            return null;
                        }
                    });
                } catch (final SQLException e) {
                    e.printStackTrace();
                }
            }
        }

        getReceiptCode().markDeleted();
    }

}