This commit is contained in:
2026-02-15 12:47:07 +01:00
parent 9565e82850
commit 4059cd591e
2 changed files with 182 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ import logging
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
from trytond.modules.account.exceptions import PeriodNotFoundError
from trytond.modules.purchase_trade.finance_tools import InterestCalculator
logger = logging.getLogger(__name__)
@@ -56,6 +57,7 @@ class Fee(ModelSQL,ModelView):
('lumpsum', 'Lump sum'),
('perqt', 'Per qt'),
('pprice', '% price'),
('rate', '% Mth'),
('pcost', '% cost price'),
('ppack', 'Per packing'),
], 'Mode', required=True)
@@ -93,6 +95,13 @@ class Fee(ModelSQL,ModelView):
('brut', 'Gross'),
], string='W. type')
fee_date = fields.Date("Date")
@classmethod
def default_fee_date(cls):
Date = Pool().get('ir.date')
return Date.today()
@classmethod
def default_qt_state(cls):
LotQtType = Pool().get('lot.qt.type')
@@ -295,6 +304,7 @@ class Fee(ModelSQL,ModelView):
return Decimal(lqts[0].lot_quantity)
def get_amount(self,name=None):
Date = Pool().get('ir.date')
sign = Decimal(1)
if self.price:
# if self.p_r:
@@ -304,6 +314,37 @@ class Fee(ModelSQL,ModelView):
return self.price * sign
elif self.mode == 'ppack':
return round(self.price * self.quantity,2)
elif self.mode == 'rate':
#take period with estimated trigger date
if self.line:
if self.line.purchase.payment_term:
est_date = self.line.purchase.payment_term.get_date()
beg_date = self.fee_date if self.fee_date else Date.today()
factor = InterestCalculator.calculate(
start_date=beg_date,
end_date=est_date,
rate=self.price,
rate_type='annual',
convention='ACT/360',
compounding='simple'
)
return round(factor * self.line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
if self.sale_line:
if self.sale_line.sale.payment_term:
est_date = self.sale_line.sale.payment_term.get_date()
beg_date = self.fee_date if self.fee_date else Date.today()
factor = InterestCalculator.calculate(
start_date=beg_date,
end_date=est_date,
rate=self.price,
rate_type='annual',
convention='ACT/360',
compounding='simple'
)
return round(factor * self.sale_line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
elif self.mode == 'perqt':
if self.shipment_in:
StockMove = Pool().get('stock.move')

View File

@@ -0,0 +1,141 @@
from decimal import Decimal, getcontext
from datetime import date
from calendar import isleap
getcontext().prec = 28
class DayCount:
@staticmethod
def year_fraction(start_date, end_date, convention):
if end_date <= start_date:
return Decimal('0')
if convention == 'ACT/360':
return Decimal((end_date - start_date).days) / Decimal(360)
elif convention in ('ACT/365', 'ACT/365F'):
return Decimal((end_date - start_date).days) / Decimal(365)
elif convention == 'ACT/ACT_ISDA':
return DayCount._act_act_isda(start_date, end_date)
elif convention == '30/360_US':
return DayCount._30_360_us(start_date, end_date)
elif convention == '30E/360':
return DayCount._30e_360(start_date, end_date)
elif convention == '30E/360_ISDA':
return DayCount._30e_360_isda(start_date, end_date)
else:
raise ValueError(f"Unsupported convention {convention}")
# ---------- IMPLEMENTATIONS ----------
@staticmethod
def _act_act_isda(start_date, end_date):
total = Decimal('0')
current = start_date
while current < end_date:
year_end = date(current.year, 12, 31)
period_end = min(year_end.replace(day=31) +
(date(current.year + 1, 1, 1) - year_end),
end_date)
days_in_period = (period_end - current).days
days_in_year = 366 if isleap(current.year) else 365
total += Decimal(days_in_period) / Decimal(days_in_year)
current = period_end
return total
@staticmethod
def _30_360_us(d1, d2):
d1_day = 30 if d1.day == 31 else d1.day
if d1.day in (30, 31) and d2.day == 31:
d2_day = 30
else:
d2_day = d2.day
days = ((d2.year - d1.year) * 360 +
(d2.month - d1.month) * 30 +
(d2_day - d1_day))
return Decimal(days) / Decimal(360)
@staticmethod
def _30e_360(d1, d2):
d1_day = min(d1.day, 30)
d2_day = min(d2.day, 30)
days = ((d2.year - d1.year) * 360 +
(d2.month - d1.month) * 30 +
(d2_day - d1_day))
return Decimal(days) / Decimal(360)
@staticmethod
def _30e_360_isda(d1, d2):
d1_day = 30 if d1.day == 31 else d1.day
d2_day = 30 if d2.day == 31 else d2.day
days = ((d2.year - d1.year) * 360 +
(d2.month - d1.month) * 30 +
(d2_day - d1_day))
return Decimal(days) / Decimal(360)
class InterestCalculator:
@staticmethod
def calculate(
start_date,
end_date,
rate,
rate_type='annual', # 'annual' or 'monthly'
convention='ACT/360',
compounding='simple', # simple, annual, monthly, continuous
):
"""
Retourne le facteur d'intérêt (pas le montant).
"""
if not start_date or not end_date:
return Decimal('0')
if end_date <= start_date:
return Decimal('0')
rate = Decimal(str(rate))
# Conversion en taux annuel si besoin
if rate_type == 'monthly':
annual_rate = rate * Decimal(12)
else:
annual_rate = rate
yf = DayCount.year_fraction(start_date, end_date, convention)
if compounding == 'simple':
return annual_rate * yf
elif compounding == 'annual':
return (Decimal(1) + annual_rate) ** yf - Decimal(1)
elif compounding == 'monthly':
monthly_rate = annual_rate / Decimal(12)
months = yf * Decimal(12)
return (Decimal(1) + monthly_rate) ** months - Decimal(1)
elif compounding == 'continuous':
from math import exp
return Decimal(exp(float(annual_rate * yf))) - Decimal(1)
else:
raise ValueError("Unsupported compounding mode")