15.02.26
This commit is contained in:
@@ -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')
|
||||
|
||||
141
modules/purchase_trade/finance_tools.py
Normal file
141
modules/purchase_trade/finance_tools.py
Normal 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")
|
||||
Reference in New Issue
Block a user