OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 151 | Rev 177 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
151 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.erp.core.finance.payment.element;
15
 
16
import org.openconcerto.erp.config.ComptaPropsConfiguration;
17
import org.openconcerto.erp.core.common.element.ComptaSQLConfElement;
18
import org.openconcerto.erp.core.sales.invoice.element.SaisieVenteFactureSQLElement;
19
import org.openconcerto.sql.element.SQLComponent;
20
import org.openconcerto.sql.element.UISQLComponent;
21
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
22
import org.openconcerto.sql.model.DBRoot;
23
import org.openconcerto.sql.model.SQLDataSource;
24
import org.openconcerto.sql.model.SQLField;
25
import org.openconcerto.sql.model.SQLRow;
26
import org.openconcerto.sql.model.SQLRowAccessor;
27
import org.openconcerto.sql.model.SQLRowListRSH;
28
import org.openconcerto.sql.model.SQLRowValues;
29
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
30
import org.openconcerto.sql.model.SQLSchema;
31
import org.openconcerto.sql.model.SQLSelect;
32
import org.openconcerto.sql.model.SQLSelect.LockStrength;
33
import org.openconcerto.sql.model.SQLSelectJoin;
34
import org.openconcerto.sql.model.SQLTable;
35
import org.openconcerto.sql.model.Where;
36
import org.openconcerto.sql.preferences.SQLPreferences;
37
import org.openconcerto.sql.request.SQLFieldTranslator;
38
import org.openconcerto.sql.utils.SQLCreateTable;
39
import org.openconcerto.sql.utils.SQLUtils;
40
import org.openconcerto.sql.view.list.IListe;
41
import org.openconcerto.sql.view.list.IListeAction.IListeEvent;
42
import org.openconcerto.sql.view.list.RowAction;
43
import org.openconcerto.utils.CollectionMap2Itf.ListMapItf;
44
import org.openconcerto.utils.DecimalUtils;
45
import org.openconcerto.utils.ExceptionHandler;
46
import org.openconcerto.utils.FileUtils;
47
import org.openconcerto.utils.ListMap;
48
import org.openconcerto.utils.StringUtils;
49
import org.openconcerto.utils.TimeUtils;
50
import org.openconcerto.utils.Tuple2;
51
import org.openconcerto.utils.XMLDateFormat;
52
import org.openconcerto.utils.cc.ITransformer;
53
import org.openconcerto.utils.i18n.Grammar_fr;
54
import org.openconcerto.xml.JDOM2Utils;
55
 
56
import java.awt.Component;
57
import java.awt.event.ActionEvent;
58
import java.io.File;
59
import java.io.IOException;
60
import java.math.BigDecimal;
61
import java.sql.Clob;
62
import java.sql.SQLException;
63
import java.text.DateFormat;
64
import java.text.SimpleDateFormat;
65
import java.util.ArrayList;
66
import java.util.Calendar;
67
import java.util.Collection;
68
import java.util.Collections;
69
import java.util.Date;
70
import java.util.HashMap;
71
import java.util.HashSet;
72
import java.util.List;
73
import java.util.Map;
74
import java.util.Map.Entry;
75
import java.util.NavigableMap;
76
import java.util.Set;
77
import java.util.TreeMap;
78
import java.util.prefs.Preferences;
79
 
80
import javax.swing.AbstractAction;
81
import javax.swing.JFileChooser;
82
 
83
import org.jdom2.Document;
84
import org.jdom2.Element;
85
import org.jdom2.Namespace;
86
 
