diff --git a/account_interests/__manifest__.py b/account_interests/__manifest__.py
index 68fe21fe1..735c5d5d3 100644
--- a/account_interests/__manifest__.py
+++ b/account_interests/__manifest__.py
@@ -19,7 +19,7 @@
##############################################################################
{
'name': 'Interests Management',
- 'version': "18.0.1.0.0",
+ 'version': "18.0.1.1.0",
'category': 'Accounting',
'sequence': 14,
'summary': 'Calculate interests for selected partners',
diff --git a/account_interests/models/res_company_interest.py b/account_interests/models/res_company_interest.py
index 6c0b19bcb..fd254d833 100644
--- a/account_interests/models/res_company_interest.py
+++ b/account_interests/models/res_company_interest.py
@@ -65,11 +65,6 @@ class ResCompanyInterest(models.Model):
default=1,
help="Repeat every (Days/Week/Month/Year)"
)
- tolerance_interval = fields.Integer(
- 'Tolerance',
- default=1,
- help="Number of periods of tolerance for dues. 0 = no tolerance"
- )
next_date = fields.Date(
'Date of Next Invoice',
default=fields.Date.today,
@@ -81,6 +76,8 @@ class ResCompanyInterest(models.Model):
)
has_domain = fields.Boolean(compute="_compute_has_domain")
+ late_payment_interest = fields.Boolean('Late payment interest', default=False, help="The interest calculation takes into account all late payments from the previous period. To obtain the daily rate, the interest is divided by the period. These days are considered depending on the type of period: 360 for annual, 30 for monthly and 7 for weekly.")
+
@api.model
def _cron_recurring_interests_invoices(self):
_logger.info('Running Interest Invoices Cron Job')
@@ -91,8 +88,8 @@ def _cron_recurring_interests_invoices(self):
try:
rec.create_interest_invoices()
rec.env.cr.commit()
- except:
- _logger.error('Error creating interest invoices for company: %s', rec.company_id.name)
+ except Exception as e:
+ _logger.error('Error creating interest invoices for company: %s, %s', rec.company_id.name, str(e))
companies_with_errors.append(rec.company_id.name)
rec.env.cr.rollback()
@@ -101,6 +98,19 @@ def _cron_recurring_interests_invoices(self):
error_message = _("We couldn't run interest invoices cron job in the following companies: %s.") % company_names
raise UserError(error_message)
+ def _calculate_date_deltas(self, rule_type, interval):
+ """
+ Calcula los intervalos de fechas para la generación de intereses.
+ """
+ deltas = {
+ 'daily': relativedelta(days=interval),
+ 'weekly': relativedelta(weeks=interval),
+ 'monthly': relativedelta(months=interval),
+ 'yearly': relativedelta(years=interval),
+ }
+ return deltas.get(rule_type, relativedelta(months=interval))
+
+
def create_interest_invoices(self):
for rec in self:
_logger.info(
@@ -108,53 +118,120 @@ def create_interest_invoices(self):
rec.company_id.name)
# hacemos un commit para refrescar cache
self.env.cr.commit()
- interests_date = rec.next_date
+ to_date = rec.next_date
rule_type = rec.rule_type
interval = rec.interval
- tolerance_interval = rec.tolerance_interval
-
- if rule_type == 'daily':
- next_delta = relativedelta(days=+interval)
- tolerance_delta = relativedelta(days=+tolerance_interval)
- elif rule_type == 'weekly':
- next_delta = relativedelta(weeks=+interval)
- tolerance_delta = relativedelta(weeks=+tolerance_interval)
- elif rule_type == 'monthly':
- next_delta = relativedelta(months=+interval)
- tolerance_delta = relativedelta(months=+tolerance_interval)
- else:
- next_delta = relativedelta(years=+interval)
- tolerance_delta = relativedelta(years=+tolerance_interval)
-
- # buscamos solo facturas que vencieron
- # antes de hoy menos un periodo
- # TODO ver si queremos que tambien se calcule interes proporcional
- # para lo que vencio en este ultimo periodo
- to_date = interests_date - tolerance_delta
- from_date = to_date - tolerance_delta
+
+ next_delta = self._calculate_date_deltas(rule_type, interval)
+ from_date_delta = self._calculate_date_deltas(rule_type, -interval)
+
+ from_date = to_date + from_date_delta
+
# llamamos a crear las facturas con la compañia del interes para
# que tome correctamente las cuentas
rec.with_company(rec.company_id).with_context(default_l10n_ar_afip_asoc_period_start=from_date,
- default_l10n_ar_afip_asoc_period_end=to_date).create_invoices(to_date)
+ default_l10n_ar_afip_asoc_period_end=to_date).create_invoices(from_date, to_date)
# seteamos proxima corrida en hoy mas un periodo
- rec.next_date = interests_date + next_delta
+ rec.next_date = to_date + next_delta
- def _get_move_line_domains(self, to_date):
+ def _get_move_line_domains(self):
self.ensure_one()
move_line_domain = [
('account_id', 'in', self.receivable_account_ids.ids),
- ('full_reconcile_id', '=', False),
- ('date_maturity', '<', to_date),
('partner_id.active', '=', True),
('parent_state', '=', 'posted'),
]
return move_line_domain
- def create_invoices(self, to_date, groupby=['partner_id']):
+ def _update_deuda(self, deuda, partner, key, value):
+ """
+ Actualiza el diccionario de deuda para un partner específico.
+ Si el partner no existe en la deuda, lo inicializa.
+ Si la clave no existe para el partner, la agrega.
+ """
+ if partner not in deuda:
+ deuda[partner] = {}
+ deuda[partner][key] = deuda[partner].get(key, 0) + value
+
+ def _calculate_debts(self, from_date, to_date, groupby=['partner_id']):
+ """
+ Calcula las deudas e intereses por partner.
+ Retorna un diccionario estructurado con los cálculos.
+ """
+ deuda = {}
+
+ interest_rate = {
+ 'daily': 1,
+ 'weekly': 7,
+ 'monthly': 30,
+ 'yearly': 360,
+ }
+
+ # Deudas de períodos anteriores
+ previous_grouped_lines = self.env['account.move.line']._read_group(
+ domain=self._get_move_line_domains() + [('full_reconcile_id', '=', False), ('date_maturity', '<', from_date)],
+ groupby=groupby,
+ aggregates=['amount_residual:sum'],
+ )
+ for x in previous_grouped_lines:
+ self._update_deuda(deuda, x[0], 'Deuda periodos anteriores', x[1] * self.rate)
+
+ # Intereses por el último período
+ last_period_lines = self.env['account.move.line'].search(
+ self._get_move_line_domains() + [('amount_residual', '>', 0), ('date_maturity', '>=', from_date), ('date_maturity', '<', to_date)]
+ )
+ for partner, amls in last_period_lines.grouped('partner_id').items():
+ interest = sum(
+ move.amount_residual * ((to_date - move.invoice_date_due).days - 1) * (self.rate / interest_rate[self.rule_type])
+ for move, lines in amls.grouped('move_id').items()
+ )
+ self._update_deuda(deuda, partner, 'Deuda último periodo', interest)
+
+ # Intereses por pagos tardíos
+ if self.late_payment_interest:
+
+ partials = self.env['account.partial.reconcile'].search([
+ # lo dejamos para NTH
+ # debit_move_id. safe eval domain
+ ('debit_move_id.partner_id.active', '=', True),
+ ('debit_move_id.date_maturity', '>=', from_date),
+ ('debit_move_id.date_maturity', '<=', to_date),
+ ('debit_move_id.parent_state', '=', 'posted'),
+ ('debit_move_id.account_id', 'in', self.receivable_account_ids.ids),
+ ('credit_move_id.date', '>=', from_date),
+ ('credit_move_id.date', '<', to_date)]).grouped('debit_move_id')
+
+ for move_line, parts in partials.items():
+ due_payments = parts.filtered(lambda x: x.credit_move_id.date > x.debit_move_id.date_maturity)
+ interest = 0
+ if due_payments:
+ due_payments_amount = sum(due_payments.mapped('amount'))
+ last_date_payment = parts.filtered(lambda x: x.credit_move_id.date > x.debit_move_id.date_maturity).sorted('max_date')[-1].max_date
+ days = (last_date_payment - move_line.date_maturity).days
+ interest += due_payments_amount * days * (self.rate / interest_rate[self.rule_type])
+ self._update_deuda(deuda, move_line.partner_id, 'Deuda pagos vencidos', interest)
+
+ return deuda
+
+ def create_invoices(self, from_date, to_date):
+ """
+ Crea facturas de intereses a cada partner basadas en los cálculos de deuda.
+ Ejemplo:
+ Tengo deudas viejas por 2000 (super viejas)
+ el 1 facturo 1000 que vencen el 20
+ el 25 pagó 400.
+ Detalle de cálculo de intereses:
+ * interés por todo lo viejo (2000) x el rate
+ * interés de todo lo que venció en el último período ($600) x días que estuvo vencido (10 días)
+ * si además marcó "late payment interest" se agrega interés por los días que pagó tarde, es decir $400 x 5 días
+ """
self.ensure_one()
+ # Calcular deudas e intereses
+ deuda = self._calculate_debts(from_date, to_date)
+
journal = self.env['account.journal'].search([
('type', '=', 'sale'),
('company_id', '=', self.company_id.id)], limit=1)
@@ -167,32 +244,18 @@ def create_invoices(self, to_date, groupby=['partner_id']):
if self.domain:
move_line_domain += safe_eval(self.domain)
- fields = ['id:recordset', 'amount_residual:sum', 'partner_id:recordset', 'account_id:recordset']
-
- move_line = self.env['account.move.line']
- grouped_lines = move_line._read_group(
- domain=move_line_domain,
- groupby=groupby,
- aggregates=fields,
- )
- self = self.with_context(
- company_id=self.company_id.id,
- mail_notrack=True,
- prefetch_fields=False).with_company(self.company_id)
-
- total_items = len(grouped_lines)
+ total_items = len(deuda)
_logger.info('%s interest invoices will be generated', total_items)
- for idx, line in enumerate(grouped_lines):
- move_vals = self._prepare_interest_invoice(
- line, to_date, journal)
+ # Crear facturas
+ for idx, partner in enumerate(deuda):
+ move_vals = self._prepare_interest_invoice(partner, deuda[partner], to_date, journal)
if not move_vals:
continue
- _logger.info('Creating Interest Invoice (%s of %s) with values:\n%s', idx + 1, total_items, line)
+ _logger.info('Creating Interest Invoice (%s of %s) for partner ID: %s', idx + 1, total_items, partner.id)
move = self.env['account.move'].create(move_vals)
-
if self.automatic_validation:
try:
move.action_post()
@@ -201,7 +264,10 @@ def create_invoices(self, to_date, groupby=['partner_id']):
"Something went wrong creating "
"interests invoice: {}".format(e))
- def prepare_info(self, to_date, debt):
+
+
+
+ def _prepare_info(self, to_date):
self.ensure_one()
# Format date to customer language
@@ -211,23 +277,18 @@ def prepare_info(self, to_date, debt):
to_date_format = to_date.strftime(date_format)
res = _(
- 'Deuda Vencida al %s: %s\n'
- 'Tasa de interés: %s') % (
- to_date_format, debt, self.rate)
+ 'Deuda Vencida al %s con tasa de interés de %s') % (
+ to_date_format, self.rate)
return res
- def _prepare_interest_invoice(self, line, to_date, journal):
+ def _prepare_interest_invoice(self, partner, debt, to_date, journal):
+ """
+ Retorna un diccionario con los datos para crear la factura
+ """
self.ensure_one()
- debt = line[2]
-
- if not debt or debt <= 0.0:
- _logger.info("Debt is negative, skipping...")
- return
- partner_id = line[0].id
- partner = self.env['res.partner'].browse(partner_id)
- comment = self.prepare_info(to_date, debt)
+ comment = self._prepare_info(to_date)
fpos = partner.property_account_position_id
taxes = self.interest_product_id.taxes_id.filtered(
lambda r: r.company_id == self.company_id)
@@ -243,16 +304,16 @@ def _prepare_interest_invoice(self, line, to_date, journal):
'invoice_origin': "Interests Invoice",
'invoice_payment_term_id': False,
'narration': self.interest_product_id.name + '.\n' + comment,
- 'is_move_sent': True,
- 'invoice_line_ids': [(0, 0, {
+ 'invoice_line_ids': [(0, 0,
+ {
"product_id": self.interest_product_id.id,
"quantity": 1.0,
- "price_unit": self.rate * debt,
+ "price_unit": value,
"partner_id": partner.id,
- "name": self.interest_product_id.name + '.\n' + comment,
+ "name": self.interest_product_id.name + '.\n' + key,
"analytic_distribution": {self.analytic_account_id.id: 100.0} if self.analytic_account_id.id else False,
- "tax_ids": [(6, 0, tax_id.ids)],
- })],
+ "tax_ids": [(6, 0, tax_id.ids)]
+ }) for key, value in debt.items() if isinstance(value, (int, float)) and value > 0],
}
# hack para evitar modulo glue con l10n_latam_document
diff --git a/account_interests/views/res_company_views.xml b/account_interests/views/res_company_views.xml
index 7f3f14c43..018718924 100644
--- a/account_interests/views/res_company_views.xml
+++ b/account_interests/views/res_company_views.xml
@@ -20,7 +20,6 @@