main #7
@@ -334,7 +334,7 @@ class Fee(ModelSQL,ModelView):
|
|||||||
if self.sale_line:
|
if self.sale_line:
|
||||||
if self.sale_line.sale.payment_term:
|
if self.sale_line.sale.payment_term:
|
||||||
beg_date = self.fee_date if self.fee_date else Date.today()
|
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:
|
if est_date and beg_date:
|
||||||
factor = InterestCalculator.calculate(
|
factor = InterestCalculator.calculate(
|
||||||
start_date=beg_date,
|
start_date=beg_date,
|
||||||
|
|||||||
259
modules/purchase_trade/financing_tools.py
Normal file
259
modules/purchase_trade/financing_tools.py
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user