This commit is contained in:
2026-02-15 14:33:14 +01:00
parent 479e0d4d5a
commit 10b370f11b
2 changed files with 260 additions and 1 deletions

View File

@@ -334,7 +334,7 @@ class Fee(ModelSQL,ModelView):
if self.sale_line:
if self.sale_line.sale.payment_term:
beg_date = self.fee_date if self.fee_date else Date.today()
est_date = self.sale_line.sale.payment_term.get_date(beg_date)
est_date = self.sale_line.sale.payment_term[0].get_date(beg_date)
if est_date and beg_date:
factor = InterestCalculator.calculate(
start_date=beg_date,

View File

@@ -0,0 +1,259 @@
from decimal import Decimal, getcontext
from datetime import datetime, date
from calendar import isleap
from typing import Callable, Dict
import uuid
getcontext().prec = 28
# {
# "computation_type": "INTEREST_ACCRUAL",
# "input": {
# "start_date": "2026-01-01",
# "end_date": "2026-06-30",
# "notional": "1000000",
# "rate": {
# "value": "0.08",
# "type": "ANNUAL"
# },
# "day_count_convention": "ACT/360",
# "compounding_method": "SIMPLE"
# }
# }
# result = FinancialComputationService.execute(payload)
# interest = Decimal(result["result"]["interest_amount"])
# interest = currency.round(interest)
# ============================================================
# VERSIONING
# ============================================================
ENGINE_VERSION = "1.0.0"
# ============================================================
# REGISTRY (PLUGIN SYSTEM)
# ============================================================
DAY_COUNT_REGISTRY: Dict[str, Callable] = {}
COMPOUNDING_REGISTRY: Dict[str, Callable] = {}
def register_day_count(name: str):
def decorator(func):
DAY_COUNT_REGISTRY[name] = func
return func
return decorator
def register_compounding(name: str):
def decorator(func):
COMPOUNDING_REGISTRY[name] = func
return func
return decorator
# ============================================================
# DOMAIN DAY COUNT CONVENTIONS
# ============================================================
@register_day_count("ACT/360")
def act_360(start: date, end: date) -> Decimal:
return Decimal((end - start).days) / Decimal(360)
@register_day_count("ACT/365F")
def act_365f(start: date, end: date) -> Decimal:
return Decimal((end - start).days) / Decimal(365)
@register_day_count("ACT/ACT_ISDA")
def act_act_isda(start: date, end: date) -> Decimal:
total = Decimal("0")
current = start
while current < end:
year_end = date(current.year, 12, 31)
next_year = date(current.year + 1, 1, 1)
period_end = min(next_year, end)
days = (period_end - current).days
year_days = 366 if isleap(current.year) else 365
total += Decimal(days) / Decimal(year_days)
current = period_end
return total
@register_day_count("30E/360")
def thirty_e_360(start: date, end: date) -> Decimal:
d1 = min(start.day, 30)
d2 = min(end.day, 30)
days = (
(end.year - start.year) * 360 +
(end.month - start.month) * 30 +
(d2 - d1)
)
return Decimal(days) / Decimal(360)
# ============================================================
# DOMAIN COMPOUNDING STRATEGIES
# ============================================================
@register_compounding("SIMPLE")
def simple(rate: Decimal, yf: Decimal) -> Decimal:
return rate * yf
@register_compounding("ANNUAL")
def annual(rate: Decimal, yf: Decimal) -> Decimal:
return (Decimal(1) + rate) ** yf - Decimal(1)
@register_compounding("MONTHLY")
def monthly(rate: Decimal, yf: Decimal) -> Decimal:
monthly_rate = rate / Decimal(12)
months = yf * Decimal(12)
return (Decimal(1) + monthly_rate) ** months - Decimal(1)
@register_compounding("CONTINUOUS")
def continuous(rate: Decimal, yf: Decimal) -> Decimal:
from math import exp
return Decimal(exp(float(rate * yf))) - Decimal(1)
# ============================================================
# DOMAIN INTEREST COMPUTATION OBJECT
# ============================================================
class InterestComputation:
def __init__(
self,
start_date: date,
end_date: date,
notional: Decimal,
rate_value: Decimal,
rate_type: str,
day_count: str,
compounding: str,
):
self.start_date = start_date
self.end_date = end_date
self.notional = notional
self.rate_value = rate_value
self.rate_type = rate_type
self.day_count = day_count
self.compounding = compounding
def compute(self):
if self.end_date <= self.start_date:
raise ValueError("end_date must be after start_date")
if self.day_count not in DAY_COUNT_REGISTRY:
raise ValueError("Unsupported day count convention")
if self.compounding not in COMPOUNDING_REGISTRY:
raise ValueError("Unsupported compounding method")
yf = DAY_COUNT_REGISTRY[self.day_count](
self.start_date,
self.end_date
)
# Normalize rate to annual
if self.rate_type == "MONTHLY":
annual_rate = self.rate_value * Decimal(12)
else:
annual_rate = self.rate_value
factor = COMPOUNDING_REGISTRY[self.compounding](
annual_rate,
yf
)
interest_amount = self.notional * factor
return {
"year_fraction": yf,
"interest_factor": factor,
"interest_amount": interest_amount,
}
# ============================================================
# APPLICATION LAYER JSON SERVICE (Camunda Ready)
# ============================================================
class FinancialComputationService:
@staticmethod
def execute(payload: dict) -> dict:
"""
Stateless JSON entrypoint.
Compatible Camunda / REST / Tryton bridge.
"""
try:
request_id = str(uuid.uuid4())
input_data = payload["input"]
start_date = datetime.strptime(
input_data["start_date"], "%Y-%m-%d"
).date()
end_date = datetime.strptime(
input_data["end_date"], "%Y-%m-%d"
).date()
notional = Decimal(input_data["notional"])
rate_value = Decimal(input_data["rate"]["value"])
rate_type = input_data["rate"]["type"].upper()
day_count = input_data["day_count_convention"]
compounding = input_data["compounding_method"]
computation = InterestComputation(
start_date=start_date,
end_date=end_date,
notional=notional,
rate_value=rate_value,
rate_type=rate_type,
day_count=day_count,
compounding=compounding,
)
result = computation.compute()
return {
"metadata": {
"engine_version": ENGINE_VERSION,
"request_id": request_id,
"timestamp": datetime.utcnow().isoformat() + "Z"
},
"result": {
"year_fraction": str(result["year_fraction"]),
"interest_factor": str(result["interest_factor"]),
"interest_amount": str(result["interest_amount"]),
},
"explainability": {
"formula": "Interest = Notional × Factor",
"factor_definition": f"{compounding} compounding applied to annualized rate",
"day_count_used": day_count
},
"status": "SUCCESS"
}
except Exception as e:
return {
"status": "ERROR",
"message": str(e),
"engine_version": ENGINE_VERSION
}