/**
* 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.debtrecovery;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
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.AccountingSituation;
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.PaymentScheduleLine;
import com.axelor.apps.account.db.Reminder;
import com.axelor.apps.account.db.repo.AccountingSituationRepository;
import com.axelor.apps.account.db.repo.MoveLineRepository;
import com.axelor.apps.account.db.repo.PaymentScheduleLineRepository;
import com.axelor.apps.account.db.repo.ReminderRepository;
import com.axelor.apps.account.exception.IExceptionMessage;
import com.axelor.apps.account.service.AccountCustomerService;
import com.axelor.apps.account.service.config.AccountConfigService;
import com.axelor.apps.base.db.Company;
import com.axelor.apps.base.db.Partner;
import com.axelor.apps.base.service.administration.GeneralService;
import com.axelor.apps.base.service.administration.GeneralServiceImpl;
import com.axelor.apps.tool.date.DateTool;
import com.axelor.exception.AxelorException;
import com.axelor.exception.db.IException;
import com.axelor.exception.service.TraceBackService;
import com.axelor.i18n.I18n;
import com.axelor.inject.Beans;
import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
public class ReminderService {
private final Logger log = LoggerFactory.getLogger( getClass() );
protected ReminderSessionService reminderSessionService;
protected ReminderActionService reminderActionService;
protected AccountCustomerService accountCustomerService;
protected MoveLineRepository moveLineRepo;
protected PaymentScheduleLineRepository paymentScheduleLineRepo;
protected AccountConfigService accountConfigService;
protected ReminderRepository reminderRepo;
protected LocalDate today;
@Inject
public ReminderService(ReminderSessionService reminderSessionService, ReminderActionService reminderActionService, AccountCustomerService accountCustomerService,
MoveLineRepository moveLineRepo, PaymentScheduleLineRepository paymentScheduleLineRepo, AccountConfigService accountConfigService, ReminderRepository reminderRepo,
GeneralService generalService) {
this.reminderSessionService = reminderSessionService;
this.reminderActionService = reminderActionService;
this.accountCustomerService = accountCustomerService;
this.moveLineRepo = moveLineRepo;
this.paymentScheduleLineRepo = paymentScheduleLineRepo;
this.accountConfigService = accountConfigService;
this.reminderRepo = reminderRepo;
this.today = generalService.getTodayDate();
}
public void testCompanyField(Company company) throws AxelorException {
AccountConfig accountConfig = accountConfigService.getAccountConfig(company);
accountConfigService.getReminderConfigLineList(accountConfig);
}
/**
* Fonction permettant de calculer le solde exigible relançable d'un tiers
* @param reminder
* Une relance
* @return
* Le solde exigible relançable
*/
public BigDecimal getBalanceDueReminder(List<MoveLine> moveLineList, Partner partner) {
BigDecimal balanceSubstract = this.getSubstractBalanceDue(partner);
BigDecimal balanceDueReminder = BigDecimal.ZERO;
for(MoveLine moveLine : moveLineList) {
balanceDueReminder = balanceDueReminder.add(moveLine.getAmountRemaining());
}
balanceDueReminder = balanceDueReminder.add(balanceSubstract);
return balanceDueReminder;
}
public BigDecimal getSubstractBalanceDue( Partner partner) {
List<? extends MoveLine> moveLineQuery = moveLineRepo.all().filter("self.partner = ?1", partner).fetch();
BigDecimal balance = BigDecimal.ZERO;
for(MoveLine moveLine : moveLineQuery) {
if(moveLine.getCredit().compareTo(BigDecimal.ZERO) > 0) {
if(moveLine.getAccount()!=null && moveLine.getAccount().getReconcileOk()) {
balance = balance.subtract(moveLine.getAmountRemaining());
}
}
}
return balance;
}
/**
* Fonction qui récupère la plus ancienne date d'échéance d'une liste de lignes d'écriture
* @param moveLineList
* Une liste de lignes d'écriture
* @return
* la plus ancienne date d'échéance
*/
public LocalDate getOldDateMoveLine(List<MoveLine> moveLineList) {
LocalDate minMoveLineDate = new LocalDate();
if(moveLineList != null && !moveLineList.isEmpty()) {
for(MoveLine moveLine : moveLineList) {
if(minMoveLineDate.isAfter(moveLine.getDueDate())) { minMoveLineDate=moveLine.getDueDate(); }
}
}
else { minMoveLineDate=null; }
return minMoveLineDate;
}
/**
* Fonction qui récupére la plus récente date entre deux date
* @param date1
* Une date
* @param date2
* Une date
* @return minDate
* La plus ancienne date
*/
public LocalDate getLastDate(LocalDate date1, LocalDate date2) {
LocalDate minDate = new LocalDate();
if(date1!=null && date2!=null) {
if(date1.isAfter(date2)) { minDate=date1; }
else { minDate=date2; }
}
else if(date1!=null) { minDate=date1; }
else if(date2!=null) { minDate=date2; }
else { minDate=null; }
return minDate;
}
/**
* Fonction qui permet de récupérer la date de relance la plus récente
* @param reminder
* Une relance
* @return
* La date de relance la plus récente
*/
public LocalDate getLastDateReminder(Reminder reminder) {
return reminder.getReminderDate();
}
/**
* Fonction qui détermine la date de référence
* @param reminder
* Une relance
* @return
* La date de référence
*/
public LocalDate getReferenceDate(Reminder reminder) {
AccountingSituation accountingSituation = reminder.getAccountingSituation();
List<MoveLine> moveLineList = this.getMoveLineReminder(accountingSituation.getPartner(), accountingSituation.getCompany());
// Date la plus ancienne des lignes d'écriture
LocalDate minMoveLineDate = getOldDateMoveLine(moveLineList);
log.debug("minMoveLineDate : {}",minMoveLineDate);
// 2: Date la plus récente des relances
LocalDate reminderLastDate = getLastDateReminder(reminder);
log.debug("reminderLastDate : {}",reminderLastDate);
// Date de référence : Date la plus récente des deux ensembles (1 et 2)
LocalDate reminderRefDate = getLastDate(minMoveLineDate, reminderLastDate);
log.debug("reminderRefDate : {}",reminderRefDate);
return reminderRefDate;
}
/**
* Fonction permettant de récuperer une liste de ligne d'écriture exigible relançable d'un tiers
* @param partner
* Un tiers
* @param company
* Une société
* @return
* La liste de ligne d'écriture
*/
public List<MoveLine> getMoveLineReminder(Partner partner, Company company) {
List<MoveLine> moveLineList = new ArrayList<MoveLine>();
List<MoveLine> moveLineQuery = (List<MoveLine>) this.getMoveLine(partner, company);
int mailTransitTime = company.getAccountConfig().getMailTransitTime();
for(MoveLine moveLine : moveLineQuery) {
if(moveLine.getMove()!=null && !moveLine.getMove().getIgnoreInReminderOk()) {
Move move = moveLine.getMove();
//facture exigibles non bloquée en relance et dont la date de facture + délai d'acheminement < date du jour
if(move.getInvoice()!=null && !move.getInvoice().getReminderBlockingOk()
&& !move.getInvoice().getSchedulePaymentOk()
&& ((move.getInvoice().getInvoiceDate()).plusDays(mailTransitTime)).isBefore(today)) {
if((moveLine.getDebit().compareTo(BigDecimal.ZERO) > 0)
&& moveLine.getDueDate() != null
&& (today.isAfter(moveLine.getDueDate()) || today.isEqual(moveLine.getDueDate()))) {
if(moveLine.getAccount()!=null && moveLine.getAccount().getReconcileOk()) {
if(moveLine.getAmountRemaining().compareTo(BigDecimal.ZERO) > 0) {
moveLineList.add(moveLine);
}
}
}
}
//échéances rejetées qui ne sont pas bloqués
else if(move.getInvoice()==null) {
if(moveLine.getPaymentScheduleLine() != null
&& (moveLine.getDebit().compareTo(BigDecimal.ZERO) > 0)
&& moveLine.getDueDate() != null
&& (today.isAfter(moveLine.getDueDate()) || today.isEqual(moveLine.getDueDate()))) {
if(moveLine.getAccount()!=null && moveLine.getAccount().getReconcileOk()) {
if(moveLine.getAmountRemaining().compareTo(BigDecimal.ZERO) > 0) {
moveLineList.add(moveLine);
}
}
}
}
}
}
return moveLineList;
}
public List<Invoice> getInvoiceList(List<MoveLine> moveLineList) {
List<Invoice> invoiceList = new ArrayList<Invoice>();
for(MoveLine moveLine : moveLineList) {
if(moveLine.getMove().getInvoice()!=null && !moveLine.getMove().getInvoice().getReminderBlockingOk() ) {
invoiceList.add(moveLine.getMove().getInvoice());
}
}
return invoiceList;
}
public List<PaymentScheduleLine> getPaymentScheduleList(List<MoveLine> moveLineList, Partner partner) {
List<PaymentScheduleLine> paymentScheduleLineList = new ArrayList<PaymentScheduleLine>();
for(MoveLine moveLine : moveLineList) {
if(moveLine.getMove().getInvoice()==null ) {
// Ajout à la liste des échéances exigibles relançables
PaymentScheduleLine paymentScheduleLine = getPaymentScheduleFromMoveLine(partner, moveLine);
if(paymentScheduleLine != null) {
// Si un montant reste à payer, c'est à dire une échéance rejeté
if(moveLine.getAmountRemaining().compareTo(BigDecimal.ZERO) > 0) {
paymentScheduleLineList.add(paymentScheduleLine);
}
}
}
}
return paymentScheduleLineList;
}
/**
* Méthode permettant de récupérer l'ensemble des lignes d'écriture d'un tiers
* @param partner
* Un tiers
* @param company
* Une société
* @return
*/
public List<? extends MoveLine> getMoveLine(Partner partner, Company company) {
return moveLineRepo.all().filter("self.partner = ?1 and self.move.company = ?2", partner, company).fetch();
}
/**
* Méthode permettant de récupérer une ligne d'échéancier depuis une ligne d'écriture
* @param partner
* Un tiers
* @param moveLine
* @return
*/
public PaymentScheduleLine getPaymentScheduleFromMoveLine(Partner partner, MoveLine moveLine) {
return paymentScheduleLineRepo.all().filter("self.rejectMoveLine = ?1", moveLine).fetchOne();
}
/**
* Procédure permettant de tester si aujourd'hui nous sommes dans une période particulière
* @param dayBegin
* Le jour du début de la période
* @param dayEnd
* Le jour de fin de la période
* @param monthBegin
* Le mois de début de la période
* @param monthEnd
* Le mois de fin de la période
* @return
* Sommes-nous dans la période?
*/
public boolean periodOk(int dayBegin, int dayEnd, int monthBegin, int monthEnd) {
return DateTool.dateInPeriod(today, dayBegin, monthBegin, dayEnd, monthEnd);
}
public Reminder getReminder(Partner partner, Company company) throws AxelorException {
AccountingSituationRepository accSituationRepo = Beans.get(AccountingSituationRepository.class);
AccountingSituation accountingSituation = accSituationRepo.all().filter("self.partner = ?1 and self.company = ?2", partner, company).fetchOne();
if(accountingSituation != null) {
if(accountingSituation.getReminder() != null) {
return accountingSituation.getReminder();
}
else {
return this.createReminder(accountingSituation);
}
}
else {
throw new AxelorException(String.format("%s :\n"+I18n.get("Tiers")+" %s, "+I18n.get("Société")+" %s : "+I18n.get(IExceptionMessage.REMINDER_1),
GeneralServiceImpl.EXCEPTION, partner.getName(), company.getName()), IException.CONFIGURATION_ERROR);
}
}
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
public Reminder createReminder(AccountingSituation accountingSituation) {
Reminder reminder = new Reminder();
reminder.setAccountingSituation(accountingSituation);
reminderRepo.save(reminder);
return reminder;
}
/**
* Méthode de relance en masse
* @param partner
* Un tiers
* @param company
* Une société
* @throws AxelorException
* @throws IllegalAccessException
* @throws InstantiationException
* @throws ClassNotFoundException
* @throws IOException
*/
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
public boolean reminderGenerate(Partner partner, Company company) throws AxelorException, ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
boolean remindedOk = false;
Reminder reminder = this.getReminder(partner, company); // ou getReminder si existe
BigDecimal balanceDue = accountCustomerService.getBalanceDue(partner, company);
if (balanceDue.compareTo(BigDecimal.ZERO) > 0) {
reminder.setBalanceDue(balanceDue);
log.debug("balanceDue : {} ",balanceDue);
BigDecimal balanceDueReminder = accountCustomerService.getBalanceDueReminder(partner, company);
if (balanceDueReminder.compareTo(BigDecimal.ZERO) > 0) {
log.debug("balanceDueReminder : {} ",balanceDueReminder);
remindedOk = true;
List<MoveLine> moveLineList = this.getMoveLineReminder(partner, company);
this.updateInvoiceReminder(reminder, this.getInvoiceList(moveLineList));
this.updatePaymentScheduleLineReminder(reminder, this.getPaymentScheduleList(moveLineList, partner));
reminder.setBalanceDueReminder(balanceDueReminder);
Integer levelReminder = 0;
if(reminder.getReminderMethodLine() != null) {
levelReminder = reminder.getReminderMethodLine().getReminderLevel().getName();
}
LocalDate referenceDate = this.getReferenceDate(reminder);
if(referenceDate != null) {
log.debug("date de référence : {} ",referenceDate);
reminder.setReferenceDate(referenceDate);
}
else {
throw new AxelorException(String.format("%s :\n"+I18n.get("Tiers")+" %s, "+I18n.get("Société")+" %s : "+I18n.get(IExceptionMessage.REMINDER_2),
GeneralServiceImpl.EXCEPTION, partner.getName(), company.getName()), IException.CONFIGURATION_ERROR);
}
if(reminder.getReminderMethod() == null) {
if(reminderSessionService.getReminderMethod(reminder)!=null) {
reminder.setReminderMethod(reminderSessionService.getReminderMethod(reminder));
reminderSessionService.reminderSession(reminder);
}
else {
throw new AxelorException(String.format("%s :\n"+I18n.get("Tiers")+" %s, "+I18n.get("Société")+" %s : "+I18n.get(IExceptionMessage.REMINDER_3),
GeneralServiceImpl.EXCEPTION, partner.getName(), company.getName()), IException.CONFIGURATION_ERROR);
}
}
else {
reminderSessionService.reminderSession(reminder);
}
if(reminder.getWaitReminderMethodLine()==null) {
// Si le niveau de relance à évolué
if(reminder.getReminderMethodLine() != null && reminder.getReminderMethodLine().getReminderLevel() != null &&
reminder.getReminderMethodLine().getReminderLevel().getName() > levelReminder) {
reminderActionService.runAction(reminder);
}
}
else {
log.debug("Tiers {}, Société {} - Niveau de relance en attente ", partner.getName(), company.getName());
// TODO Alarm ?
TraceBackService.trace(new AxelorException(
String.format("%s :\n"+I18n.get("Tiers")+" %s, "+I18n.get("Société")+" %s : "+I18n.get(IExceptionMessage.REMINDER_4),
GeneralServiceImpl.EXCEPTION, partner.getName(), company.getName()), IException.INCONSISTENCY));
}
}
}
else {
reminderSessionService.reminderInitialisation(reminder);
}
return remindedOk;
}
public void updateInvoiceReminder(Reminder reminder, List<Invoice> invoiceList) {
reminder.setInvoiceReminderSet(new HashSet<Invoice>());
reminder.getInvoiceReminderSet().addAll(invoiceList);
}
public void updatePaymentScheduleLineReminder(Reminder reminder, List<PaymentScheduleLine> paymentSchedueLineList) {
reminder.setPaymentScheduleLineReminderSet(new HashSet<PaymentScheduleLine>());
reminder.getPaymentScheduleLineReminderSet().addAll(paymentSchedueLineList);
}
}