15.02.26
This commit is contained in:
@@ -19,6 +19,7 @@ import logging
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from trytond.exceptions import UserWarning, UserError
|
from trytond.exceptions import UserWarning, UserError
|
||||||
from trytond.modules.account.exceptions import PeriodNotFoundError
|
from trytond.modules.account.exceptions import PeriodNotFoundError
|
||||||
|
from trytond.modules.purchase_trade.finance_tools import InterestCalculator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ class Fee(ModelSQL,ModelView):
|
|||||||
('lumpsum', 'Lump sum'),
|
('lumpsum', 'Lump sum'),
|
||||||
('perqt', 'Per qt'),
|
('perqt', 'Per qt'),
|
||||||
('pprice', '% price'),
|
('pprice', '% price'),
|
||||||
|
('rate', '% Mth'),
|
||||||
('pcost', '% cost price'),
|
('pcost', '% cost price'),
|
||||||
('ppack', 'Per packing'),
|
('ppack', 'Per packing'),
|
||||||
], 'Mode', required=True)
|
], 'Mode', required=True)
|
||||||
@@ -93,6 +95,13 @@ class Fee(ModelSQL,ModelView):
|
|||||||
('brut', 'Gross'),
|
('brut', 'Gross'),
|
||||||
], string='W. type')
|
], string='W. type')
|
||||||
|
|
||||||
|
fee_date = fields.Date("Date")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_fee_date(cls):
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
|
return Date.today()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_qt_state(cls):
|
def default_qt_state(cls):
|
||||||
LotQtType = Pool().get('lot.qt.type')
|
LotQtType = Pool().get('lot.qt.type')
|
||||||
@@ -295,6 +304,7 @@ class Fee(ModelSQL,ModelView):
|
|||||||
return Decimal(lqts[0].lot_quantity)
|
return Decimal(lqts[0].lot_quantity)
|
||||||
|
|
||||||
def get_amount(self,name=None):
|
def get_amount(self,name=None):
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
sign = Decimal(1)
|
sign = Decimal(1)
|
||||||
if self.price:
|
if self.price:
|
||||||
# if self.p_r:
|
# if self.p_r:
|
||||||
@@ -304,6 +314,37 @@ class Fee(ModelSQL,ModelView):
|
|||||||
return self.price * sign
|
return self.price * sign
|
||||||
elif self.mode == 'ppack':
|
elif self.mode == 'ppack':
|
||||||
return round(self.price * self.quantity,2)
|
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':
|
elif self.mode == 'perqt':
|
||||||
if self.shipment_in:
|
if self.shipment_in:
|
||||||
StockMove = Pool().get('stock.move')
|
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