Skip to content

Commit

Permalink
Cherry pick of e121812 failed (#631)
Browse files Browse the repository at this point in the history
stdout:
Auto-merging account_interests/views/res_company_views.xml
Auto-merging account_interests/models/res_company_interest.py
Auto-merging account_interests/__manifest__.py
CONFLICT (content): Merge conflict in account_interests/__manifest__.py

stderr:
10:41:54.010297 git.c:444               trace: built-in: git cherry-pick e121812
error: could not apply e121812... [ADD] account_interests: late payment interest feature
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
----------
status:

Co-authored-by: fw-bot-adhoc <>
  • Loading branch information
fw-bot-adhoc authored Jan 9, 2025
1 parent ed4db8c commit b9d25da
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 76 deletions.
2 changes: 1 addition & 1 deletion account_interests/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
207 changes: 134 additions & 73 deletions account_interests/models/res_company_interest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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')
Expand All @@ -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()

Expand All @@ -101,60 +98,140 @@ 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(
'Creating Interest Invoices (id: %s, company: %s)', rec.id,
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)
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
3 changes: 1 addition & 2 deletions account_interests/views/res_company_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
<field name="interval"/>
<field name="rule_type"/>
<field name="next_date"/>
<field name="tolerance_interval"/>
<field name="has_domain"/>
</list>
<form>
Expand All @@ -37,7 +36,7 @@
<field name="interval"/>
<field name="rule_type"/>
<field name="next_date"/>
<field name="tolerance_interval"/>
<field name="late_payment_interest"/>
</group>
<field name="domain" widget="domain" options="{ 'model': 'account.move.line', 'in_dialog': True }" />
</group>
Expand Down

0 comments on commit b9d25da

Please sign in to comment.