87
public final class SDDMessageSQLElement extends ComptaSQLConfElement {
156 ilm 88
    static public final String TABLE_NAME = "SEPA_DIRECT_DEBIT_MESSAGE";
151 ilm 89
    static public final String XML_LOCATION_PREF_KEY = "SDD.XML.location";
90
    public static final String SERIAL_MD = "SDD_MESSAGE_SERIAL";
91
 
92
    public static SQLCreateTable getCreateTable(final DBRoot root) {
93
        if (root.contains(TABLE_NAME))
94
            return null;
95
        final SQLCreateTable res = new SQLCreateTable(root, TABLE_NAME);
96
        res.addVarCharColumn("MessageIdentification", 35);
97
        res.addColumn("CreationDateTime", res.getSyntax().getDateAndTimeType(), null, false);
98
        res.addIntegerColumn("NumberOfTransactions", 0);
99
        res.addDecimalColumn("ControlSum", 16, 6, BigDecimal.ZERO, false);
100
        res.addColumn("XML", res.getSyntax().getTypeNames(Clob.class).iterator().next(), null, false);
101
        return res;
102
    }
103
 
104
    private final String prefsPath;
105
    private final SQLRow rowSociété;
106
    private final SQLPreferences dbPrefs;
107
    private final SQLFieldTranslator fieldTranslator;
108
 
109
    public SDDMessageSQLElement(final ComptaPropsConfiguration conf) {
110
        super(conf.getRootSociete().findTable(TABLE_NAME, true));
111
        this.prefsPath = conf.getAppID() + '/' + this.getCode().replace('.', '/');
112
        this.getRowActions().add(new RowAction.PredicateRowAction(new AbstractAction("Exporter…") {
113
            @Override
114
            public void actionPerformed(ActionEvent e) {
115
                final IListe l = IListe.get(e);
116
                // XML field values are quite large so only fetch them when needed
117
                final SQLTable t = l.getSource().getPrimaryTable();
118
                final SQLSelect sel = new SQLSelect().addSelectStar(t);
119
                sel.setWhere(new Where(t.getKey(), l.getSelection().getUserSelectedIDs()));
120
                exportXML(l, SQLRowListRSH.execute(sel));
121
            }
122
        }, true, false).setPredicate(IListeEvent.getNonEmptySelectionPredicate()));
123
        this.rowSociété = conf.getRowSociete();
124
        this.dbPrefs = new SQLPreferences(conf.getRootSociete());
125
        this.fieldTranslator = conf.getTranslator();
126
    }
127
 
128
    public final Preferences getPreferences() {
129
        return Preferences.userRoot().node(this.prefsPath);
130
    }
131
 
132
    @Override
133
    protected List<String> getListFields() {
134
        final List<String> l = new ArrayList<String>();
135
        l.add("MessageIdentification");
136
        l.add("CreationDateTime");
137
        l.add("NumberOfTransactions");
138
        l.add("ControlSum");
139
        return l;
140
    }
141
 
142
    @Override
143
    protected List<String> getComboFields() {
144
        final List<String> l = new ArrayList<String>();
145
        l.add("MessageIdentification");
146
        l.add("CreationDateTime");
147
        l.add("ControlSum");
148
        return l;
149
    }
150
 
151
    @Override
152
    public Set<String> getReadOnlyFields() {
153
        return this.getTable().getFieldsName();
154
    }
155
 
156
    @Override
157
    protected SQLComponent createComponent() {
158
        return new UISQLComponent(this) {
159
            @Override
160
            protected void addViews() {
161
                this.addView("MessageIdentification");
162
                this.addView("CreationDateTime");
163
                this.addView("NumberOfTransactions");
164
                this.addView("ControlSum");
165
                this.addView("XML");
166
            }
167
        };
168
    }
169
 
170
    @Override
171
    protected String createCode() {
172
        // TODO rename createCodeFromPackage() to createCodeOfPackage() and change createCode()
173
        // implementation to use a new createCodeFromPackage() which uses the class name (w/o
174
        // SQLElement suffix)
156 ilm 175
        return this.createCodeOfPackage() + ".SDDMessage";
151 ilm 176
    }
177
 
178
    public final void exportXML(final Component comp, final List<? extends SQLRowAccessor> messages) {
179
        final Preferences sddMsgPrefs = getPreferences();
180
        final String storedPath = sddMsgPrefs.get(XML_LOCATION_PREF_KEY, "");
181
        final JFileChooser fileChooser = new JFileChooser(storedPath);
182
        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
183
        final int answer = fileChooser.showDialog(comp, "Exporter " + getName().getVariant(Grammar_fr.DEFINITE_ARTICLE_PLURAL));
184
        if (answer == JFileChooser.APPROVE_OPTION) {
185
            final String newPath = fileChooser.getSelectedFile().getAbsolutePath();
186
            sddMsgPrefs.put(XML_LOCATION_PREF_KEY, newPath);
187
            final File directDebitDir = new File(newPath);
188
            try {
189
                for (final SQLRowAccessor messageRow : messages) {
190
                    FileUtils.write(messageRow.getString("XML"), new File(directDebitDir, messageRow.getString("MessageIdentification") + ".xml"), StringUtils.UTF8, false);
191
                }
192
            } catch (IOException exn) {
193
                ExceptionHandler.handle(comp, "Impossible d'exporter", exn);
194
            }
195
        }
196
    }
197
 
198
    protected static BigDecimal getInvoiceAmount(final SQLRowValues invoice) {
199
        return BigDecimal.valueOf(invoice.getLong("NET_A_PAYER")).movePointLeft(2);
200
    }
201
 
202
    static private final class InvoiceElem extends Tuple2<SQLRowValues, Element> {
203
        protected InvoiceElem(SQLRowValues a, Element b) {
204
            super(a, b);
205
        }
206
    }
207
 
208
    // Un lot est obligatoirement homogène sur les critères suivants :
209
    // - Même type de prélèvement SEPA (index 2.11 « LocalInstrument ») : SDD Core ou
210
    // SDD B2B
211
    // - Même séquence de présentation (index 2.14 « SequenceType ») : FRST ou RCUR
212
    static private final class PaymentInfo {
213
        private final Date collectionDate;
214
        private final String seqType;
215
        private final List<InvoiceElem> invoices;
216
        private final BigDecimal sum;
217
 
218
        protected PaymentInfo(Date collectionDate, String seqType, List<InvoiceElem> invoices) {
219
            super();
220
            this.collectionDate = collectionDate;
221
            if (!SEPAMandateSQLElement.SEQ_VALUES.contains(seqType))
222
                throw new IllegalArgumentException("Invalid sequence type : " + seqType);
223
            this.seqType = seqType;
224
            this.invoices = invoices;
225
            BigDecimal d = BigDecimal.ZERO;
226
            for (final InvoiceElem invoice : invoices) {
227
                d = d.add(getInvoiceAmount(invoice.get0()));
228
            }
229
            this.sum = d;
230
        }
231
 
232
        @Override
233
        public String toString() {
234
            return this.getClass().getSimpleName() + " for " + this.invoices.size() + " " + this.seqType + " invoices at " + this.collectionDate;
235
        }
236
    }
237
 
238
    static private final class InvoicesByPaymentInfo {
239
        private final Date lowerBound, upperBound;
240
 
156 ilm 241
        // { collection date -> { sequence type -> invoice } }
151 ilm 242
        private final NavigableMap<Date, ListMap<String, InvoiceElem>> map = new TreeMap<>();
243
        private final Set<Number> lockedInvoicesIDs = new HashSet<>();
244
        private BigDecimal sum = BigDecimal.ZERO;
245
        private final Set<String> invoiceNumbers = new HashSet<>();
246
        private final Set<String> invoiceMandates = new HashSet<>();
247
        private final ElementCreator elemCreator;
248
 
249
        protected InvoicesByPaymentInfo(Date lowerBound, Date upperBound, final ElementCreator elemCreator) {
250
            super();
251
            if (lowerBound.compareTo(upperBound) >= 0)
252
                throw new IllegalArgumentException("Lower date after upper date : " + lowerBound + " >= " + upperBound);
253
            this.lowerBound = lowerBound;
254
            this.upperBound = upperBound;
255
            this.elemCreator = elemCreator;
256
        }
257
 
258
        final IgnoreReason addInvoice(final SQLRowValues invoice) {
259
            Date dueDate = ModeDeReglementSQLElement.calculDate(invoice.getForeign("ID_MODE_REGLEMENT"), invoice.getObjectAs("DATE", Date.class));
260
 
261
            // don't ask direct debit too far in advance
262
            if (dueDate.after(this.upperBound)) {
263
                return IgnoreReason.TOO_FAR_IN_FUTURE;
264
            } else if (dueDate.before(this.lowerBound)) {
265
                dueDate = this.lowerBound;
266
            }
267
 
268
            final Element elem;
269
            try {
270
                elem = createDDTx(this.elemCreator, invoice);
271
            } catch (MissingInfoException e) {
272
                return IgnoreReason.MISSING_INFO;
273
            }
274
 
275
            // needed so that EndToEndId is unique
276
            if (!this.invoiceNumbers.add(invoice.getString("NUMERO")))
277
                throw new IllegalStateException("Duplicate invoice number : " + invoice);
278
            final SQLRowAccessor mandate = invoice.getForeign("ID_MODE_REGLEMENT").getForeign("ID_SEPA_MANDATE");
279
            if (!mandate.getBoolean("ACTIVE"))
280
                throw new IllegalStateException("Inactive mandate for " + invoice);
281
            // needed otherwise would have to update seqType while generating
156 ilm 282
            // MAYBE sum all invoices for a single mandate, but which date to choose ?
151 ilm 283
            if (!this.invoiceMandates.add(mandate.getString("MandateIdentification")))
284
                return IgnoreReason.DUPLICATE_MANDATE;
285
 
286
            this.lockedInvoicesIDs.add(invoice.getIDNumber());
287
            ListMap<String, InvoiceElem> bySeqType = this.map.get(dueDate);
288
            if (bySeqType == null) {
289
                bySeqType = new ListMap<>();
290
                this.map.put(dueDate, bySeqType);
291
            }
292
            bySeqType.add(mandate.getString("SequenceType"), new InvoiceElem(invoice, elem));
293
 
294
            this.sum = this.sum.add(getInvoiceAmount(invoice));
295
 
296
            return IgnoreReason.NONE;
297
        }
298
 
299
        public final int getTransactionCount() {
300
            return this.lockedInvoicesIDs.size();
301
        }
302
 
303
        public final List<PaymentInfo> getPaymentInfos() {
304
            final List<PaymentInfo> res = new ArrayList<>();
305
            for (final Entry<Date, ListMap<String, InvoiceElem>> e : this.map.entrySet()) {
306
                final Date collectionDate = e.getKey();
307
                for (final Entry<String, List<InvoiceElem>> e2 : e.getValue().entrySet()) {
308
                    final String seqType = e2.getKey();
309
                    res.add(new PaymentInfo(collectionDate, seqType, e2.getValue()));
310
                }
311
            }
312
            return res;
313
        }
314
    }
315
 
316
    public static enum IgnoreReason {
317
        NONE, TOO_FAR_IN_FUTURE, DUPLICATE_MANDATE, MISSING_INFO;
318
    }
319
 
320
    public static final class GenerationResult {
321
        private final Collection<? extends Number> passedIDs;
322
        private final List<SQLRowValues> withDDWithoutMessage;
323
        private final ListMapItf<IgnoreReason, SQLRowValues> ignoredInvoices;
324
        private final SQLRow insertedMessage;
325
        private final int invoiceCount;
326
 
327
        protected GenerationResult(Collection<? extends Number> passedIDs, List<SQLRowValues> withDDWithoutMessage, ListMap<IgnoreReason, SQLRowValues> ignoredInvoices, SQLRow insertedMessage) {
328
            super();
329
            this.passedIDs = passedIDs;
330
            this.withDDWithoutMessage = Collections.unmodifiableList(withDDWithoutMessage);
331
            assert !ignoredInvoices.containsKey(null) && !ignoredInvoices.containsKey(IgnoreReason.NONE);
332
            this.ignoredInvoices = ListMap.unmodifiableMap(ignoredInvoices);
333
            this.insertedMessage = insertedMessage;
334
            this.invoiceCount = insertedMessage == null ? 0 : insertedMessage.getInt("NumberOfTransactions");
335
            assert this.withDDWithoutMessage.size() - this.ignoredInvoices.allValues().size() == this.invoiceCount;
336
        }
337
 
338
        // OK since both the list and the SQLRowValues are immutable
339
        public final List<SQLRowValues> getDDInvoicesWithoutMessage() {
340
            return this.withDDWithoutMessage;
341
        }
342
 
343
        // OK since both the list and the SQLRowValues are immutable
344
        public final ListMapItf<IgnoreReason, SQLRowValues> getIgnoredInvoices() {
345
            return this.ignoredInvoices;
346
        }
347
 
348
        public final SQLRow getInsertedMessage() {
349
            return this.insertedMessage;
350
        }
351
 
352
        public final int getIncludedInvoicesCount() {
353
            return this.invoiceCount;
354
        }
355
 
356
        @Override
357
        public String toString() {
358
            return this.getClass().getSimpleName() + ": of the " + this.passedIDs.size() + " passed, " + this.withDDWithoutMessage.size() + " needed a SDD message, of those "
359
                    + this.ignoredInvoices.allValues().size() + " were ignored (either being too far in the future or duplicate mandates) ; the inserted row was " + this.insertedMessage;
360
        }
361
    }
362
 
363
    public GenerationResult generateXML(Collection<? extends Number> invoiceIDs) throws SQLException {
364
        final Namespace painNS = Namespace.getNamespace("urn:iso:std:iso:20022:tech:xsd:pain.008.001.02");
365
        final Element rootElem = new Element("Document", painNS);
366
        final Namespace xsiNS = Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
367
        rootElem.setAttribute("schemaLocation", "urn:iso:std:iso:20022:tech:xsd:pain.008.001.02 pain.008.001.02.xsd", xsiNS);
368
        final Document doc = new Document(rootElem);
369
        final Element ddElem = new Element("CstmrDrctDbtInitn", painNS);
370
        rootElem.addContent(ddElem);
371
 
372
        // Lead Time / Time cycle
373
        // https://www.ecb.europa.eu/paym/retpaym/undpaym/qa/html/index.en.html
374
        final Calendar now = Calendar.getInstance();
375
        final Calendar cal = (Calendar) now.clone();
376
        TimeUtils.clearTime(cal);
377
        // perhaps handle business days and difference between FRST/OOFF (5 days) and RCURR/FNAL (2
378
        // days).
379
        cal.add(Calendar.DAY_OF_YEAR, this.dbPrefs.getInt(getCode() + ".leadDays", 7));
380
        final Date lowerBound = cal.getTime();
381
 
382
        cal.setTime(now.getTime());
383
        cal.add(Calendar.DAY_OF_YEAR, 30);
384
        final Date upperBound = cal.getTime();
385
 
386
        // TODO use createFetcher()
387
        final SQLRowValuesListFetcher selSociété = SQLRowValuesListFetcher.create(this.getDirectory().getElement(this.rowSociété.getTable()).createGraph());
388
        selSociété.setFullOnly(true);
389
        selSociété.appendSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
390
            @Override
391
            public SQLSelect transformChecked(SQLSelect sel) {
392
                sel.setLockStrength(LockStrength.SHARE);
393
                return sel;
394
            }
395
        });
