Dépôt officiel du code source de l'ERP OpenConcerto
Rev 156 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011 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.finance.payment.element;
import org.openconcerto.erp.config.ComptaPropsConfiguration;
import org.openconcerto.erp.core.common.element.ComptaSQLConfElement;
import org.openconcerto.erp.core.sales.invoice.element.SaisieVenteFactureSQLElement;
import org.openconcerto.sql.element.SQLComponent;
import org.openconcerto.sql.element.UISQLComponent;
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
import org.openconcerto.sql.model.DBRoot;
import org.openconcerto.sql.model.SQLDataSource;
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.SQLSchema;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLSelect.LockStrength;
import org.openconcerto.sql.model.SQLSelectJoin;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.preferences.SQLPreferences;
import org.openconcerto.sql.request.SQLFieldTranslator;
import org.openconcerto.sql.utils.SQLCreateTable;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.sql.view.list.IListe;
import org.openconcerto.sql.view.list.IListeAction.IListeEvent;
import org.openconcerto.sql.view.list.RowAction;
import org.openconcerto.utils.CollectionMap2Itf.ListMapItf;
import org.openconcerto.utils.DecimalUtils;
import org.openconcerto.utils.ExceptionHandler;
import org.openconcerto.utils.FileUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.TimeUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.XMLDateFormat;
import org.openconcerto.utils.cc.ITransformer;
import org.openconcerto.utils.i18n.Grammar_fr;
import org.openconcerto.xml.JDOM2Utils;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.sql.Clob;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.prefs.Preferences;
import javax.swing.AbstractAction;
import javax.swing.JFileChooser;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
public final class SDDMessageSQLElement extends ComptaSQLConfElement {
static final String TABLE_NAME = "SEPA_DIRECT_DEBIT_MESSAGE";
static public final String XML_LOCATION_PREF_KEY = "SDD.XML.location";
public static final String SERIAL_MD = "SDD_MESSAGE_SERIAL";
public static SQLCreateTable getCreateTable(final DBRoot root) {
if (root.contains(TABLE_NAME))
return null;
final SQLCreateTable res = new SQLCreateTable(root, TABLE_NAME);
res.addVarCharColumn("MessageIdentification", 35);
res.addColumn("CreationDateTime", res.getSyntax().getDateAndTimeType(), null, false);
res.addIntegerColumn("NumberOfTransactions", 0);
res.addDecimalColumn("ControlSum", 16, 6, BigDecimal.ZERO, false);
res.addColumn("XML", res.getSyntax().getTypeNames(Clob.class).iterator().next(), null, false);
return res;
}
private final String prefsPath;
private final SQLRow rowSociété;
private final SQLPreferences dbPrefs;
private final SQLFieldTranslator fieldTranslator;
public SDDMessageSQLElement(final ComptaPropsConfiguration conf) {
super(conf.getRootSociete().findTable(TABLE_NAME, true));
this.prefsPath = conf.getAppID() + '/' + this.getCode().replace('.', '/');
this.getRowActions().add(new RowAction.PredicateRowAction(new AbstractAction("Exporter…") {
@Override
public void actionPerformed(ActionEvent e) {
final IListe l = IListe.get(e);
// XML field values are quite large so only fetch them when needed
final SQLTable t = l.getSource().getPrimaryTable();
final SQLSelect sel = new SQLSelect().addSelectStar(t);
sel.setWhere(new Where(t.getKey(), l.getSelection().getUserSelectedIDs()));
exportXML(l, SQLRowListRSH.execute(sel));
}
}, true, false).setPredicate(IListeEvent.getNonEmptySelectionPredicate()));
this.rowSociété = conf.getRowSociete();
this.dbPrefs = new SQLPreferences(conf.getRootSociete());
this.fieldTranslator = conf.getTranslator();
}
public final Preferences getPreferences() {
return Preferences.userRoot().node(this.prefsPath);
}
@Override
protected List<String> getListFields() {
final List<String> l = new ArrayList<String>();
l.add("MessageIdentification");
l.add("CreationDateTime");
l.add("NumberOfTransactions");
l.add("ControlSum");
return l;
}
@Override
protected List<String> getComboFields() {
final List<String> l = new ArrayList<String>();
l.add("MessageIdentification");
l.add("CreationDateTime");
l.add("ControlSum");
return l;
}
@Override
public Set<String> getReadOnlyFields() {
return this.getTable().getFieldsName();
}
@Override
protected SQLComponent createComponent() {
return new UISQLComponent(this) {
@Override
protected void addViews() {
this.addView("MessageIdentification");
this.addView("CreationDateTime");
this.addView("NumberOfTransactions");
this.addView("ControlSum");
this.addView("XML");
}
};
}
@Override
protected String createCode() {
// TODO rename createCodeFromPackage() to createCodeOfPackage() and change createCode()
// implementation to use a new createCodeFromPackage() which uses the class name (w/o
// SQLElement suffix)
return this.createCodeFromPackage() + ".SDDMessage";
}
public final void exportXML(final Component comp, final List<? extends SQLRowAccessor> messages) {
final Preferences sddMsgPrefs = getPreferences();
final String storedPath = sddMsgPrefs.get(XML_LOCATION_PREF_KEY, "");
final JFileChooser fileChooser = new JFileChooser(storedPath);
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
final int answer = fileChooser.showDialog(comp, "Exporter " + getName().getVariant(Grammar_fr.DEFINITE_ARTICLE_PLURAL));
if (answer == JFileChooser.APPROVE_OPTION) {
final String newPath = fileChooser.getSelectedFile().getAbsolutePath();
sddMsgPrefs.put(XML_LOCATION_PREF_KEY, newPath);
final File directDebitDir = new File(newPath);
try {
for (final SQLRowAccessor messageRow : messages) {
FileUtils.write(messageRow.getString("XML"), new File(directDebitDir, messageRow.getString("MessageIdentification") + ".xml"), StringUtils.UTF8, false);
}
} catch (IOException exn) {
ExceptionHandler.handle(comp, "Impossible d'exporter", exn);
}
}
}
protected static BigDecimal getInvoiceAmount(final SQLRowValues invoice) {
return BigDecimal.valueOf(invoice.getLong("NET_A_PAYER")).movePointLeft(2);
}
static private final class InvoiceElem extends Tuple2<SQLRowValues, Element> {
protected InvoiceElem(SQLRowValues a, Element b) {
super(a, b);
}
}
// Un lot est obligatoirement homogène sur les critères suivants :
// - Même type de prélèvement SEPA (index 2.11 « LocalInstrument ») : SDD Core ou
// SDD B2B
// - Même séquence de présentation (index 2.14 « SequenceType ») : FRST ou RCUR
static private final class PaymentInfo {
private final Date collectionDate;
private final String seqType;
private final List<InvoiceElem> invoices;
private final BigDecimal sum;
protected PaymentInfo(Date collectionDate, String seqType, List<InvoiceElem> invoices) {
super();
this.collectionDate = collectionDate;
if (!SEPAMandateSQLElement.SEQ_VALUES.contains(seqType))
throw new IllegalArgumentException("Invalid sequence type : " + seqType);
this.seqType = seqType;
this.invoices = invoices;
BigDecimal d = BigDecimal.ZERO;
for (final InvoiceElem invoice : invoices) {
d = d.add(getInvoiceAmount(invoice.get0()));
}
this.sum = d;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " for " + this.invoices.size() + " " + this.seqType + " invoices at " + this.collectionDate;
}
}
static private final class InvoicesByPaymentInfo {
private final Date lowerBound, upperBound;
private final NavigableMap<Date, ListMap<String, InvoiceElem>> map = new TreeMap<>();
private final Set<Number> lockedInvoicesIDs = new HashSet<>();
private BigDecimal sum = BigDecimal.ZERO;
private final Set<String> invoiceNumbers = new HashSet<>();
private final Set<String> invoiceMandates = new HashSet<>();
private final ElementCreator elemCreator;
protected InvoicesByPaymentInfo(Date lowerBound, Date upperBound, final ElementCreator elemCreator) {
super();
if (lowerBound.compareTo(upperBound) >= 0)
throw new IllegalArgumentException("Lower date after upper date : " + lowerBound + " >= " + upperBound);
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.elemCreator = elemCreator;
}
final IgnoreReason addInvoice(final SQLRowValues invoice) {
Date dueDate = ModeDeReglementSQLElement.calculDate(invoice.getForeign("ID_MODE_REGLEMENT"), invoice.getObjectAs("DATE", Date.class));
// don't ask direct debit too far in advance
if (dueDate.after(this.upperBound)) {
return IgnoreReason.TOO_FAR_IN_FUTURE;
} else if (dueDate.before(this.lowerBound)) {
dueDate = this.lowerBound;
}
final Element elem;
try {
elem = createDDTx(this.elemCreator, invoice);
} catch (MissingInfoException e) {
return IgnoreReason.MISSING_INFO;
}
// needed so that EndToEndId is unique
if (!this.invoiceNumbers.add(invoice.getString("NUMERO")))
throw new IllegalStateException("Duplicate invoice number : " + invoice);
final SQLRowAccessor mandate = invoice.getForeign("ID_MODE_REGLEMENT").getForeign("ID_SEPA_MANDATE");
if (!mandate.getBoolean("ACTIVE"))
throw new IllegalStateException("Inactive mandate for " + invoice);
// needed otherwise would have to update seqType while generating
if (!this.invoiceMandates.add(mandate.getString("MandateIdentification")))
return IgnoreReason.DUPLICATE_MANDATE;
this.lockedInvoicesIDs.add(invoice.getIDNumber());
ListMap<String, InvoiceElem> bySeqType = this.map.get(dueDate);
if (bySeqType == null) {
bySeqType = new ListMap<>();
this.map.put(dueDate, bySeqType);
}
bySeqType.add(mandate.getString("SequenceType"), new InvoiceElem(invoice, elem));
this.sum = this.sum.add(getInvoiceAmount(invoice));
return IgnoreReason.NONE;
}
public final int getTransactionCount() {
return this.lockedInvoicesIDs.size();
}
public final List<PaymentInfo> getPaymentInfos() {
final List<PaymentInfo> res = new ArrayList<>();
for (final Entry<Date, ListMap<String, InvoiceElem>> e : this.map.entrySet()) {
final Date collectionDate = e.getKey();
for (final Entry<String, List<InvoiceElem>> e2 : e.getValue().entrySet()) {
final String seqType = e2.getKey();
res.add(new PaymentInfo(collectionDate, seqType, e2.getValue()));
}
}
return res;
}
}
public static enum IgnoreReason {
NONE, TOO_FAR_IN_FUTURE, DUPLICATE_MANDATE, MISSING_INFO;
}
public static final class GenerationResult {
private final Collection<? extends Number> passedIDs;
private final List<SQLRowValues> withDDWithoutMessage;
private final ListMapItf<IgnoreReason, SQLRowValues> ignoredInvoices;
private final SQLRow insertedMessage;
private final int invoiceCount;
protected GenerationResult(Collection<? extends Number> passedIDs, List<SQLRowValues> withDDWithoutMessage, ListMap<IgnoreReason, SQLRowValues> ignoredInvoices, SQLRow insertedMessage) {
super();
this.passedIDs = passedIDs;
this.withDDWithoutMessage = Collections.unmodifiableList(withDDWithoutMessage);
assert !ignoredInvoices.containsKey(null) && !ignoredInvoices.containsKey(IgnoreReason.NONE);
this.ignoredInvoices = ListMap.unmodifiableMap(ignoredInvoices);
this.insertedMessage = insertedMessage;
this.invoiceCount = insertedMessage == null ? 0 : insertedMessage.getInt("NumberOfTransactions");
assert this.withDDWithoutMessage.size() - this.ignoredInvoices.allValues().size() == this.invoiceCount;
}
// OK since both the list and the SQLRowValues are immutable
public final List<SQLRowValues> getDDInvoicesWithoutMessage() {
return this.withDDWithoutMessage;
}
// OK since both the list and the SQLRowValues are immutable
public final ListMapItf<IgnoreReason, SQLRowValues> getIgnoredInvoices() {
return this.ignoredInvoices;
}
public final SQLRow getInsertedMessage() {
return this.insertedMessage;
}
public final int getIncludedInvoicesCount() {
return this.invoiceCount;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + ": of the " + this.passedIDs.size() + " passed, " + this.withDDWithoutMessage.size() + " needed a SDD message, of those "
+ this.ignoredInvoices.allValues().size() + " were ignored (either being too far in the future or duplicate mandates) ; the inserted row was " + this.insertedMessage;
}
}
public GenerationResult generateXML(Collection<? extends Number> invoiceIDs) throws SQLException {
final Namespace painNS = Namespace.getNamespace("urn:iso:std:iso:20022:tech:xsd:pain.008.001.02");
final Element rootElem = new Element("Document", painNS);
final Namespace xsiNS = Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
rootElem.setAttribute("schemaLocation", "urn:iso:std:iso:20022:tech:xsd:pain.008.001.02 pain.008.001.02.xsd", xsiNS);
final Document doc = new Document(rootElem);
final Element ddElem = new Element("CstmrDrctDbtInitn", painNS);
rootElem.addContent(ddElem);
// Lead Time / Time cycle
// https://www.ecb.europa.eu/paym/retpaym/undpaym/qa/html/index.en.html
final Calendar now = Calendar.getInstance();
final Calendar cal = (Calendar) now.clone();
TimeUtils.clearTime(cal);
// perhaps handle business days and difference between FRST/OOFF (5 days) and RCURR/FNAL (2
// days).
cal.add(Calendar.DAY_OF_YEAR, this.dbPrefs.getInt(getCode() + ".leadDays", 7));
final Date lowerBound = cal.getTime();
cal.setTime(now.getTime());
cal.add(Calendar.DAY_OF_YEAR, 30);
final Date upperBound = cal.getTime();
// TODO use createFetcher()
final SQLRowValuesListFetcher selSociété = SQLRowValuesListFetcher.create(this.getDirectory().getElement(this.rowSociété.getTable()).createGraph());
selSociété.setFullOnly(true);
selSociété.appendSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
@Override
public SQLSelect transformChecked(SQLSelect sel) {
sel.setLockStrength(LockStrength.SHARE);
return sel;
}
});
final SQLTable invoiceT = getDirectory().getElement(SaisieVenteFactureSQLElement.class).getTable();
final SQLField invoiceSDDMessageF = invoiceT.getField(SaisieVenteFactureSQLElement.MESSAGE_FIELD_NAME);
return SQLUtils.executeAtomic(getTable().getDBSystemRoot().getDataSource(), new ConnectionHandlerNoSetup<GenerationResult, SQLException>() {
@Override
public GenerationResult handle(SQLDataSource ds) throws SQLException {
final SQLRowValues lockedSociété = selSociété.fetchOne(SDDMessageSQLElement.this.rowSociété.getIDNumber());
if (lockedSociété == null)
throw new IllegalStateException("Missing société " + SDDMessageSQLElement.this.rowSociété);
// find and lock invoices with TYPE_REGLEMENT direct debit and no message
final SQLRowValues invoiceVals = new SQLRowValues(invoiceT);
invoiceVals.putRowValues("ID_CLIENT").putNulls("NOM", "BIC", "IBAN");
invoiceVals.putRowValues("ID_MODE_REGLEMENT").putNulls("AJOURS", "LENJOUR").putRowValues("ID_SEPA_MANDATE").setAllToNull();
invoiceVals.putNulls("NET_A_PAYER", "DATE", "NUMERO");
final SQLRowValuesListFetcher fetcher = SQLRowValuesListFetcher.create(invoiceVals);
fetcher.setReturnedRowsUnmodifiable(true);
// required for locking rows and to make sure that there's a SEPA Mandate
fetcher.setFullOnly(true);
fetcher.appendSelTransf(new ITransformer<SQLSelect, SQLSelect>() {
@Override
public SQLSelect transformChecked(SQLSelect sel) {
// we will update FACTURE.ID_MESSAGE
sel.setLockStrength(LockStrength.UPDATE);
final SQLSelectJoin join = sel.getJoin(invoiceT.getField("ID_MODE_REGLEMENT"));
join.setWhere(new Where(join.getJoinedTable().getField("ID_TYPE_REGLEMENT"), "=", TypeReglementSQLElement.PRELEVEMENT));
return sel;
}
});
final List<SQLRowValues> ddInvoices = fetcher
.fetch(new Where(invoiceT.getKey(), invoiceIDs).and(new Where(invoiceSDDMessageF, "=", invoiceSDDMessageF.getForeignTable().getUndefinedIDNumber())));
final InvoicesByPaymentInfo map = new InvoicesByPaymentInfo(lowerBound, upperBound, new ElementCreator(painNS, SDDMessageSQLElement.this.fieldTranslator));
final ListMap<IgnoreReason, SQLRowValues> ignoredInvoices = new ListMap<>();
for (final SQLRowValues invoice : ddInvoices) {
final IgnoreReason ignoredReason = map.addInvoice(invoice);
if (ignoredReason != IgnoreReason.NONE) {
ignoredInvoices.add(ignoredReason, invoice);
}
}
final SQLRow newMsg;
final int txCount = map.getTransactionCount();
if (txCount == 0) {
newMsg = null;
} else {
// find and lock message serial
final SQLTable mdT = getTable().getTable(SQLSchema.METADATA_TABLENAME);
final SQLSelect sel = new SQLSelect(true).addSelect(mdT.getField("VALUE"));
sel.setWhere(new Where(mdT.getField("NAME"), "=", SERIAL_MD));
sel.setLockStrength(LockStrength.UPDATE);
final String msgSerial = String.valueOf(Integer.parseInt((String) getTable().getDBSystemRoot().getDataSource().executeScalar(sel.asString())) + 1);
// generate XML
final BigDecimal totalSum = map.sum;
final Element groupHeaderElem = createGroupHeader(painNS, lockedSociété, now, msgSerial, txCount, totalSum);
ddElem.addContent(groupHeaderElem);
final String msgID = groupHeaderElem.getChild("MsgId", painNS).getText();
final Map<SQLRow, String> end2endIDs = new HashMap<>();
int index = 1;
for (final PaymentInfo info : map.getPaymentInfos()) {
try {
createPaymentInfo(ddElem, end2endIDs, map.elemCreator, lockedSociété, info, msgID, index++);
} catch (Exception e) {
throw new IllegalStateException("Couldn't create XML for " + info, e);
}
}
assert end2endIDs.size() == txCount : "Expected " + txCount + " transactions but got " + end2endIDs.size() + " rows : " + end2endIDs;
// insert message in DB
final SQLRowValues msgVals = new SQLRowValues(getTable());
msgVals.put("MessageIdentification", msgID);
msgVals.put("CreationDateTime", now.getTime());
msgVals.put("NumberOfTransactions", txCount);
msgVals.put("ControlSum", totalSum);
msgVals.put("XML", JDOM2Utils.output(doc));
newMsg = msgVals.insert();
// update invoices with new message
for (final Entry<SQLRow, String> e : end2endIDs.entrySet()) {
final SQLRowValues vals = e.getKey().createEmptyUpdateRow();
vals.putForeignID(invoiceSDDMessageF.getName(), newMsg);
vals.put(SaisieVenteFactureSQLElement.END2END_FIELD_NAME, e.getValue());
vals.update();
}
// update message serial
getTable().getDBRoot().setMetadata(SERIAL_MD, msgSerial);
}
return new GenerationResult(invoiceIDs, ddInvoices, ignoredInvoices, newMsg);
}
});
}
static private Element createGroupHeader(final Namespace painNS, final SQLRowValues lockedSociété, final Calendar now, final String msgSerial, final int txCount, final BigDecimal total) {
final Element res = new Element("GrpHdr", painNS);
res.addContent(new Element("MsgId", painNS).setText("openconcerto-" + now.get(Calendar.YEAR) + "-" + msgSerial));
res.addContent(new Element("CreDtTm", painNS).setText(new XMLDateFormat().format(now)));
res.addContent(new Element("NbOfTxs", painNS).setText(String.valueOf(txCount)));
if (DecimalUtils.decimalDigits(total) > 2)
throw new IllegalArgumentException("Too many decimals : " + total);
res.addContent(new Element("CtrlSum", painNS).setText(total.toPlainString()));
res.addContent(new Element("InitgPty", painNS).addContent(new Element("Nm", painNS).setText(getCompanyName(lockedSociété))));
return res;
}
static private String getCompanyName(final SQLRowValues lockedSociété) {
final String companyName = lockedSociété.getString("NOM");
if (StringUtils.isEmpty(companyName))
throw new IllegalStateException("Empty company name : " + lockedSociété);
return lockedSociété.getString("TYPE") + ' ' + companyName;
}
static private final DateFormat XML_DATE_FMT = new SimpleDateFormat("yyyy-MM-dd");
static synchronized private final String formatDate(final Date date) {
return XML_DATE_FMT.format(date);
}
static private void createPaymentInfo(Element ddElem, Map<SQLRow, String> end2endIDs, final ElementCreator elemCreator, final SQLRowValues lockedSociété, final PaymentInfo info,
final String msgID, final int index) throws SQLException, MissingInfoException {
if (info.invoices.isEmpty())
return;
final Namespace painNS = elemCreator.painNS;
final String formattedDate = formatDate(info.collectionDate);
final Element res = new Element("PmtInf", painNS);
res.addContent(new Element("PmtInfId", painNS).setText("openconcerto-" + formattedDate + '.' + index));
res.addContent(new Element("PmtMtd", painNS).setText("DD"));
res.addContent(new Element("BtchBookg", painNS).setText("false"));
res.addContent(new Element("NbOfTxs", painNS).setText(String.valueOf(info.invoices.size())));
if (DecimalUtils.decimalDigits(info.sum) > 2)
throw new IllegalArgumentException("Too many decimals : " + info.sum);
res.addContent(new Element("CtrlSum", painNS).setText(info.sum.toPlainString()));
final Element typeInformation = new Element("PmtTpInf", painNS);
typeInformation.addContent(new Element("SvcLvl", painNS).addContent(new Element("Cd", painNS).setText("SEPA")));
typeInformation.addContent(new Element("LclInstrm", painNS).addContent(new Element("Cd", painNS).setText("CORE")));
typeInformation.addContent(new Element("SeqTp", painNS).setText(info.seqType));
res.addContent(typeInformation);
res.addContent(new Element("ReqdColltnDt", painNS).setText(formattedDate));
final Element creditor = new Element("Cdtr", painNS);
creditor.addContent(new Element("Nm", painNS).setText(getCompanyName(lockedSociété)));
final Element postalAddr = new Element("PstlAdr", painNS);
final SQLRowAccessor addr = lockedSociété.getNonEmptyForeign("ID_ADRESSE_COMMON");
final String country = addr.getString("PAYS");
final String country2;
if (StringUtils.isEmpty(country, true) || country.trim().equalsIgnoreCase("France")) {
country2 = "FR";
} else {
// TODO map to 2 letter code
throw new IllegalStateException("Unknown country : " + country);
}
postalAddr.addContent(new Element("Ctry", painNS).setText(country2));
postalAddr.addContent(new Element("AdrLine", painNS).setText(addr.getString("RUE")));
postalAddr.addContent(new Element("AdrLine", painNS).setText(addr.getString("CODE_POSTAL") + " " + addr.getString("VILLE")));
creditor.addContent(postalAddr);
res.addContent(creditor);
final Element creditorAccount = new Element("CdtrAcct", painNS);
creditorAccount.addContent(new Element("Id", painNS).addContent(elemCreator.createWithNonEmptyText("IBAN", lockedSociété, "IBAN")));
res.addContent(creditorAccount);
final Element creditorAgent = new Element("CdtrAgt", painNS);
creditorAgent.addContent(new Element("FinInstnId", painNS).addContent(elemCreator.createWithNonEmptyText("BIC", lockedSociété, "BIC")));
res.addContent(creditorAgent);
res.addContent(new Element("ChrgBr", painNS).setText("SLEV"));
final Element creditorID = new Element("CdtrSchmeId", painNS);
final Element other = new Element("Othr", painNS);
other.addContent(elemCreator.createWithNonEmptyText("Id", lockedSociété, "SEPA_CREDITOR_ID"));
other.addContent(new Element("SchmeNm", painNS).addContent(new Element("Prtry", painNS).setText("SEPA")));
creditorID.addContent(new Element("Id", painNS).addContent(new Element("PrvtId", painNS).addContent(other)));
res.addContent(creditorID);
for (final InvoiceElem invoice : info.invoices) {
final String end2endID = msgID + '.' + invoice.get0().getString("NUMERO");
if (end2endIDs.put(invoice.get0().asRow(), end2endID) != null)
throw new IllegalStateException("Duplicate invoice : " + invoice);
try {
res.addContent(fillDDTx(invoice, elemCreator, end2endID));
} catch (Exception e) {
throw new IllegalStateException("Couldn't create XML for " + invoice, e);
}
}
ddElem.addContent(res);
}
static private Element fillDDTx(final InvoiceElem invoiceElem, final ElementCreator elemCreator, final String end2endID) throws SQLException, MissingInfoException {
final Element paymentId = new Element("PmtId", elemCreator.painNS);
paymentId.addContent(elemCreator.createWithNonEmptyText("InstrId", end2endID));
paymentId.addContent(elemCreator.createWithNonEmptyText("EndToEndId", end2endID));
invoiceElem.get1().addContent(0, paymentId);
// update mandate fields
final SQLRowAccessor mandate = invoiceElem.get0().getForeign("ID_MODE_REGLEMENT").getForeign("ID_SEPA_MANDATE");
final String seqType = mandate.getString("SequenceType");
if (seqType.equals(SEPAMandateSQLElement.SEQ_FIRST)) {
mandate.createEmptyUpdateRow().put("SequenceType", SEPAMandateSQLElement.SEQ_RECURRENT).update();
} else if (seqType.equals(SEPAMandateSQLElement.SEQ_FINAL) || seqType.equals(SEPAMandateSQLElement.SEQ_ONEOFF)) {
mandate.createEmptyUpdateRow().put("ACTIVE", Boolean.FALSE).update();
} // else SEQ_RECURRENT
return invoiceElem.get1();
}
static private Element createDDTx(final ElementCreator elemCreator, final SQLRowValues invoice) throws MissingInfoException {
final Namespace painNS = elemCreator.painNS;
final Element res = new Element("DrctDbtTxInf", painNS);
res.addContent(new Element("InstdAmt", painNS).setAttribute("Ccy", "EUR").setText(getInvoiceAmount(invoice).toPlainString()));
final Element mandateRltdInfo = new Element("MndtRltdInf", painNS);
final SQLRowAccessor mandate = invoice.getForeign("ID_MODE_REGLEMENT").getForeign("ID_SEPA_MANDATE");
assert !mandate.isUndefined() : "Undefined mandate returned by fetcher";
mandateRltdInfo.addContent(elemCreator.createWithNonEmptyText("MndtId", mandate, "MandateIdentification"));
mandateRltdInfo.addContent(elemCreator.createWithNonEmptyText("DtOfSgntr", formatDate(mandate.getObjectAs("DateOfSignature", Date.class))));
mandateRltdInfo.addContent(elemCreator.createWithNonEmptyText("AmdmntInd", "false"));
res.addContent(new Element("DrctDbtTx", painNS).addContent(mandateRltdInfo));
final SQLRowAccessor clientRow = invoice.getForeign("ID_CLIENT");
res.addContent(new Element("DbtrAgt", painNS).addContent(new Element("FinInstnId", painNS).addContent(elemCreator.createWithNonEmptyText("BIC", clientRow, "BIC"))));
res.addContent(new Element("Dbtr", painNS).addContent(elemCreator.createWithNonEmptyText("Nm", clientRow, "NOM")));
res.addContent(new Element("DbtrAcct", painNS).addContent(new Element("Id", painNS).addContent(elemCreator.createWithNonEmptyText("IBAN", clientRow, "IBAN"))));
res.addContent(new Element("Purp", painNS).addContent(new Element("Cd", painNS).setText("OTHR")));
// TODO <RmtInf><Ustrd>ligne de facture avec n° d'abonnement et indice de paiement.
return res;
}
static public final class MissingInfoException extends Exception {
private final String label;
protected MissingInfoException(final String label) {
super("Empty " + label);
this.label = label;
}
public final String getLabel() {
return this.label;
}
}
static private final class ElementCreator {
private final Namespace painNS;
private final SQLFieldTranslator fieldTrans;
protected ElementCreator(Namespace painNS, final SQLFieldTranslator fieldTrans) {
super();
this.painNS = painNS;
this.fieldTrans = fieldTrans;
}
protected Element create(final String elemName) {
return new Element(elemName, this.painNS);
}
protected Element createWithNonEmptyText(final String elemName, final SQLRowAccessor r, final String field) throws MissingInfoException {
return this.createWithNonEmptyText(elemName, r.getString(field), this.fieldTrans.getDescFor(r.getTable(), field).getLabel());
}
protected Element createWithNonEmptyText(final String elemName, final String text) throws MissingInfoException {
return this.createWithNonEmptyText(elemName, text, null);
}
protected Element createWithNonEmptyText(final String elemName, final String text, final String label) throws MissingInfoException {
if (StringUtils.isEmpty(text))
throw new MissingInfoException(label == null ? elemName : label);
return create(elemName).setText(text);
}
}
}