/**
* Axelor Business Solutions
*
* Copyright (C) 2016 Axelor (<http://axelor.com>).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.axelor.apps.account.service;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import javax.xml.bind.JAXBException;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.axelor.apps.account.db.AccountConfig;
import com.axelor.apps.account.db.Invoice;
import com.axelor.apps.account.db.Move;
import com.axelor.apps.account.db.MoveLine;
import com.axelor.apps.account.db.Reconcile;
import com.axelor.apps.account.db.Reimbursement;
import com.axelor.apps.account.db.repo.MoveLineRepository;
import com.axelor.apps.account.db.repo.MoveRepository;
import com.axelor.apps.account.db.repo.ReimbursementRepository;
import com.axelor.apps.account.exception.IExceptionMessage;
import com.axelor.apps.account.service.config.AccountConfigService;
import com.axelor.apps.account.service.move.MoveLineService;
import com.axelor.apps.account.service.move.MoveService;
import com.axelor.apps.base.db.BankDetails;
import com.axelor.apps.base.db.Company;
import com.axelor.apps.base.db.IAdministration;
import com.axelor.apps.base.db.Partner;
import com.axelor.apps.base.db.repo.PartnerRepository;
import com.axelor.apps.base.service.PartnerService;
import com.axelor.apps.base.service.administration.GeneralService;
import com.axelor.apps.base.service.administration.GeneralServiceImpl;
import com.axelor.apps.base.service.administration.SequenceService;
import com.axelor.apps.tool.xml.Marschaller;
import com.axelor.apps.xsd.sepa.AccountIdentification3Choice;
import com.axelor.apps.xsd.sepa.AmountType2Choice;
import com.axelor.apps.xsd.sepa.BranchAndFinancialInstitutionIdentification3;
import com.axelor.apps.xsd.sepa.CashAccount7;
import com.axelor.apps.xsd.sepa.CreditTransferTransactionInformation1;
import com.axelor.apps.xsd.sepa.CurrencyAndAmount;
import com.axelor.apps.xsd.sepa.Document;
import com.axelor.apps.xsd.sepa.FinancialInstitutionIdentification5Choice;
import com.axelor.apps.xsd.sepa.GroupHeader1;
import com.axelor.apps.xsd.sepa.Grouping1Code;
import com.axelor.apps.xsd.sepa.ObjectFactory;
import com.axelor.apps.xsd.sepa.Pain00100102;
import com.axelor.apps.xsd.sepa.PartyIdentification8;
import com.axelor.apps.xsd.sepa.PaymentIdentification1;
import com.axelor.apps.xsd.sepa.PaymentInstructionInformation1;
import com.axelor.apps.xsd.sepa.PaymentMethod3Code;
import com.axelor.apps.xsd.sepa.PaymentTypeInformation1;
import com.axelor.apps.xsd.sepa.RemittanceInformation1;
import com.axelor.apps.xsd.sepa.ServiceLevel1Code;
import com.axelor.apps.xsd.sepa.ServiceLevel2Choice;
import com.axelor.exception.AxelorException;
import com.axelor.exception.db.IException;
import com.axelor.i18n.I18n;
import com.axelor.inject.Beans;
import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
public class ReimbursementExportService {
private final Logger log = LoggerFactory.getLogger( getClass() );
protected MoveService moveService;
protected MoveRepository moveRepo;
protected MoveLineService moveLineService;
protected ReconcileService reconcileService;
protected SequenceService sequenceService;
protected AccountBlockingService accountBlockingService;
protected ReimbursementRepository reimbursementRepo;
protected AccountConfigService accountConfigService;
protected PartnerService partnerService;
protected PartnerRepository partnerRepository;
protected LocalDate today;
@Inject
public ReimbursementExportService(MoveService moveService, MoveRepository moveRepo, MoveLineService moveLineService, ReconcileService reconcileService,
SequenceService sequenceService, AccountBlockingService accountBlockingService, ReimbursementRepository reimbursementRepo, AccountConfigService accountConfigService,
PartnerService partnerService, GeneralService generalService, PartnerRepository partnerRepository) {
this.moveService = moveService;
this.moveRepo = moveRepo;
this.moveLineService = moveLineService;
this.reconcileService = reconcileService;
this.sequenceService = sequenceService;
this.accountBlockingService = accountBlockingService;
this.reimbursementRepo = reimbursementRepo;
this.accountConfigService = accountConfigService;
this.partnerService = partnerService;
this.partnerRepository = partnerRepository;
this.today = generalService.getTodayDate();
}
/**
*
* @param reimbursementExport
*/
public void fillMoveLineSet(Reimbursement reimbursement, List<MoveLine> moveLineList, BigDecimal total) {
log.debug("In fillMoveLineSet");
log.debug("Nombre de trop-perçus trouvés : {}", moveLineList.size());
for(MoveLine moveLine : moveLineList) {
// On passe les lignes d'écriture (trop perçu) à l'état 'en cours de remboursement'
moveLine.setReimbursementStatusSelect(MoveLineRepository.REIMBURSEMENT_STATUS_REIMBURSING);
}
reimbursement.setMoveLineSet(new HashSet<MoveLine>());
reimbursement.getMoveLineSet().addAll(moveLineList);
log.debug("End fillMoveLineSet");
}
/**
*
* @param reimbursementExport
* @throws AxelorException
*/
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
public Reimbursement runCreateReimbursement(List<MoveLine> moveLineList, Company company, Partner partner) throws AxelorException {
log.debug("In runReimbursementProcess");
BigDecimal total = this.getTotalAmountRemaining(moveLineList);
AccountConfig accountConfig = company.getAccountConfig();
// Seuil bas respecté et remboursement manuel autorisé
if(total.compareTo(accountConfig.getLowerThresholdReimbursement()) > 0 ) {
Reimbursement reimbursement = createReimbursement(partner, company);
fillMoveLineSet(reimbursement, moveLineList, total);
if(total.compareTo(accountConfig.getUpperThresholdReimbursement()) > 0 || reimbursement.getBankDetails() == null) {
// Seuil haut dépassé
reimbursement.setStatusSelect(ReimbursementRepository.STATUS_TO_VALIDATE);
}
else {
reimbursement.setStatusSelect(ReimbursementRepository.STATUS_VALIDATED);
}
reimbursementRepo.save(reimbursement);
return reimbursement;
}
log.debug("End runReimbursementProcess");
return null;
}
/**
* Fonction permettant de calculer le montant total restant à payer / à lettrer
* @param movelineList
* Une liste de ligne d'écriture
* @return
* Le montant total restant à payer / à lettrer
*/
public BigDecimal getTotalAmountRemaining(List<MoveLine> moveLineList) {
BigDecimal total = BigDecimal.ZERO;
for(MoveLine moveLine : moveLineList) {
total=total.add(moveLine.getAmountRemaining());
}
log.debug("Total Amount Remaining : {}",total);
return total;
}
/**
* Methode permettant de créer l'écriture de remboursement
* @param reimbursementExport
* Un objet d'export des prélèvements
* @throws AxelorException
*/
public void createReimbursementMove(Reimbursement reimbursement, Company company) throws AxelorException {
reimbursement = reimbursementRepo.find(reimbursement.getId());
Partner partner = null;
Move newMove = null;
boolean first = true;
AccountConfig accountConfig = company.getAccountConfig();
if(reimbursement.getMoveLineSet() != null && !reimbursement.getMoveLineSet().isEmpty()) {
int seq = 1;
for(MoveLine moveLine : reimbursement.getMoveLineSet()) {
BigDecimal amountRemaining = moveLine.getAmountRemaining();
if(amountRemaining.compareTo(BigDecimal.ZERO) > 0) {
partner = moveLine.getPartner();
// On passe les lignes d'écriture (trop perçu) à l'état 'remboursé'
moveLine.setReimbursementStatusSelect(MoveLineRepository.REIMBURSEMENT_STATUS_REIMBURSED);
if(first) {
newMove = moveService.getMoveCreateService().createMove(accountConfig.getReimbursementJournal(), company, null, partner, null);
first = false;
}
// Création d'une ligne au débit
MoveLine newDebitMoveLine = moveLineService.createMoveLine(newMove , partner, moveLine.getAccount(), amountRemaining, true, today, seq, null);
newMove.getMoveLineList().add(newDebitMoveLine);
if(reimbursement.getDescription() != null && !reimbursement.getDescription().isEmpty()) {
newDebitMoveLine.setDescription(reimbursement.getDescription());
}
seq++;
//Création de la réconciliation
Reconcile reconcile = reconcileService.createReconcile(newDebitMoveLine, moveLine, amountRemaining, false);
reconcileService.confirmReconcile(reconcile);
}
}
// Création de la ligne au crédit
MoveLine newCreditMoveLine = moveLineService.createMoveLine(newMove, partner, accountConfig.getReimbursementAccount(), reimbursement.getAmountReimbursed(), false, today, seq, null);
newMove.getMoveLineList().add(newCreditMoveLine);
if(reimbursement.getDescription() != null && !reimbursement.getDescription().isEmpty()) {
newCreditMoveLine.setDescription(reimbursement.getDescription());
}
moveService.getMoveValidateService().validateMove(newMove);
moveRepo.save(newMove);
}
}
/**
* Procédure permettant de tester la présence des champs et des séquences nécessaire aux remboursements.
*
* @param company
* Une société
* @throws AxelorException
*/
public void testCompanyField(Company company) throws AxelorException {
AccountConfig accountConfig = accountConfigService.getAccountConfig(company);
accountConfigService.getReimbursementAccount(accountConfig);
accountConfigService.getReimbursementJournal(accountConfig);
accountConfigService.getReimbursementExportFolderPath(accountConfig);
if(!sequenceService.hasSequence(IAdministration.REIMBOURSEMENT, company)) {
throw new AxelorException(String.format(I18n.get(IExceptionMessage.REIMBURSEMENT_1),
GeneralServiceImpl.EXCEPTION,company.getName()), IException.CONFIGURATION_ERROR);
}
}
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
public void reimburse(Reimbursement reimbursement, Company company) throws AxelorException {
reimbursement.setAmountReimbursed(reimbursement.getAmountToReimburse());
this.createReimbursementMove(reimbursement, company);
reimbursement.setStatusSelect(ReimbursementRepository.STATUS_REIMBURSED);
reimbursementRepo.save(reimbursement);
}
/**
* Méthode permettant de créer un remboursement
* @param partner
* Un tiers
* @param company
* Une société
* @param reimbursementExport
* Un export des remboursement
* @return
* Le remboursmeent créé
* @throws AxelorException
*/
public Reimbursement createReimbursement(Partner partner, Company company) throws AxelorException {
Reimbursement reimbursement = new Reimbursement();
reimbursement.setPartner(partner);
BankDetails bankDetails = partnerService.getDefaultBankDetails(partner);
reimbursement.setBankDetails(bankDetails);
reimbursement.setRef(sequenceService.getSequenceNumber(IAdministration.REIMBOURSEMENT, company));
return reimbursement;
}
/**
* Le tiers peux t-il être remboursé ?
* Si le tiers est bloqué en remboursement et que la date de fin de blocage n'est pas passée alors on ne peut pas rembourser.
*
* @return
*/
public boolean canBeReimbursed(Partner partner, Company company){
return !accountBlockingService.isReminderBlocking(partner, company);
}
/**
* Procédure permettant de mettre à jour la liste des RIBs du tiers
* @param reimbursement
* Un remboursement
*/
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
public void updatePartnerCurrentRIB(Reimbursement reimbursement) {
BankDetails bankDetails = reimbursement.getBankDetails();
Partner partner = reimbursement.getPartner();
BankDetails defaultBankDetails = partnerService.getDefaultBankDetails(partner);
if(partner != null && bankDetails != null && !bankDetails.equals(defaultBankDetails)) {
bankDetails.setIsDefault(true);
defaultBankDetails.setIsDefault(false);
partner.addBankDetailsListItem(bankDetails);
partnerRepository.save(partner);
}
}
/**
* Méthode permettant de créer un fichier xml de virement au format SEPA
* @param export
* @param dateTime
* @param reimbursementList
* @throws AxelorException
* @throws DatatypeConfigurationException
* @throws JAXBException
* @throws IOException
*/
public void exportSepa(Company company, DateTime dateTime, List<Reimbursement> reimbursementList, BankDetails bankDetails) throws AxelorException, DatatypeConfigurationException, JAXBException, IOException {
String exportFolderPath = accountConfigService.getReimbursementExportFolderPath(accountConfigService.getAccountConfig(company));
if (exportFolderPath == null) {
throw new AxelorException(String.format(I18n.get(IExceptionMessage.REIMBURSEMENT_2),
company.getName()), IException.MISSING_FIELD);
}
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
DatatypeFactory datatypeFactory = DatatypeFactory.newInstance();
Date date = dateTime.toDate();
BigDecimal ctrlSum = BigDecimal.ZERO;
int nbOfTxs = 0;
/**
* Création du documemnt XML en mémoire
*/
ObjectFactory factory = new ObjectFactory();
// Débit
// Paiement
ServiceLevel2Choice svcLvl = factory.createServiceLevel2Choice();
svcLvl.setCd(ServiceLevel1Code.SEPA);
PaymentTypeInformation1 pmtTpInf = factory.createPaymentTypeInformation1();
pmtTpInf.setSvcLvl(svcLvl);
// Payeur
PartyIdentification8 dbtr = factory.createPartyIdentification8();
dbtr.setNm(bankDetails.getOwnerName());
// IBAN
AccountIdentification3Choice iban = factory.createAccountIdentification3Choice();
iban.setIBAN(bankDetails.getIban());
CashAccount7 dbtrAcct = factory.createCashAccount7();
dbtrAcct.setId(iban);
// BIC
FinancialInstitutionIdentification5Choice finInstnId = factory.createFinancialInstitutionIdentification5Choice();
finInstnId.setBIC(bankDetails.getBic());
BranchAndFinancialInstitutionIdentification3 dbtrAgt = factory.createBranchAndFinancialInstitutionIdentification3();
dbtrAgt.setFinInstnId(finInstnId);
// Lot
PaymentInstructionInformation1 pmtInf = factory.createPaymentInstructionInformation1();
pmtInf.setPmtMtd(PaymentMethod3Code.TRF);
pmtInf.setPmtTpInf(pmtTpInf);
pmtInf.setReqdExctnDt(datatypeFactory.newXMLGregorianCalendar(dateFormat.format(date)));
pmtInf.setDbtr(dbtr);
pmtInf.setDbtrAcct(dbtrAcct);
pmtInf.setDbtrAgt(dbtrAgt);
// Crédit
CreditTransferTransactionInformation1 cdtTrfTxInf = null; PaymentIdentification1 pmtId = null;
AmountType2Choice amt = null; CurrencyAndAmount instdAmt = null;
PartyIdentification8 cbtr = null; CashAccount7 cbtrAcct = null;
BranchAndFinancialInstitutionIdentification3 cbtrAgt = null;
RemittanceInformation1 rmtInf = null;
for (Reimbursement reimbursement : reimbursementList){
reimbursement = reimbursementRepo.find(reimbursement.getId());
nbOfTxs++;
ctrlSum = ctrlSum.add(reimbursement.getAmountReimbursed());
bankDetails = reimbursement.getBankDetails();
// Paiement
pmtId = factory.createPaymentIdentification1();
pmtId.setEndToEndId(reimbursement.getRef());
// Montant
instdAmt = factory.createCurrencyAndAmount();
instdAmt.setCcy("EUR");
instdAmt.setValue(reimbursement.getAmountReimbursed());
amt = factory.createAmountType2Choice();
amt.setInstdAmt(instdAmt);
// Débiteur
cbtr = factory.createPartyIdentification8();
cbtr.setNm(bankDetails.getOwnerName());
// IBAN
iban = factory.createAccountIdentification3Choice();
iban.setIBAN(bankDetails.getIban());
cbtrAcct = factory.createCashAccount7();
cbtrAcct.setId(iban);
// BIC
finInstnId = factory.createFinancialInstitutionIdentification5Choice();
finInstnId.setBIC(bankDetails.getBic());
cbtrAgt = factory.createBranchAndFinancialInstitutionIdentification3();
cbtrAgt.setFinInstnId(finInstnId);
rmtInf = factory.createRemittanceInformation1();
rmtInf.getUstrd().add(reimbursement.getDescription());
// Transaction
cdtTrfTxInf = factory.createCreditTransferTransactionInformation1();
cdtTrfTxInf.setPmtId(pmtId);
cdtTrfTxInf.setAmt(amt);
cdtTrfTxInf.setCdtr(cbtr);
cdtTrfTxInf.setCdtrAcct(cbtrAcct);
cdtTrfTxInf.setCdtrAgt(cbtrAgt);
cdtTrfTxInf.setRmtInf(rmtInf);
pmtInf.getCdtTrfTxInf().add(cdtTrfTxInf);
}
// En-tête
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
GroupHeader1 grpHdr = factory.createGroupHeader1();
grpHdr.setCreDtTm(datatypeFactory.newXMLGregorianCalendar(dateFormat.format(date)));
grpHdr.setNbOfTxs(Integer.toString(nbOfTxs));
grpHdr.setCtrlSum(ctrlSum);
grpHdr.setGrpg(Grouping1Code.MIXD);
grpHdr.setInitgPty(dbtr);
// Parent
Pain00100102 pain00100102 = factory.createPain00100102();
pain00100102.setGrpHdr(grpHdr);
pain00100102.getPmtInf().add(pmtInf);
// Document
Document xml = factory.createDocument();
xml.setPain00100102(pain00100102);
/**
* Création du documemnt XML physique
*/
Marschaller.marschalFile(factory.createDocument(xml), "com.axelor.apps.xsd.sepa",
exportFolderPath, String.format("%s.xml", dateFormat.format(date)));
}
/************************* Remboursement lors d'une facture fin de cycle *********************************/
/**
* Procédure permettant de créer un remboursement si un trop perçu est généré à la facture fin de cycle
* @param partner
* Un tiers
* @param company
* Une société
* @param moveLine
* Un trop-perçu
* @throws AxelorException
*/
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
public void createReimbursementInvoice(Partner partner, Company company, List<? extends MoveLine> moveLineList) throws AxelorException {
BigDecimal total = this.getTotalAmountRemaining((List<MoveLine>) moveLineList);
if(total.compareTo(BigDecimal.ZERO) > 0) {
this.testCompanyField(company);
Reimbursement reimbursement = this.createReimbursement(partner, company);
this.fillMoveLineSet(reimbursement, (List<MoveLine>) moveLineList, total);
if(total.compareTo(company.getAccountConfig().getUpperThresholdReimbursement()) > 0 || reimbursement.getBankDetails() == null) {
// Seuil haut dépassé
reimbursement.setStatusSelect(ReimbursementRepository.STATUS_TO_VALIDATE);
}
else {
// Seuil haut non dépassé
reimbursement.setStatusSelect(ReimbursementRepository.STATUS_VALIDATED);
}
reimbursementRepo.save(reimbursement);
}
}
/**
* Procédure permettant de créer un remboursement si un trop perçu est généré à la facture fin de cycle grand comptes
* @param invoice
* Une facture
* @throws AxelorException
*/
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
public void createReimbursementInvoice(Invoice invoice) throws AxelorException {
Company company = invoice.getCompany();
Partner partner = invoice.getPartner();
MoveLineRepository moveLineRepo = Beans.get(MoveLineRepository.class);
// récupération des trop-perçus du tiers
List<? extends MoveLine> moveLineList = moveLineRepo.all().filter("self.account.reconcileOk = 'true' AND self.fromSchedulePaymentOk = 'false' " +
"AND self.move.statusSelect = ?1 AND self.amountRemaining > 0 AND self.credit > 0 AND self.partner = ?2 AND self.reimbursementStatusSelect = ?3 ",
MoveRepository.STATUS_VALIDATED , partner, MoveLineRepository.REIMBURSEMENT_STATUS_NULL).fetch();
this.createReimbursementInvoice(partner, company, moveLineList);
}
}