396
 
397
        final SQLTable invoiceT = getDirectory().getElement(SaisieVenteFactureSQLElement.class).getTable();
398
        final SQLField invoiceSDDMessageF = invoiceT.getField(SaisieVenteFactureSQLElement.MESSAGE_FIELD_NAME);
399
        return SQLUtils.executeAtomic(getTable().getDBSystemRoot().getDataSource(), new ConnectionHandlerNoSetup<GenerationResult, SQLException>() {
400
            @Override
401
            public GenerationResult handle(SQLDataSource ds) throws SQLException {
402
                final SQLRowValues lockedSociété = selSociété.fetchOne(SDDMessageSQLElement.this.rowSociété.getIDNumber());
403
                if (lockedSociété == null)
404
                    throw new IllegalStateException("Missing société " + SDDMessageSQLElement.this.rowSociété);
405
 
406
                // find and lock invoices with TYPE_REGLEMENT direct debit and no message
407
                final SQLRowValues invoiceVals = new SQLRowValues(invoiceT);
408
                invoiceVals.putRowValues("ID_CLIENT").putNulls("NOM", "BIC", "IBAN");
409
                invoiceVals.putRowValues("ID_MODE_REGLEMENT").putNulls("AJOURS", "LENJOUR").putRowValues("ID_SEPA_MANDATE").setAllToNull();
156 ilm 410
                invoiceVals.putNulls("NET_A_PAYER", "DATE", "NUMERO", "NOM");
151 ilm 411
                final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(invoiceVals);
412
                fetcher.setReturnedRowsUnmodifiable(true);
413
                // required for locking rows and to make sure that there's a SEPA Mandate
414
                fetcher.setFullOnly(true);
415
                fetcher.appendSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
416
                    @Override
417
                    public SQLSelect transformChecked(SQLSelect sel) {
418
                        // we will update FACTURE.ID_MESSAGE
419
                        sel.setLockStrength(LockStrength.UPDATE);
420
                        final SQLSelectJoin join = sel.getJoin(invoiceT.getField("ID_MODE_REGLEMENT"));
421
                        join.setWhere(new Where(join.getJoinedTable().getField("ID_TYPE_REGLEMENT"), "=", TypeReglementSQLElement.PRELEVEMENT));
422
                        return sel;
423
                    }
424
                });
425
                final List<SQLRowValues> ddInvoices = fetcher
426
                        .fetch(new Where(invoiceT.getKey(), invoiceIDs).and(new Where(invoiceSDDMessageF, "=", invoiceSDDMessageF.getForeignTable().getUndefinedIDNumber())));
427
                final InvoicesByPaymentInfo map = new InvoicesByPaymentInfo(lowerBound, upperBound, new ElementCreator(painNS, SDDMessageSQLElement.this.fieldTranslator));
428
                final ListMap<IgnoreReason, SQLRowValues> ignoredInvoices = new ListMap<>();
429
                for (final SQLRowValues invoice : ddInvoices) {
430
                    final IgnoreReason ignoredReason = map.addInvoice(invoice);
431
                    if (ignoredReason != IgnoreReason.NONE) {
432
                        ignoredInvoices.add(ignoredReason, invoice);
433
                    }
434
                }
435
 
436
                final SQLRow newMsg;
437
                final int txCount = map.getTransactionCount();
438
                if (txCount == 0) {
439
                    newMsg = null;
440
                } else {
441
                    // find and lock message serial
442
                    final SQLTable mdT = getTable().getTable(SQLSchema.METADATA_TABLENAME);
443
                    final SQLSelect sel = new SQLSelect(true).addSelect(mdT.getField("VALUE"));
444
                    sel.setWhere(new Where(mdT.getField("NAME"), "=", SERIAL_MD));
445
                    sel.setLockStrength(LockStrength.UPDATE);
446
                    final String msgSerial = String.valueOf(Integer.parseInt((String) getTable().getDBSystemRoot().getDataSource().executeScalar(sel.asString())) + 1);
447
 
448
                    // generate XML
449
                    final BigDecimal totalSum = map.sum;
450
                    final Element groupHeaderElem = createGroupHeader(painNS, lockedSociété, now, msgSerial, txCount, totalSum);
451
                    ddElem.addContent(groupHeaderElem);
452
                    final String msgID = groupHeaderElem.getChild("MsgId", painNS).getText();
453
                    final Map<SQLRow, String> end2endIDs = new HashMap<>();
454
                    int index = 1;
455
                    for (final PaymentInfo info : map.getPaymentInfos()) {
456
                        try {
457
                            createPaymentInfo(ddElem, end2endIDs, map.elemCreator, lockedSociété, info, msgID, index++);
458
                        } catch (Exception e) {
459
                            throw new IllegalStateException("Couldn't create XML for " + info, e);
460
                        }
461
                    }
462
                    assert end2endIDs.size() == txCount : "Expected " + txCount + " transactions but got " + end2endIDs.size() + " rows : " + end2endIDs;
463
 
464
                    // insert message in DB
465
                    final SQLRowValues msgVals = new SQLRowValues(getTable());
466
                    msgVals.put("MessageIdentification", msgID);
467
                    msgVals.put("CreationDateTime", now.getTime());
468
                    msgVals.put("NumberOfTransactions", txCount);
469
                    msgVals.put("ControlSum", totalSum);
470
                    msgVals.put("XML", JDOM2Utils.output(doc));
471
                    newMsg = msgVals.insert();
472
 
473
                    // update invoices with new message
474
                    for (final Entry<SQLRow, String> e : end2endIDs.entrySet()) {
475
                        final SQLRowValues vals = e.getKey().createEmptyUpdateRow();
476
                        vals.putForeignID(invoiceSDDMessageF.getName(), newMsg);
477
                        vals.put(SaisieVenteFactureSQLElement.END2END_FIELD_NAME, e.getValue());
478
                        vals.update();
479
                    }
480
                    // update message serial
481
                    getTable().getDBRoot().setMetadata(SERIAL_MD, msgSerial);
482
                }
483
 
484
                return new GenerationResult(invoiceIDs, ddInvoices, ignoredInvoices, newMsg);
485
            }
