Files
enterprise-addons/account_transfer/models/transfer_model.py
T

252 lines
13 KiB
Python

import ast
from datetime import date
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Domain
from odoo.tools import groupby
from odoo.tools.date_utils import get_timedelta
from odoo.tools.float_utils import float_is_zero, float_round
class AccountTransferModel(models.Model):
_name = 'account.transfer.model'
_description = "Account Transfer Model"
# DEFAULTS
def _get_default_date_start(self):
company = self.env.company
return company.compute_fiscalyear_dates(date.today())['date_from'] if company else None
def _get_default_journal(self):
return self.env['account.journal'].search([
*self.env['account.journal']._check_company_domain(self.env.company),
('type', '=', 'general'),
], limit=1)
name = fields.Char(required=True)
active = fields.Boolean(default=True)
journal_id = fields.Many2one('account.journal', required=True, string="Destination Journal", default=_get_default_journal)
company_id = fields.Many2one('res.company', readonly=True, related='journal_id.company_id')
date_start = fields.Date(string="Start Date", required=True, default=_get_default_date_start)
date_stop = fields.Date(string="Stop Date", required=False)
frequency = fields.Selection([('month', 'Monthly'), ('quarter', 'Quarterly'), ('year', 'Yearly')], required=True, default='month')
account_ids = fields.Many2many('account.account', 'account_model_rel', string="Origin Accounts", domain="[('account_type', '!=', 'off_balance')]")
line_ids = fields.One2many('account.transfer.model.line', 'transfer_model_id', string="Destination Accounts")
move_ids = fields.One2many('account.move', 'transfer_model_id', string="Generated Moves")
move_ids_count = fields.Integer(compute="_compute_move_ids_count")
has_draft_moves = fields.Boolean(compute="_compute_has_draft_moves")
total_percent = fields.Float(compute="_compute_total_percent", string="Total Percent", readonly=True)
state = fields.Selection([('disabled', 'Disabled'), ('in_progress', 'In Progress')], default='disabled', required=True)
conditions = fields.Char(
string='Conditions',
default='[]',
help="A domain to filter the accounts from which the resulting entry will transfer amounts.",
)
def copy(self, default=None):
new_models = super().copy(default)
for old_model, new_model in zip(self, new_models):
new_model.account_ids += old_model.account_ids
old_model.line_ids.copy({'transfer_model_id': new_model.id})
return new_models
@api.ondelete(at_uninstall=False)
def _unlink_with_check_moves(self):
# Only unlink a transfer that has no posted/draft moves attached.
for transfer in self:
if transfer.move_ids_count > 0:
posted_moves = any(move.state == 'posted' for move in transfer.move_ids)
if posted_moves:
raise UserError(_("You cannot delete a transfer model that has posted moves attached ('%s').", transfer.name))
draft_moves = any(move.state == 'draft' for move in transfer.move_ids)
if draft_moves:
raise UserError(_("You cannot delete a transfer model that has draft moves attached ('%s'). "
"Please delete them before deleting this transfer.", transfer.name))
@api.depends('move_ids')
def _compute_move_ids_count(self):
"""Compute the amount of move ids have been generated by this transfer model."""
for record in self:
record.move_ids_count = len(record.move_ids)
@api.depends('move_ids')
def _compute_has_draft_moves(self):
"""Compute if there are any draft moves generated by this transfer model."""
for record in self:
record.has_draft_moves = any(move.state == 'draft' for move in record.move_ids)
@api.constrains('line_ids')
def _check_line_ids_percent(self):
"""Check that the total percent is not bigger than 100.0."""
for record in self:
if not 0 < record.total_percent <= 100.0:
raise ValidationError(_('The total percentage (%s) should be less or equal to 100!', record.total_percent))
@api.depends('line_ids')
def _compute_total_percent(self):
"""Compute the total percentage of all lines linked to this model."""
for record in self:
total_percent = sum(record.line_ids.filtered(lambda line: line.account_id).mapped('percent'))
record.total_percent = float_round(total_percent, precision_digits=6)
def action_enable(self):
"""Put this move model in 'in progress' state."""
return self.write({'state': 'in_progress'})
def action_disable(self):
"""Put this move model in 'disabled' state."""
return self.write({'state': 'disabled'})
def action_archive(self):
self.action_disable()
return super().action_archive()
@api.model
def action_cron_auto_transfer(self):
"""Compute the transfer entries for the all active move models."""
self.search([('state', '=', 'in_progress')]).action_perform_auto_transfer()
def action_perform_auto_transfer(self):
"""Compute the transfer entries for the current recordset of models."""
for record in self:
# If no account to ventilate or no account to ventilate into : nothing to do
if record.account_ids and record.line_ids:
today = date.today()
max_date = record.date_stop and min(today, record.date_stop) or today
start_date = record._determine_start_date()
next_move_date = record._get_next_move_date(start_date)
# (Re)Generate moves in draft untill today
# Journal entries will be recomputed everyday until posted.
while next_move_date <= max_date:
record._create_or_update_move_for_period(start_date, next_move_date)
start_date = next_move_date + relativedelta(days=1)
next_move_date = record._get_next_move_date(start_date)
# (Re)Generate move for one more period if needed
if not record.date_stop:
record._create_or_update_move_for_period(start_date, next_move_date)
elif today < record.date_stop:
record._create_or_update_move_for_period(start_date, min(next_move_date, record.date_stop))
return False
def _create_or_update_move_for_period(self, start_date, end_date):
"""
Create or update a transfer entry for a given period. This means (re)generate all the required moves and their
associated move lines in order to execute the transfers.
:param start_date: the start date of the targeted period
:param end_date: the end date of the targeted period
:return: the created (or updated) move
"""
self.ensure_one()
current_move = self._get_move_for_period(end_date)
if current_move and current_move.state != 'draft':
return current_move
line_values = self._get_transfer_move_lines_values(start_date, end_date)
if line_values:
if current_move is None:
current_move = self.env['account.move'].create({
'ref': '%s: %s --> %s' % (self.name, str(start_date), str(end_date)),
'date': end_date,
'journal_id': self.journal_id.id,
'transfer_model_id': self.id,
})
line_ids_values = [(0, 0, value) for value in line_values]
# unlink all old line ids
current_move.line_ids.unlink()
# recreate line ids
current_move.write({'line_ids': line_ids_values})
return current_move
def _get_move_for_period(self, end_date):
"""Get the generated move for a given period.
:param end_date: the end date of the wished period, do not need the start date as the move will always be
generated with end date of a period as date
:return: a recordset containing the move found if any, else None
"""
self.ensure_one()
# Move will always be generated with end_date of a period as date
domain = Domain('date', '=', end_date) & Domain('transfer_model_id', '=', self.id)
current_moves = self.env['account.move'].search(domain, limit=1, order="date desc")
return current_moves[0] if current_moves else None
def _determine_start_date(self):
"""Determine the transfer start date which is the most recent between: the global lock date, the hard lock date and the start date of the model."""
self.ensure_one()
global_lock_date = self.company_id.fiscalyear_lock_date or fields.Date.to_date('1900-01-01')
hard_lock_date = self.company_id.hard_lock_date or fields.Date.to_date('1900-01-01')
return max(global_lock_date.replace(day=1), hard_lock_date.replace(day=1), self.date_start)
def _get_next_move_date(self, date):
"""Compute the date of the next transfer entry with date and frequency."""
self.ensure_one()
quantity = 3 if self.frequency == 'quarter' else 1
granularity = 'month' if self.frequency in ['month', 'quarter'] else 'year'
delta = get_timedelta(quantity, granularity)
return date + delta - relativedelta(days=1)
def _get_transfer_move_lines_values(self, start_date, end_date):
"""
Get values to create the move to perform all needed transfers between accounts linked to current recordset
for a given period
:param start_date: the start date of the targeted period
:param end_date: the end date of the targeted period
:return: a list containing all the values needed to create the needed transfers
:rtype: list
"""
self.ensure_one()
transferred_amls = self.env['account.move.line'].search([
('account_id', 'in', self.account_ids.ids),
('date', '>=', start_date),
('date', '<=', end_date),
('parent_state', '=', 'posted'),
*ast.literal_eval(self.conditions),
])
if not transferred_amls:
return []
values = []
currency_round = self.company_id.currency_id.round
transferred_lines_by_account = dict(groupby(transferred_amls, lambda aml: aml.account_id.id))
transferred_lines_total_balance = sum(transferred_amls.mapped('balance'))
amount_to_transfer = 0
for source_account in self.account_ids:
transferred_lines_balance = sum(transferred_line.balance for transferred_line in transferred_lines_by_account.get(source_account.id, []))
if not float_is_zero(transferred_lines_balance, precision_digits=6):
percentage = sum(self.line_ids.mapped('percent'))
amount = currency_round(transferred_lines_balance * (percentage / 100.0))
values.append({
'name': _("Transfer (%(percent)s%% from %(account)s)", percent=percentage, account=source_account.name),
'account_id': source_account.id,
'balance': -amount,
'date_maturity': end_date,
})
amount_to_transfer += amount
for transfer_line in self.line_ids:
destination_account = transfer_line.account_id
if destination_account == self.line_ids.mapped('account_id')[-1]:
# The last destination line takes whatever remains to transfer instead of computing it in %, to avoid rounding issues.
# The sum of all destination amounts must always match the amount removed from the source accounts,
# regardless of whether the destination percentages add up to 100%.
amount = amount_to_transfer
else:
amount = transferred_lines_total_balance * (transfer_line.percent / 100.0)
amount = currency_round(amount)
if not float_is_zero(amount, precision_digits=6):
values.append({
'name': _("Transfer (-%(percent)s%% to %(account)s)", percent=transfer_line.percent, account=destination_account.name),
'account_id': destination_account.id,
'balance': amount,
'date_maturity': end_date,
})
amount_to_transfer -= amount
return values