Cuando un pago solo cubre parcialmente el fee moratorio ($100), se necesita calcular y registrar el IVA proporcional (13% de los $50 pagados = $6.50). Actualmente Fineract trata cada charge como independiente.
- Enfoque: Nativo en Fineract (charges vinculados)
- Vinculación: Campo explícito
parent_charge_idal crear el charge - Migración: Solo aplica para préstamos nuevos (no migrar existentes)
- TaxGroups NO funciona para Loans - Solo está implementado para Savings
- Charges son independientes - No hay vinculación padre-hijo entre fee moratorio y su IVA
LoanCharge (fee moratorio: $100)
└── LoanCharge (IVA 13%: $13) [parent_charge_id = fee moratorio]
Pago de $50 (IVA INCLUIDO):
1. Se recibe pago de $50
2. Se detecta charge vinculado (IVA con tax_percentage=13%)
3. Se calcula distribución:
- IVA = $50 / 1.13 * 0.13 = $5.75
- Fee neto = $50 - $5.75 = $44.25
4. Se aplica $44.25 al fee moratorio (quedan $55.75 pendientes)
5. Se aplica $5.75 al IVA (quedan $7.25 pendientes)
6. Total pagado = $50
Estado después del pago:
- Fee moratorio: $44.25 pagado / $55.75 pendiente
- IVA: $5.75 pagado / $7.25 pendiente
Pago de $113 (IVA INCLUIDO):
1. Se recibe pago de $113
2. Se detecta charge vinculado (IVA con tax_percentage=13%)
3. Se calcula distribución:
- IVA = $113 / 1.13 * 0.13 = $13
- Fee neto = $113 - $13 = $100
4. Se aplica $100 al fee moratorio (queda $0 pendiente) ✓
5. Se aplica $13 al IVA (queda $0 pendiente) ✓
6. Ambos charges marcados como PAID
7. Total pagado = $113
Estado después del pago:
- Fee moratorio: PAGADO COMPLETO
- IVA: PAGADO COMPLETO
Fee pendiente: $55.75, IVA pendiente: $7.25 (Total: $63)
Pago recibido: $100
Distribución calculada:
- IVA = $100 / 1.13 * 0.13 = $11.50
- Fee neto = $100 - $11.50 = $88.50
Pero pendiente es menor, entonces:
- Fee aplicado = min($88.50, $55.75) = $55.75 ✓
- IVA aplicado = min($11.50, $7.25) = $7.25 ✓
- Sobrante = $100 - $55.75 - $7.25 = $37 → se aplica a otros charges/principal
Resultado:
- Fee moratorio: PAGADO COMPLETO
- IVA: PAGADO COMPLETO
- Sobrante: $37 para otros conceptos
IVA = monto / (1 + taxPercentage) * taxPercentage
Fee = monto - IVA
// Con límites
IVA_aplicado = min(IVA_calculado, IVA_pendiente)
Fee_aplicado = min(Fee_calculado, Fee_pendiente)
Sobrante = monto - IVA_aplicado - Fee_aplicado
Crear: fineract-provider/src/main/resources/db/changelog/tenant/parts/XXXX_linked_charges.xml
ALTER TABLE m_loan_charge ADD COLUMN parent_charge_id BIGINT NULL;
ALTER TABLE m_loan_charge ADD COLUMN tax_percentage DECIMAL(5,2) NULL;
ALTER TABLE m_loan_charge ADD CONSTRAINT fk_loan_charge_parent
FOREIGN KEY (parent_charge_id) REFERENCES m_loan_charge(id);
CREATE INDEX idx_loan_charge_parent ON m_loan_charge(parent_charge_id);Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_charge_id")
private LoanCharge parentCharge;
@OneToMany(mappedBy = "parentCharge", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<LoanCharge> linkedCharges = new HashSet<>();
@Column(name = "tax_percentage", scale = 2, precision = 5)
private BigDecimal taxPercentage;
public boolean hasLinkedCharges() { ... }
public Set<LoanCharge> getActiveLinkedCharges() { ... }
// Calcula la distribución del pago entre fee e IVA (IVA incluido en el monto)
public PaymentSplit calculatePaymentSplit(Money totalPayment) {
// taxAmount = totalPayment / (1 + taxPercentage) * taxPercentage
// feeAmount = totalPayment - taxAmount
BigDecimal divisor = BigDecimal.ONE.add(this.taxPercentage.divide(new BigDecimal("100")));
Money taxAmount = totalPayment.dividedBy(divisor).multipliedBy(this.taxPercentage).dividedBy(100);
Money feeAmount = totalPayment.minus(taxAmount);
return new PaymentSplit(feeAmount, taxAmount);
}Modificar: fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
En método payLoanCharge():
// Si el charge tiene un charge de IVA vinculado, distribuir el pago
if (loanCharge.hasLinkedCharges()) {
LoanCharge taxCharge = loanCharge.getLinkedTaxCharge();
// Calcular distribución proporcional
PaymentSplit split = taxCharge.calculatePaymentSplit(paymentAmount);
// Aplicar límites (no pagar más de lo pendiente)
Money feeOutstanding = loanCharge.getAmountOutstanding(currency);
Money taxOutstanding = taxCharge.getAmountOutstanding(currency);
Money feeToApply = Money.min(split.getFeeAmount(), feeOutstanding);
Money taxToApply = Money.min(split.getTaxAmount(), taxOutstanding);
// Aplicar pagos
applyChargePayment(loan, loanCharge, feeToApply, transactionDate);
applyChargePayment(loan, taxCharge, taxToApply, transactionDate);
// Calcular sobrante para otros conceptos
Money totalApplied = feeToApply.plus(taxToApply);
Money surplus = paymentAmount.minus(totalApplied);
if (surplus.isGreaterThanZero()) {
// El sobrante se aplica al siguiente charge/principal según PaymentAllocationStrategy
applyRemainingToLoan(loan, surplus, transactionDate);
}
} else {
// Comportamiento normal (sin IVA vinculado)
applyChargePayment(loan, loanCharge, paymentAmount, transactionDate);
}Crear: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/PaymentSplit.java
public class PaymentSplit {
private final Money feeAmount;
private final Money taxAmount;
public PaymentSplit(Money feeAmount, Money taxAmount) {
this.feeAmount = feeAmount;
this.taxAmount = taxAmount;
}
// getters
}Modificar:
LoanChargesApiResource.java- AceptarparentChargeIdytaxPercentageLoanChargeData.java- Agregar campos nuevosChargesApiConstants.java- Constantes para nuevos parámetros
Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
@Query("SELECT lc FROM LoanCharge lc WHERE lc.parentCharge.id = :parentId AND lc.active = true")
List<LoanCharge> findActiveLinkedCharges(@Param("parentId") Long parentChargeId);| Archivo | Acción | Líneas Est. |
|---|---|---|
XXXX_linked_charges.xml |
Crear | ~30 |
LoanCharge.java |
Modificar | ~100 |
PaymentSplit.java |
Crear | ~30 |
LoanChargeRepository.java |
Modificar | ~15 |
LoanChargeWritePlatformServiceImpl.java |
Modificar | ~120 |
LoanChargeReadPlatformServiceImpl.java |
Modificar | ~40 |
LoanChargeData.java |
Modificar | ~25 |
LoanChargesApiResource.java |
Modificar | ~30 |
ChargesApiConstants.java |
Modificar | ~10 |
| Tests unitarios | Crear | ~200 |
| Tests integración | Crear | ~150 |
Total estimado: ~750 líneas
- Crear migración XML para nuevas columnas
- Modificar
LoanCharge.javacon campos y relaciones - Actualizar
LoanChargeRepository.java
- Implementar
calculateProportionalAmount()en LoanCharge - Modificar
payLoanCharge()para procesar charges vinculados - Agregar validaciones (no permitir pagar charge vinculado directamente)
- Actualizar
LoanChargeData.javacon nuevos campos - Modificar
LoanChargesApiResource.javapara aceptarparentChargeId - Actualizar serialización/deserialización
- Tests unitarios para cálculo proporcional
- Tests de integración para flujo completo de pago
- Tests de regresión para pagos normales
fineract-provider/src/main/resources/db/changelog/tenant/parts/
├── XXXX_linked_charges.xml (CREAR)
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/
├── domain/
│ ├── LoanCharge.java (MODIFICAR)
│ ├── PaymentSplit.java (CREAR)
│ └── LoanChargeRepository.java (MODIFICAR)
├── data/
│ └── LoanChargeData.java (MODIFICAR)
└── service/
└── LoanChargeReadPlatformServiceImpl.java (MODIFICAR)
fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/
├── api/
│ └── LoanChargesApiResource.java (MODIFICAR)
└── service/
└── LoanChargeWritePlatformServiceImpl.java (MODIFICAR)
fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/api/
└── ChargesApiConstants.java (MODIFICAR)
- Al crear charge vinculado: Validar que
parentChargeIdexista y pertenezca al mismo préstamo - Al pagar fee principal: Si tiene IVA vinculado, distribuir automáticamente
- Al pagar charge de IVA directamente: Rechazar si tiene
parentCharge(debe pagarse vía el padre) - Monto máximo: Validar que el pago no exceda lo pendiente (fee + IVA)
El mapeo contable funciona automáticamente porque:
- Cada
Chargetiene su propia cuenta GL (income_or_liability_account_id) - Al pagar, se crea un
LoanChargePaidBypor cada charge AccountingProcessorHelper.createJournalEntriesForLoanChargesInternal()genera asientos separados
Pago de $50 distribuido:
├── LoanChargePaidBy { charge: "Fee Moratorio", amount: $44.25 }
└── LoanChargePaidBy { charge: "IVA", amount: $5.75 }
Journal Entries generados:
├── Crédito $44.25 → Cuenta 4100 (Ingresos por comisiones)
├── Crédito $5.75 → Cuenta 2100 (IVA por pagar)
└── Débito $50.00 → Cuenta Caja/Fondo
Al crear los charges, asegurarse de configurar:
- Fee Moratorio:
income_or_liability_account_id= Cuenta de ingresos por comisiones - Charge IVA:
income_or_liability_account_id= Cuenta de IVA por pagar
Implementar el soporte de TaxGroups en el módulo de Loans, replicando el patrón que ya funciona en Savings. Esto aprovecha la infraestructura existente:
- Charge ya tiene
tax_group_id- Solo falta usarlo en LoanCharge - TaxUtils.splitTax() - Ya existe y calcula impuestos proporcionalmente
- TaxComponent - Ya tiene cuentas GL configuradas para contabilidad
| Aspecto | Opción A (Charges Vinculados) | Opción B (TaxGroups) |
|---|---|---|
| Reutilización | Código nuevo | Replica patrón probado de Savings |
| Configuración | 2 charges separados | 1 charge + TaxGroup |
| Mantenimiento | Lógica custom | Usa TaxUtils estándar |
| Contabilidad | Config por charge | Config en TaxComponent |
| Múltiples impuestos | N charges vinculados | 1 TaxGroup con N componentes |
| Historial de tasas | Manual | TaxComponentHistory automático |
Charge (fee moratorio)
└── TaxGroup (IVA 13%)
└── TaxComponent (IVA)
├── percentage: 13.00
├── creditAccount: Cuenta 2100 (IVA por pagar)
└── debitAccount: Cuenta de retención
LoanCharge hereda TaxGroup del Charge padre
└── Al pagar, usa TaxUtils.splitTax() para calcular IVA
- Hoy creas 2 charges separados: Fee neto ($100) + Charge IVA ($13)
- Problema: Al pagar parcialmente no se distribuye proporcionalmente
- 1 solo charge: Fee neto ($100) con TaxGroup (IVA 13%)
- El IVA se calcula automáticamente SOBRE el monto pagado
Charge configurado: Fee moratorio $100 (NETO) + TaxGroup IVA 13%
Total adeudado: $100 + $13 = $113
Escenario: Pago parcial de $50
1. Se recibe pago de $50
2. Se distribuye proporcionalmente:
- Proporción pagada: $50 / $113 = 44.25%
- Fee neto pagado: $100 × 44.25% = $44.25
- IVA pagado: $13 × 44.25% = $5.75
3. Se usa TaxUtils.splitTax() existente:
- IVA = $44.25 × 13% = $5.75 ✓
4. Se crea LoanTransactionTaxDetails:
- taxComponent: IVA
- amount: $5.75
5. Journal entries:
- Crédito $44.25 → Cuenta ingresos comisiones
- Crédito $5.75 → Cuenta 2100 (IVA por pagar)
- Débito $50 → Cuenta caja
Estado después del pago:
- Fee pendiente: $100 - $44.25 = $55.75
- IVA pendiente: $13 - $5.75 = $7.25
- Total pendiente: $63
Fórmula IVA Adicional (TaxUtils.splitTax existente):
// IVA sobre el monto neto - YA EXISTE EN FINERACT
tax = netAmount × percentage / 100
// Ejemplo: $44.25 × 13% = $5.75Ventaja: No necesitamos crear nuevo método, reutilizamos TaxUtils.splitTax() tal cual.
Crear: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionTaxDetails.java
@Entity
@Table(name = "m_loan_transaction_tax_details")
public class LoanTransactionTaxDetails extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "loan_transaction_id", nullable = false)
private LoanTransaction loanTransaction;
@ManyToOne
@JoinColumn(name = "tax_component_id", nullable = false)
private TaxComponent taxComponent;
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
// Constructor, getters
}Crear: fineract-provider/src/main/resources/db/changelog/tenant/parts/XXXX_loan_tax_details.xml
CREATE TABLE m_loan_transaction_tax_details (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
loan_transaction_id BIGINT NOT NULL,
tax_component_id BIGINT NOT NULL,
amount DECIMAL(19,6) NOT NULL,
CONSTRAINT fk_loan_tx_tax_loan_transaction
FOREIGN KEY (loan_transaction_id) REFERENCES m_loan_transaction(id),
CONSTRAINT fk_loan_tx_tax_component
FOREIGN KEY (tax_component_id) REFERENCES m_tax_component(id)
);
CREATE INDEX idx_loan_tx_tax_transaction ON m_loan_transaction_tax_details(loan_transaction_id);Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
// Agregar campo
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true,
fetch = FetchType.EAGER, mappedBy = "loanTransaction")
private List<LoanTransactionTaxDetails> taxDetails = new ArrayList<>();
// Agregar método
public List<LoanTransactionTaxDetails> getTaxDetails() {
return this.taxDetails;
}
// Agregar método estático para actualizar detalles
public static void updateTaxDetails(
final Map<TaxComponent, BigDecimal> taxDetails,
final LoanTransaction transaction) {
if (taxDetails != null) {
for (Map.Entry<TaxComponent, BigDecimal> entry : taxDetails.entrySet()) {
transaction.getTaxDetails().add(
new LoanTransactionTaxDetails(transaction, entry.getKey(), entry.getValue())
);
}
}
}Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java
// Agregar método para obtener TaxGroup del Charge padre
public TaxGroup getTaxGroup() {
return this.charge.getTaxGroup();
}
public boolean hasTaxGroup() {
return this.charge.getTaxGroup() != null;
}Modificar: fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
En método donde se procesa el pago:
// Si el charge tiene TaxGroup, calcular impuestos (IVA ADICIONAL)
if (loanCharge.hasTaxGroup()) {
TaxGroup taxGroup = loanCharge.getTaxGroup();
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
// Calcular impuestos usando TaxUtils.splitTax() EXISTENTE
// El pago se distribuye proporcionalmente entre fee neto e IVA
Map<TaxComponent, BigDecimal> taxSplit = TaxUtils.splitTax(
feeNetAmount.getAmount(), // Monto neto del fee pagado
transactionDate,
taxGroup.getTaxGroupMappings(),
feeNetAmount.getAmount().scale()
);
BigDecimal totalTax = TaxUtils.totalTaxAmount(taxSplit);
if (totalTax.compareTo(BigDecimal.ZERO) > 0) {
// Registrar detalles de impuestos en la transacción
LoanTransaction.updateTaxDetails(taxSplit, loanTransaction);
}
}Modificar: fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
Agregar método similar a createCashBasedJournalEntriesAndReversalsForSavingsTax():
public void createJournalEntriesForLoanChargeTax(
final Office office,
final String currencyCode,
final Long loanProductId,
final Long loanId,
final String transactionId,
final LocalDate transactionDate,
final List<TaxPaymentDTO> taxDetails) {
for (TaxPaymentDTO taxPaymentDTO : taxDetails) {
if (taxPaymentDTO.getAmount() != null &&
taxPaymentDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) {
// Crédito a la cuenta del TaxComponent
if (taxPaymentDTO.getCreditAccountId() != null) {
createCreditJournalEntryForLoan(
office, currencyCode,
taxPaymentDTO.getCreditAccountId(),
loanId, transactionId, transactionDate,
taxPaymentDTO.getAmount()
);
}
}
}
}Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java
Agregar extracción de taxDetails similar a como se hace en Savings.
| Archivo | Acción | Líneas Est. |
|---|---|---|
XXXX_loan_tax_details.xml |
Crear | ~25 |
LoanTransactionTaxDetails.java |
Crear | ~50 |
LoanTransaction.java |
Modificar | ~40 |
LoanCharge.java |
Modificar | ~15 |
LoanChargeWritePlatformServiceImpl.java |
Modificar | ~60 |
AccountingProcessorHelper.java |
Modificar | ~50 |
LoanAccountingBridgeMapper.java |
Modificar | ~30 |
AccrualBasedAccountingProcessorForLoan.java |
Modificar | ~40 |
| Tests unitarios | Crear | ~150 |
| Tests integración | Crear | ~100 |
Total estimado: ~560 líneas
Nota: No se requiere modificar TaxUtils.java - se reutiliza splitTax() existente.
- Crear migración para
m_loan_transaction_tax_details - Crear entidad
LoanTransactionTaxDetails - Modificar
LoanTransactionpara incluir taxDetails
- Agregar métodos en
LoanChargepara acceder al TaxGroup - Modificar procesamiento de pagos para calcular impuestos con TaxUtils
- Registrar LoanTransactionTaxDetails al pagar
- Modificar AccountingProcessorHelper para manejar taxDetails de Loans
- Asegurar que se generen asientos a las cuentas del TaxComponent
- Tests unitarios para cálculo de impuestos
- Tests de integración para flujo completo
- Validar asientos contables generados
POST /taxes/component
{
"name": "IVA 13%",
"percentage": 13.00,
"creditAccountId": 2100, // Cuenta IVA por pagar
"startDate": "2024-01-01"
}
POST /taxes/group
{
"name": "Impuestos Comisiones",
"taxComponents": [
{ "taxComponentId": 1, "startDate": "2024-01-01" }
]
}
POST /charges
{
"name": "Fee Moratorio",
"chargeAppliesTo": 1, // LOAN
"amount": 100,
"taxGroupId": 1, // TaxGroup creado
...
}
| Criterio | Opción A | Opción B |
|---|---|---|
| Líneas de código | ~750 | ~560 |
| Complejidad | Media | Media |
| Reutilización | Baja | Alta (replica Savings) |
| Mantenimiento | Lógica custom | Usa infraestructura existente |
| Flexibilidad | Solo IVA vinculado | Múltiples impuestos, historial de tasas |
| Configuración | Por charge | Por TaxGroup (centralizada) |
| Fórmula IVA | IVA incluido | IVA adicional (estándar Fineract) ✓ |
| Charges visibles | 2 (fee + IVA) | 1 (fee con TaxGroup) |
| TaxUtils | Nuevo método | Reutiliza splitTax() existente ✓ |
Opción B (TaxGroups) es preferible si:
- Quieres reutilizar la infraestructura existente de Fineract
- Necesitas flexibilidad para múltiples impuestos o cambios de tasas
- Prefieres configuración centralizada
- Solo quieres ver 1 charge (el fee), con el IVA calculado internamente
Opción A (Charges Vinculados) es preferible si:
- Necesitas ver el IVA como un charge separado en la UI/API
- Quieres control granular sobre cada charge de IVA
- Prefieres no depender de la configuración de TaxGroups
| Cronograma | Compatibilidad | Notas |
|---|---|---|
| CUMULATIVE | ✅ Alta | Períodos fijos, sin recálculo dinámico |
| PROGRESSIVE | Requiere validación con recálculo de interés |
| Estrategia | Compatibilidad |
|---|---|
| HeavensFamily | ✅ Compatible |
| Creocor | ✅ Compatible |
| RBI | ✅ Compatible |
| FineractStyle | ✅ Compatible |
| EarlyPayment | ✅ Compatible |
| InterestPrincipalPenaltyFees | ✅ Compatible |
| PrincipalInterestPenaltyFees | ✅ Compatible |
| DuePen/InAdvance | ✅ Compatible |
| AdvancedPaymentSchedule |
Todas las estrategias usan el mismo método base:
LoanCharge.updatePaidAmountBy()- Punto central donde se aplican pagosAbstractLoanRepaymentScheduleTransactionProcessor.updateChargesPaidAmountBy()- Procesa pagos a charges
Al integrar TaxGroup en estos métodos, todas las estrategias heredan automáticamente el soporte de impuestos.
El procesador avanzado tiene lógica adicional para:
- Recálculo dinámico de interés
- PaymentAllocationRules configurables
- FutureInstallmentAllocationRule
Recomendación: Ejecutar tests de integración específicos para este procesador después de implementar.
Los intereses devengados también son un ingreso que requiere facturar IVA según la regulación financiera. Actualmente Fineract no soporta TaxGroups para intereses de préstamos.
LoanTransaction.interestPortion → AccrualBasedAccountingProcessorForLoan
→ Crédito a INTEREST_ON_LOANS (cuenta de ingresos)
→ Débito a INTEREST_RECEIVABLE (activo) o FUND_SOURCE (en cobro)
Archivos clave:
LoanTransaction.java:94- CampointerestPortionAccrualBasedAccountingProcessorForLoan.java- Genera asientos contablesAccountingConstants.java:94-INTEREST_ON_LOANS(3)
LoanProduct
└── interestTaxGroup (NUEVO campo)
└── TaxGroup (IVA 13%)
└── TaxComponent (IVA)
├── percentage: 13.00
└── creditAccount: 2100 (IVA por pagar)
Interés devengado: $100 NETO
Cálculo con TaxGroup:
IVA = $100 × 13% = $13
Total = $113
Journal Entries (devengamiento):
├── Débito $100 → INTEREST_RECEIVABLE (Activo)
├── Débito $13 → IVA_RECEIVABLE (Activo) ← NUEVA CUENTA
├── Crédito $100 → INTEREST_ON_LOANS (Ingreso)
└── Crédito $13 → IVA_POR_PAGAR (Pasivo) ← del TaxComponent
Pago parcial de interés: $50 de $113 pendientes
Distribución proporcional:
- Proporción: $50 / $113 = 44.25%
- Interés neto cobrado: $100 × 44.25% = $44.25
- IVA cobrado: $13 × 44.25% = $5.75
Journal Entries (cobro):
├── Débito $50 → FUND_SOURCE (Caja)
├── Crédito $44.25 → INTEREST_RECEIVABLE
└── Crédito $5.75 → IVA_RECEIVABLE
Modificar: XXXX_loan_tax_details.xml
-- Agregar TaxGroup a LoanProduct para intereses
ALTER TABLE m_product_loan ADD COLUMN interest_tax_group_id BIGINT NULL;
ALTER TABLE m_product_loan ADD CONSTRAINT fk_product_loan_interest_tax_group
FOREIGN KEY (interest_tax_group_id) REFERENCES m_tax_group(id);
-- Nueva cuenta contable para IVA por cobrar sobre intereses
-- Se configura a nivel de AccrualAccountsForLoanModificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "interest_tax_group_id")
private TaxGroup interestTaxGroup;
public TaxGroup getInterestTaxGroup() {
return this.interestTaxGroup;
}
public boolean hasInterestTaxGroup() {
return this.interestTaxGroup != null;
}Modificar: AccountingConstants.java
public enum AccrualAccountsForLoan {
// ... existentes ...
INTEREST_TAX_RECEIVABLE(26); // NUEVA
}
public enum LoanProductAccountingParams {
// ... existentes ...
INTEREST_TAX_RECEIVABLE("interestTaxReceivableAccountId"); // NUEVA
}Modificar: AccrualBasedAccountingProcessorForLoan.java
En el método que procesa accruals:
// Si el producto tiene TaxGroup para intereses, calcular impuestos
if (loanDTO.hasInterestTaxGroup() && MathUtil.isGreaterThanZero(interestAmount)) {
TaxGroup taxGroup = loanDTO.getInterestTaxGroup();
// Calcular impuestos usando TaxUtils.splitTax()
Map<TaxComponent, BigDecimal> taxSplit = TaxUtils.splitTax(
interestAmount, transactionDate,
taxGroup.getTaxGroupMappings(), interestAmount.scale()
);
BigDecimal totalTax = TaxUtils.totalTaxAmount(taxSplit);
// Registrar impuesto sobre interés
for (Map.Entry<TaxComponent, BigDecimal> entry : taxSplit.entrySet()) {
TaxComponent taxComponent = entry.getKey();
BigDecimal taxAmount = entry.getValue();
if (MathUtil.isGreaterThanZero(taxAmount)) {
// Débito a IVA_RECEIVABLE (activo)
populateDebitMap(loanProductId, taxAmount,
AccrualAccountsForLoan.INTEREST_TAX_RECEIVABLE.getValue(),
glAccountBalanceHolder);
// Crédito a cuenta del TaxComponent (IVA por pagar)
glAccountBalanceHolder.addToCredit(
taxComponent.getCreditAcount(), taxAmount);
}
}
// Registrar en LoanTransactionTaxDetails
LoanTransaction.updateTaxDetails(taxSplit, loanTransaction);
}Modificar: fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java
// Agregar campo para detalles de impuestos
private List<TaxPaymentDTO> interestTaxDetails;Modificar: fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
private TaxGroup interestTaxGroup;
public boolean hasInterestTaxGroup() {
return this.interestTaxGroup != null;
}| Archivo | Acción | Líneas Est. |
|---|---|---|
LoanProduct.java |
Modificar | ~20 |
AccountingConstants.java |
Modificar | ~15 |
AccrualBasedAccountingProcessorForLoan.java |
Modificar | ~80 |
CashBasedAccountingProcessorForLoan.java |
Modificar | ~60 |
LoanDTO.java |
Modificar | ~15 |
LoanTransactionDTO.java |
Modificar | ~10 |
LoanProductWritePlatformServiceImpl.java |
Modificar | ~30 |
LoanProductReadPlatformServiceImpl.java |
Modificar | ~20 |
| Migración XML | Modificar | ~20 |
| Tests | Crear | ~150 |
Total adicional estimado: ~420 líneas
- Agregar
interest_tax_group_idam_product_loan - Agregar campo y relación en
LoanProduct.java - Agregar nueva cuenta
INTEREST_TAX_RECEIVABLEen AccountingConstants
- Modificar
AccrualBasedAccountingProcessorForLoanpara calcular IVA - Modificar
CashBasedAccountingProcessorForLoansimilarmente - Generar asientos contables separados para IVA
- Distribuir pagos proporcionalmente entre interés neto e IVA
- Registrar
LoanTransactionTaxDetailspara intereses
- Permitir configurar
interestTaxGroupIdal crear/editar producto - Exponer información de impuestos en APIs de transacciones
CONFIGURACIÓN:
- Producto: "Crédito Personal"
- interestTaxGroup: TaxGroup "IVA Intereses" (13%)
- Cuenta INTEREST_ON_LOANS: 4200 (Ingresos por intereses)
- Cuenta INTEREST_TAX_RECEIVABLE: 1150 (IVA por cobrar)
- TaxComponent.creditAccount: 2100 (IVA por pagar)
PRÉSTAMO:
- Principal: $10,000
- Interés mensual: $500 (NETO)
- IVA sobre interés: $65 (13%)
- Total interés + IVA: $565
DEVENGAMIENTO (día 1 del mes):
├── Débito $500 → 1140 INTEREST_RECEIVABLE
├── Débito $65 → 1150 INTEREST_TAX_RECEIVABLE
├── Crédito $500 → 4200 INTEREST_ON_LOANS
└── Crédito $65 → 2100 IVA_POR_PAGAR
PAGO PARCIAL ($300 de $565):
├── Débito $300 → 1000 CAJA
├── Crédito $265.49 → 1140 INTEREST_RECEIVABLE (53.1%)
└── Crédito $34.51 → 1150 INTEREST_TAX_RECEIVABLE (53.1%)
Estado pendiente:
- Interés: $234.51
- IVA: $30.49
- Total: $265
URL: https://gist.github.com/jluisflo/d41ea919b23e3233b4dcd23d0c81d2b9
(Actualizar manualmente con el contenido final)