486
        });
487
    }
488
 
489
    static private Element createGroupHeader(final Namespace painNS, final SQLRowValues lockedSociété, final Calendar now, final String msgSerial, final int txCount, final BigDecimal total) {
490
        final Element res = new Element("GrpHdr", painNS);
491
        res.addContent(new Element("MsgId", painNS).setText("openconcerto-" + now.get(Calendar.YEAR) + "-" + msgSerial));
492
        res.addContent(new Element("CreDtTm", painNS).setText(new XMLDateFormat().format(now)));
493
        res.addContent(new Element("NbOfTxs", painNS).setText(String.valueOf(txCount)));
494
        if (DecimalUtils.decimalDigits(total) > 2)
495
            throw new IllegalArgumentException("Too many decimals : " + total);
496
        res.addContent(new Element("CtrlSum", painNS).setText(total.toPlainString()));
497
        res.addContent(new Element("InitgPty", painNS).addContent(new Element("Nm", painNS).setText(getCompanyName(lockedSociété))));
498
        return res;
499
    }
500
 
501
    static private String getCompanyName(final SQLRowValues lockedSociété) {
502
        final String companyName = lockedSociété.getString("NOM");
503
        if (StringUtils.isEmpty(companyName))
504
            throw new IllegalStateException("Empty company name : " + lockedSociété);
505
        return lockedSociété.getString("TYPE") + ' ' + companyName;
506
    }
