added penal charge to schedule
This commit was merged in pull request #67.
This commit is contained in:
+19
-3
@@ -47,8 +47,8 @@ class Loan(db.Model):
|
||||
verify_description = db.Column(db.String(100), nullable=True)
|
||||
reference = db.Column(db.String(50), nullable=True)
|
||||
|
||||
# total_penal_charge = db.Column(db.Float, default=0.0)
|
||||
# last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||
total_penal_charge = db.Column(db.Float, default=0.0)
|
||||
last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
customer = relationship(
|
||||
"Customer",
|
||||
@@ -94,7 +94,9 @@ class Loan(db.Model):
|
||||
'disburseVerify': self.disburse_verify.isoformat() if self.disburse_verify else None,
|
||||
'reference': self.reference,
|
||||
'balance': self.balance,
|
||||
'tenor': self.tenor,
|
||||
'tenor': self.tenor,
|
||||
'totalPenalCharge': self.total_penal_charge,
|
||||
'lastPenalDate': self.last_penal_date
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -397,3 +399,17 @@ class Loan(db.Model):
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching overdue loans: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def apply_penal_to_loan(cls, loan_id, penal_amount):
|
||||
|
||||
loan = cls.query.get(loan_id)
|
||||
|
||||
if not loan:
|
||||
raise ValueError("Loan not found")
|
||||
penal_amount = Decimal(str(penal_amount))
|
||||
|
||||
loan.total_penal_charge = Decimal(str(loan.total_penal_charge or 0)) + penal_amount
|
||||
loan.last_penal_date = datetime.now(timezone.utc)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from os.path import devnull
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from app.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
@@ -43,7 +43,85 @@ class LoanCharge(db.Model):
|
||||
'description': self.description,
|
||||
'due': self.due
|
||||
}
|
||||
#get last penal
|
||||
@classmethod
|
||||
def get_last_penal_no(cls, loan_id):
|
||||
"""
|
||||
Returns the last penal number created for a loan.
|
||||
Example:
|
||||
PENAL1 -> returns 1
|
||||
PENAL3 -> returns 3
|
||||
If none exists, returns 0.
|
||||
"""
|
||||
last_penal = (
|
||||
cls.query
|
||||
.filter(cls.loan_id == loan_id)
|
||||
.filter(cls.code.like("PENAL%"))
|
||||
.order_by(cls.id.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not last_penal:
|
||||
return 0
|
||||
|
||||
try:
|
||||
return int(last_penal.code.replace("PENAL", ""))
|
||||
except ValueError:
|
||||
return 0
|
||||
@classmethod
|
||||
def get_penal_charges_by_loan_id(cls, loan_id):
|
||||
"""
|
||||
Returns all penal charges for a specific loan.
|
||||
"""
|
||||
return cls.query.filter(
|
||||
cls.loan_id == loan_id,
|
||||
cls.code.like("PENAL%")
|
||||
).all()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_loan_charge_by_debt_id(cls, debt_id):
|
||||
return cls.query.filter_by(loan_id=debt_id)
|
||||
return cls.query.filter_by(loan_id=debt_id)
|
||||
|
||||
#create penal charge
|
||||
@classmethod
|
||||
def create_penal_charges_for_loan(cls, loan_id, transaction_id, percent, penal_no, schedule_number, penal_amount=0.0):
|
||||
"""
|
||||
Create a penal charge for a given loan and schedule.
|
||||
"""
|
||||
|
||||
if loan_id is None:
|
||||
raise ValueError("loan_id cannot be None")
|
||||
|
||||
code = f"PENAL{penal_no:02d}-SCHEDULE{schedule_number:02d}"
|
||||
|
||||
# Check if this penal charge already exists
|
||||
existing = cls.query.filter_by(
|
||||
loan_id=loan_id,
|
||||
code=code
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
penal_charge = cls(
|
||||
loan_id=loan_id,
|
||||
transaction_id=transaction_id,
|
||||
code=code,
|
||||
amount=penal_amount,
|
||||
percent=percent,
|
||||
description=f"Penal Charge {penal_no} for loan {loan_id} schedule {schedule_number}",
|
||||
due=True,
|
||||
due_date=now
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(penal_charge)
|
||||
db.session.commit()
|
||||
except IntegrityError as err:
|
||||
db.session.rollback()
|
||||
raise ValueError(f"Database integrity error: {err}")
|
||||
|
||||
return penal_charge
|
||||
@@ -28,9 +28,9 @@ class LoanRepaymentSchedule(db.Model):
|
||||
partial_balance = db.Column(db.Float, default=0.0)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
||||
|
||||
# penal_charge = db.Column(db.Float, default=0.0)
|
||||
# penal_count = db.Column(db.Integer, default=0)
|
||||
penal_charge = db.Column(db.Float, default=0.0)
|
||||
penal_count = db.Column(db.Integer, default=0)
|
||||
last_penal_date = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
|
||||
|
||||
@@ -52,7 +52,11 @@ class LoanRepaymentSchedule(db.Model):
|
||||
'partial_balance': self.partial_balance,
|
||||
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'penal_charge': self.penal_charge,
|
||||
'penal_count': self.penal_count,
|
||||
'last_penal_date': self.last_penal_date.isoformat() if self.last_penal_date else None
|
||||
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
@@ -296,54 +300,37 @@ class LoanRepaymentSchedule(db.Model):
|
||||
db.session.rollback()
|
||||
logger.error(f"Error applying repayment for schedule {schedule_id}: {e}")
|
||||
raise
|
||||
@classmethod
|
||||
def update_due_process_date_and_count(cls, schedule_id):
|
||||
"""
|
||||
Update the due process date to now and increment the due process count.
|
||||
"""
|
||||
try:
|
||||
schedule = cls.query.get(schedule_id)
|
||||
if not schedule:
|
||||
raise ValueError(f"Schedule with ID {schedule_id} does not exist.")
|
||||
if schedule.due_process_count is None:
|
||||
schedule.due_process_count = 0
|
||||
schedule.due_process_count += 1
|
||||
schedule.due_process_date = datetime.now(timezone.utc)
|
||||
schedule.updated_at = datetime.now(timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
# Calculate DPD
|
||||
dpd = max((now.date() - schedule.due_date.date()).days, 0)
|
||||
# Determine outstanding balance
|
||||
if schedule.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID and schedule.partial_balance:
|
||||
outstanding_amount = Decimal(str(schedule.partial_balance))
|
||||
else:
|
||||
outstanding_amount = Decimal(str(schedule.installment_amount))
|
||||
@classmethod
|
||||
def apply_penal_to_schedule(cls, schedule_id, penal_amount):
|
||||
|
||||
# Calculate penal charge (1% default if settings not set)
|
||||
try:
|
||||
penalty_rate = getattr(settings, "PENAL_CHARGE_PERCENTAGE", 1)/100
|
||||
except:
|
||||
penalty_rate = 0.01
|
||||
penal_charge = (outstanding_amount * Decimal(str(penalty_rate))).quantize(
|
||||
Decimal("0.01"), rounding=ROUND_HALF_UP
|
||||
)
|
||||
|
||||
#new balance + penal charge
|
||||
new_balance = outstanding_amount + penal_charge
|
||||
schedule = cls.query.get(schedule_id)
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"""Updated due process date and count for schedule ID {schedule.id},
|
||||
amount due: {schedule.installment_amount},
|
||||
outstanding amount: {outstanding_amount},
|
||||
due process count: {schedule.due_process_count},
|
||||
DPD: {dpd},
|
||||
penal charge(1%): {penal_charge},
|
||||
new balance(plus penal charge): {new_balance}
|
||||
""")
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
schedule.penal_count = (schedule.penal_count or 0) + 1
|
||||
schedule.penal_charge = (schedule.penal_charge or 0) + penal_amount
|
||||
schedule.last_penal_date = now
|
||||
schedule.due_process_date = now
|
||||
schedule.updated_at = now
|
||||
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating due process date and count for schedule {schedule.id}: {e}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def calculate_penal_charge(cls, schedule):
|
||||
|
||||
if schedule.paid_status == RepaymentScheduleStatus.PARTIALLY_PAID:
|
||||
outstanding = Decimal(str(schedule.partial_balance))
|
||||
else:
|
||||
outstanding = Decimal(str(schedule.installment_amount))
|
||||
|
||||
rate = Decimal(str(settings.PENAL_CHARGE_PERCENTAGE)) / 100
|
||||
|
||||
penal_charge = (outstanding * rate).quantize(
|
||||
Decimal("0.01"),
|
||||
rounding=ROUND_HALF_UP
|
||||
)
|
||||
|
||||
return penal_charge
|
||||
|
||||
|
||||
Reference in New Issue
Block a user