507
 
508
    static private final DateFormat XML_DATE_FMT = new SimpleDateFormat("yyyy-MM-dd");
509
 
510
    static synchronized private final String formatDate(final Date date) {
511
        return XML_DATE_FMT.format(date);
512
    }
513
 
514
    static private void createPaymentInfo(Element ddElem, Map<SQLRow, String> end2endIDs, final ElementCreator elemCreator, final SQLRowValues lockedSociété, final PaymentInfo info,
515
            final String msgID, final int index) throws SQLException, MissingInfoException {
516
        if (info.invoices.isEmpty())
517
            return;
518
 
519
        final Namespace painNS = elemCreator.painNS;
520
        final String formattedDate = formatDate(info.collectionDate);
521
 
522
        final Element res = new Element("PmtInf", painNS);
523
        res.addContent(new Element("PmtInfId", painNS).setText("openconcerto-" + formattedDate + '.' + index));
524
        res.addContent(new Element("PmtMtd", painNS).setText("DD"));
525
        res.addContent(new Element("BtchBookg", painNS).setText("false"));
526
        res.addContent(new Element("NbOfTxs", painNS).setText(String.valueOf(info.invoices.size())));
527
        if (DecimalUtils.decimalDigits(info.sum) > 2)
528
            throw new IllegalArgumentException("Too many decimals : " + info.sum);
529
        res.addContent(new Element("CtrlSum", painNS).setText(info.sum.toPlainString()));
530
 
531
        final Element typeInformation = new Element("PmtTpInf", painNS);
532
        typeInformation.addContent(new Element("SvcLvl", painNS).addContent(new Element("Cd", painNS).setText("SEPA")));
533
        typeInformation.addContent(new Element("LclInstrm", painNS).addContent(new Element("Cd", painNS).setText("CORE")));
534
        typeInformation.addContent(new Element("SeqTp", painNS).setText(info.seqType));
535
        res.addContent(typeInformation);
536
 
537
        res.addContent(new Element("ReqdColltnDt", painNS).setText(formattedDate));
538
 
539
        final Element creditor = new Element("Cdtr", painNS);
540
        creditor.addContent(new Element("Nm", painNS).setText(getCompanyName(lockedSociété)));
541
        final Element postalAddr = new Element("PstlAdr", painNS);
542
        final SQLRowAccessor addr = lockedSociété.getNonEmptyForeign("ID_ADRESSE_COMMON");
543
        final String country = addr.getString("PAYS");
544
        final String country2;
545
        if (StringUtils.isEmpty(country, true) || country.trim().equalsIgnoreCase("France")) {
546
            country2 = "FR";
547
        } else {
548
            // TODO map to 2 letter code
549
            throw new IllegalStateException("Unknown country : " + country);
550
        }
551
        postalAddr.addContent(new Element("Ctry", painNS).setText(country2));
552
        postalAddr.addContent(new Element("AdrLine", painNS).setText(addr.getString("RUE")));
553
        postalAddr.addContent(new Element("AdrLine", painNS).setText(addr.getString("CODE_POSTAL") + " " + addr.getString("VILLE")));
554
        creditor.addContent(postalAddr);
555
        res.addContent(creditor);
556
 
557
        final Element creditorAccount = new Element("CdtrAcct", painNS);
558
        creditorAccount.addContent(new Element("Id", painNS).addContent(elemCreator.createWithNonEmptyText("IBAN", lockedSociété, "IBAN")));
559
        res.addContent(creditorAccount);
560
 
561
        final Element creditorAgent = new Element("CdtrAgt", painNS);
562
        creditorAgent.addContent(new Element("FinInstnId", painNS).addContent(elemCreator.createWithNonEmptyText("BIC", lockedSociété, "BIC")));
563
        res.addContent(creditorAgent);
564
 
565
        res.addContent(new Element("ChrgBr", painNS).setText("SLEV"));
566
 
567
        final Element creditorID = new Element("CdtrSchmeId", painNS);
568
        final Element other = new Element("Othr", painNS);
569
        other.addContent(elemCreator.createWithNonEmptyText("Id", lockedSociété, "SEPA_CREDITOR_ID"));
570
        other.addContent(new Element("SchmeNm", painNS).addContent(new Element("Prtry", painNS).setText("SEPA")));
571
        creditorID.addContent(new Element("Id", painNS).addContent(new Element("PrvtId", painNS).addContent(other)));
572
        res.addContent(creditorID);
573
 
574
        for (final InvoiceElem invoice : info.invoices) {
575
            final String end2endID = msgID + '.' + invoice.get0().getString("NUMERO");
576
            if (end2endIDs.put(invoice.get0().asRow(), end2endID) != null)
577
                throw new IllegalStateException("Duplicate invoice : " + invoice);
578
            try {
579
                res.addContent(fillDDTx(invoice, elemCreator, end2endID));
580
            } catch (Exception e) {
581
                throw new IllegalStateException("Couldn't create XML for " + invoice, e);
582
            }
583
        }
584
 
585
        ddElem.addContent(res);
586
    }
587
 
588
    static private Element fillDDTx(final InvoiceElem invoiceElem, final ElementCreator elemCreator, final String end2endID) throws SQLException, MissingInfoException {
589
        final Element paymentId = new Element("PmtId", elemCreator.painNS);
590
        paymentId.addContent(elemCreator.createWithNonEmptyText("InstrId", end2endID));
591
        paymentId.addContent(elemCreator.createWithNonEmptyText("EndToEndId", end2endID));
592
        invoiceElem.get1().addContent(0, paymentId);
593
 
594
        // update mandate fields
595
        final SQLRowAccessor mandate = invoiceElem.get0().getForeign("ID_MODE_REGLEMENT").getForeign("ID_SEPA_MANDATE");
596
        final String seqType = mandate.getString("SequenceType");
597
        if (seqType.equals(SEPAMandateSQLElement.SEQ_FIRST)) {
598
            mandate.createEmptyUpdateRow().put("SequenceType", SEPAMandateSQLElement.SEQ_RECURRENT).update();
599
        } else if (seqType.equals(SEPAMandateSQLElement.SEQ_FINAL) || seqType.equals(SEPAMandateSQLElement.SEQ_ONEOFF)) {
600
            mandate.createEmptyUpdateRow().put("ACTIVE", Boolean.FALSE).update();
601
        } // else SEQ_RECURRENT
602
 
603
        return invoiceElem.get1();
604
    }
605
 
606
    static private Element createDDTx(final ElementCreator elemCreator, final SQLRowValues invoice) throws MissingInfoException {
607
        final Namespace painNS = elemCreator.painNS;
608
        final Element res = new Element("DrctDbtTxInf", painNS);
609
 
610
        res.addContent(new Element("InstdAmt", painNS).setAttribute("Ccy", "EUR").setText(getInvoiceAmount(invoice).toPlainString()));
611
 
612
        final Element mandateRltdInfo = new Element("MndtRltdInf", painNS);
613
        final SQLRowAccessor mandate = invoice.getForeign("ID_MODE_REGLEMENT").getForeign("ID_SEPA_MANDATE");
614
        assert !mandate.isUndefined() : "Undefined mandate returned by fetcher";
615
        mandateRltdInfo.addContent(elemCreator.createWithNonEmptyText("MndtId", mandate, "MandateIdentification"));
616
        mandateRltdInfo.addContent(elemCreator.createWithNonEmptyText("DtOfSgntr", formatDate(mandate.getObjectAs("DateOfSignature", Date.class))));
617
        mandateRltdInfo.addContent(elemCreator.createWithNonEmptyText("AmdmntInd", "false"));
618
        res.addContent(new Element("DrctDbtTx", painNS).addContent(mandateRltdInfo));
619
 
620
        final SQLRowAccessor clientRow = invoice.getForeign("ID_CLIENT");
621
        res.addContent(new Element("DbtrAgt", painNS).addContent(new Element("FinInstnId", painNS).addContent(elemCreator.createWithNonEmptyText("BIC", clientRow, "BIC"))));
622
        res.addContent(new Element("Dbtr", painNS).addContent(elemCreator.createWithNonEmptyText("Nm", clientRow, "NOM")));
623
        res.addContent(new Element("DbtrAcct", painNS).addContent(new Element("Id", painNS).addContent(elemCreator.createWithNonEmptyText("IBAN", clientRow, "IBAN"))));
624
 
625
        res.addContent(new Element("Purp", painNS).addContent(new Element("Cd", painNS).setText("OTHR")));
626
 
156 ilm 627
        final String info = (invoice.getString("NUMERO") + ' ' + invoice.getString("NOM")).trim();
628
        if (!info.isEmpty())
629
            res.addContent(new Element("RmtInf", painNS).addContent(elemCreator.create("Ustrd").setText(elemCreator.shortenText(info, 140))));
151 ilm 630
 
631
        return res;
632
    }
633
 
634
    static public final class MissingInfoException extends Exception {
635
 
636
        private final String label;
637
 
638
        protected MissingInfoException(final String label) {
639
            super("Empty " + label);
640
            this.label = label;
641
        }
642
 
643
        public final String getLabel() {
644
            return this.label;
645
        }
646
    }
647
 
648
    static private final class ElementCreator {
156 ilm 649
        static private final String TRUNCATED_SUFFIX = "...";
650
        static private final int TRUNCATED_SUFFIX_LENGTH = TRUNCATED_SUFFIX.length();
151 ilm 651
        private final Namespace painNS;
652
        private final SQLFieldTranslator fieldTrans;
653
 
654
        protected ElementCreator(Namespace painNS, final SQLFieldTranslator fieldTrans) {
655
            super();
656
            this.painNS = painNS;
657
            this.fieldTrans = fieldTrans;
658
        }
659
 
660
        protected Element create(final String elemName) {
661
            return new Element(elemName, this.painNS);
662
        }
663
 
664
        protected Element createWithNonEmptyText(final String elemName, final SQLRowAccessor r, final String field) throws MissingInfoException {
665
            return this.createWithNonEmptyText(elemName, r.getString(field), this.fieldTrans.getDescFor(r.getTable(), field).getLabel());
666
        }
667
 
668
        protected Element createWithNonEmptyText(final String elemName, final String text) throws MissingInfoException {
669
            return this.createWithNonEmptyText(elemName, text, null);
670
        }
671
 
672
        protected Element createWithNonEmptyText(final String elemName, final String text, final String label) throws MissingInfoException {
673
            if (StringUtils.isEmpty(text))
674
                throw new MissingInfoException(label == null ? elemName : label);
675
            return create(elemName).setText(text);
676
        }
156 ilm 677
 
678
        protected String shortenText(final String text, final int maxLength) {
679
            if (maxLength <= TRUNCATED_SUFFIX_LENGTH || text.length() <= maxLength)
680
                return text;
681
            return text.substring(0, maxLength - TRUNCATED_SUFFIX_LENGTH) + TRUNCATED_SUFFIX;
682
        }
151 ilm 683
    }
684
 
685
}