19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:07 +01:00
parent ba20ce7443
commit 768b70e05e
2357 changed files with 1057103 additions and 712486 deletions

View file

@ -2,14 +2,19 @@
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools.misc import format_date, formatLang
from odoo.tools.float_utils import float_repr
from odoo.tools import groupby
from collections import defaultdict
from odoo.tools import groupby, frozendict
from markupsafe import Markup, escape
from odoo.tools import frozendict
import json
class AutomaticEntryWizard(models.TransientModel):
class AccountAutomaticEntryWizard(models.TransientModel):
_name = 'account.automatic.entry.wizard'
_description = 'Create Automatic Entries'
_check_company_auto = True
# General
action = fields.Selection([('change_period', 'Change Period'), ('change_account', 'Change Account')], required=True)
@ -22,7 +27,8 @@ class AutomaticEntryWizard(models.TransientModel):
percentage = fields.Float("Percentage", compute='_compute_percentage', readonly=False, store=True, help="Percentage of each line to execute the action on.")
total_amount = fields.Monetary(compute='_compute_total_amount', store=True, readonly=False, currency_field='company_currency_id', help="Total amount impacted by the automatic entry.")
journal_id = fields.Many2one('account.journal', required=True, readonly=False, string="Journal",
domain="[('company_id', '=', company_id), ('type', '=', 'general')]",
check_company=True,
domain="[('type', '=', 'general')]",
compute="_compute_journal_id",
inverse="_inverse_journal_id",
help="Journal where to create the entry.")
@ -30,23 +36,21 @@ class AutomaticEntryWizard(models.TransientModel):
# change period
account_type = fields.Selection([('income', 'Revenue'), ('expense', 'Expense')], compute='_compute_account_type', store=True)
expense_accrual_account = fields.Many2one('account.account', readonly=False,
domain="[('company_id', '=', company_id),"
"('account_type', 'not in', ('asset_receivable', 'liability_payable')),"
"('is_off_balance', '=', False)]",
check_company=True,
domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'off_balance'))]",
compute="_compute_expense_accrual_account",
inverse="_inverse_expense_accrual_account",
)
revenue_accrual_account = fields.Many2one('account.account', readonly=False,
domain="[('company_id', '=', company_id),"
"('account_type', 'not in', ('asset_receivable', 'liability_payable')),"
"('is_off_balance', '=', False)]",
check_company=True,
domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'off_balance'))]",
compute="_compute_revenue_accrual_account",
inverse="_inverse_revenue_accrual_account",
)
lock_date_message = fields.Char(string="Lock Date Message", compute="_compute_lock_date_message")
# change account
destination_account_id = fields.Many2one(string="To", comodel_name='account.account', help="Account to transfer to.")
destination_account_id = fields.Many2one(string="To", comodel_name='account.account', help="Account to transfer to.", check_company=True)
display_currency_helper = fields.Boolean(string="Currency Conversion Helper", compute='_compute_display_currency_helper')
# Technical field. Used to indicate whether or not to display the currency conversion tooltip. The tooltip informs a currency conversion will be performed with the transfer.
@ -121,8 +125,13 @@ class AutomaticEntryWizard(models.TransientModel):
@api.constrains('date', 'move_line_ids')
def _check_date(self):
for wizard in self:
if wizard.move_line_ids.move_id._get_violated_lock_dates(wizard.date, False):
raise ValidationError(_("The date selected is protected by a lock date"))
for move in wizard.move_line_ids.move_id:
violated_lock_dates = move._get_violated_lock_dates(wizard.date, False)
if violated_lock_dates:
raise ValidationError(_(
"The date selected is protected by: %(lock_date_info)s.",
lock_date_info=self.env['res.company']._format_lock_dates(violated_lock_dates)
))
@api.model
def default_get(self, fields):
@ -136,12 +145,12 @@ class AutomaticEntryWizard(models.TransientModel):
res['move_line_ids'] = [(6, 0, move_line_ids.ids)]
if any(move.state != 'posted' for move in move_line_ids.mapped('move_id')):
raise UserError(_('You can only change the period/account for posted journal items.'))
raise UserError(_("Oops! You can only change the period or account for posted entries! Other ones aren't up for an adventure like that!"))
if any(move_line.reconciled for move_line in move_line_ids):
raise UserError(_('You can only change the period/account for items that are not yet reconciled.'))
if any(line.company_id != move_line_ids[0].company_id for line in move_line_ids):
raise UserError(_("Oops! You can only change the period or account for items that are not yet reconciled! Other ones aren't up for an adventure like that!"))
if any(line.company_id.root_id != move_line_ids[0].company_id.root_id for line in move_line_ids):
raise UserError(_('You cannot use this wizard on journal entries belonging to different companies.'))
res['company_id'] = move_line_ids[0].company_id.id
res['company_id'] = move_line_ids[0].company_id.root_id.id
allowed_actions = set(dict(self._fields['action'].selection))
if self.env.context.get('default_action'):
@ -153,6 +162,11 @@ class AutomaticEntryWizard(models.TransientModel):
res['action'] = allowed_actions.pop()
return res
def _get_cut_off_label_format(self):
""" Get the translated format string used in cut-off labels """
self.ensure_one()
return _("Cut-off {label}") if self.percentage == 100 else _("Cut-off {label} {percent}%")
def _get_move_dict_vals_change_account(self):
line_vals = []
@ -230,10 +244,17 @@ class AutomaticEntryWizard(models.TransientModel):
'analytic_distribution': analytic_distribution,
})
# Get the lowest child company based on accounts used to avoid access error
accounts = self.env['account.account'].browse([line['account_id'] for line in line_vals])
companies = accounts.company_ids.filtered(lambda c: self.env.company in c.parent_ids) | self.env.company
lowest_child_company = max(companies, key=lambda company: len(company.parent_ids))
return [{
'currency_id': self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id,
'move_type': 'entry',
'name': '/',
'journal_id': self.journal_id.id,
'company_id': lowest_child_company.id,
'date': fields.Date.to_string(self.date),
'ref': self.destination_account_id.display_name and _("Transfer entry to %s", self.destination_account_id.display_name or ''),
'line_ids': [(0, 0, line) for line in line_vals],
@ -245,11 +266,12 @@ class AutomaticEntryWizard(models.TransientModel):
reported_debit = aml.company_id.currency_id.round((self.percentage / 100) * aml.debit)
reported_credit = aml.company_id.currency_id.round((self.percentage / 100) * aml.credit)
reported_amount_currency = aml.currency_id.round((self.percentage / 100) * aml.amount_currency)
name = self._format_strings(self._get_cut_off_label_format(), aml.move_id)
if date == 'new_date':
return [
(0, 0, {
'name': aml.name or '',
'name': name,
'debit': reported_debit,
'credit': reported_credit,
'amount_currency': reported_amount_currency,
@ -259,7 +281,7 @@ class AutomaticEntryWizard(models.TransientModel):
'analytic_distribution': aml.analytic_distribution,
}),
(0, 0, {
'name': self._format_strings(_('{percent:0.2f}% to recognize on {new_date}'), aml.move_id),
'name': name,
'debit': reported_credit,
'credit': reported_debit,
'amount_currency': -reported_amount_currency,
@ -271,7 +293,7 @@ class AutomaticEntryWizard(models.TransientModel):
]
return [
(0, 0, {
'name': aml.name or '',
'name': name,
'debit': reported_credit,
'credit': reported_debit,
'amount_currency': -reported_amount_currency,
@ -281,7 +303,7 @@ class AutomaticEntryWizard(models.TransientModel):
'analytic_distribution': aml.analytic_distribution,
}),
(0, 0, {
'name': self._format_strings(_('{percent:0.2f}% to recognize on {new_date}'), aml.move_id),
'name': name,
'debit': reported_debit,
'credit': reported_credit,
'amount_currency': reported_amount_currency,
@ -304,13 +326,15 @@ class AutomaticEntryWizard(models.TransientModel):
# set the change_period account on the selected journal items
ref_format = self._get_cut_off_label_format()
move_data = {'new_date': {
'currency_id': self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id,
'move_type': 'entry',
'line_ids': [],
'ref': self._format_strings(_('{label}: Adjusting Entry of {new_date}'), self.move_line_ids[0].move_id),
'ref': self._format_strings(ref_format, self.move_line_ids[0].move_id),
'date': fields.Date.to_string(self.date),
'journal_id': self.journal_id.id,
'adjusting_entry_origin_move_ids': self.move_line_ids.move_id.ids,
}}
# complete the account.move data
for date, grouped_lines in groupby(self.move_line_ids, get_lock_safe_date):
@ -320,14 +344,15 @@ class AutomaticEntryWizard(models.TransientModel):
'currency_id': self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id,
'move_type': 'entry',
'line_ids': [],
'ref': self._format_strings(_('{label}: Adjusting Entry of {date}'), grouped_lines[0].move_id, amount),
'ref': self._format_strings(ref_format, grouped_lines[0].move_id, amount),
'date': fields.Date.to_string(date),
'journal_id': self.journal_id.id,
'adjusting_entry_origin_move_ids': self.move_line_ids.move_id.ids,
}
# compute the account.move.lines and the total amount per move
for aml in self.move_line_ids:
for date in (get_lock_safe_date(aml), 'new_date'):
for date in ('new_date', get_lock_safe_date(aml)):
move_data[date]['line_ids'] += self._get_move_line_dict_vals_change_period(aml, date)
move_vals = [m for m in move_data.values()]
@ -399,26 +424,39 @@ class AutomaticEntryWizard(models.TransientModel):
accrual_move_lines = accrual_move.mapped('line_ids').filtered(lambda line: line.account_id == accrual_account)[accrual_move_offsets[accrual_move]:accrual_move_offsets[accrual_move]+2]
accrual_move_offsets[accrual_move] += 2
(accrual_move_lines + destination_move_lines).filtered(lambda line: not line.currency_id.is_zero(line.balance)).reconcile()
move.message_post(body=self._format_strings(_('Adjusting Entries have been created for this invoice:<ul><li>%(link1)s cancelling '
'{percent:.2f}%% of {amount}</li><li>%(link0)s postponing it to {new_date}</li></ul>',
link0=self._format_move_link(destination_move),
link1=self._format_move_link(accrual_move),
), move, amount))
destination_messages += [self._format_strings(_('Adjusting Entry {link}: {percent:.2f}% of {amount} recognized from {date}'), move, amount)]
accrual_move_messages[accrual_move] += [self._format_strings(_('Adjusting Entry for {link}: {percent:.2f}% of {amount} recognized on {new_date}'), move, amount)]
body = Markup("%(title)s<ul><li>%(link1)s %(second)s</li><li>%(link2)s %(third)s</li></ul>") % {
'title': _("Adjusting Entries have been created for this invoice:"),
'link1': self._format_move_link(accrual_move),
'second': self._format_strings(_("cancelling {percent}%% of {amount}"), move, amount),
'link2': self._format_move_link(destination_move),
'third': self._format_strings(_("postponing it to {new_date}"), move, amount),
}
move.message_post(body=body)
destination_messages += [
self._format_strings(
escape(_("Adjusting Entry {link} {percent}%% of {amount} recognized from {date}")),
move, amount,
)
]
accrual_move_messages[accrual_move] += [
self._format_strings(
escape(_("Adjusting Entry {link} {percent}%% of {amount} recognized on {new_date}")),
move, amount,
)
]
destination_move.message_post(body='<br/>\n'.join(destination_messages))
destination_move.message_post(body=Markup('<br/>\n').join(destination_messages))
for accrual_move, messages in accrual_move_messages.items():
accrual_move.message_post(body='<br/>\n'.join(messages))
accrual_move.message_post(body=Markup('<br/>\n').join(messages))
# open the generated entries
action = {
'name': _('Generated Entries'),
'domain': [('id', 'in', created_moves.ids)],
'res_model': 'account.move',
'view_mode': 'tree,form',
'view_mode': 'list,form',
'type': 'ir.actions.act_window',
'views': [(self.env.ref('account.view_move_tree').id, 'tree'), (False, 'form')],
'views': [(self.env.ref('account.view_move_tree').id, 'list'), (False, 'form')],
}
if len(created_moves) == 1:
action.update({'view_mode': 'form', 'res_id': created_moves.id})
@ -460,7 +498,6 @@ class AutomaticEntryWizard(models.TransientModel):
return {
'name': _("Transfer"),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'account.move',
'res_id': new_move.id,
@ -468,31 +505,45 @@ class AutomaticEntryWizard(models.TransientModel):
# Transfer utils
def _format_new_transfer_move_log(self, acc_transfer_per_move):
format = _("<li>{amount} ({debit_credit}) from {link}, <strong>%(account_source_name)s</strong></li>")
rslt = _("This entry transfers the following amounts to <strong>%(destination)s</strong> <ul>", destination=self.destination_account_id.display_name)
for move, balances_per_account in acc_transfer_per_move.items():
for account, balance in balances_per_account.items():
if account != self.destination_account_id: # Otherwise, logging it here is confusing for the user
rslt += self._format_strings(format % {'account_source_name': account.display_name}, move, balance)
rslt += '</ul>'
transfer_format = Markup("<li>%s, <strong>%%(account_source_name)s</strong></li>") % \
_("{amount} ({debit_credit}) from {link}")
rslt = _(
"This entry transfers the following amounts to %(destination)s",
destination=Markup("<strong>%s</strong>") % self.destination_account_id.display_name,
) + Markup("<ul>%(transfer_logs)s</ul>") % {
"transfer_logs": Markup().join(
[
self._format_strings(transfer_format % {"account_source_name": account.display_name}, move, balance)
for move, balances_per_account in acc_transfer_per_move.items()
for account, balance in balances_per_account.items()
if account != self.destination_account_id # Otherwise, logging it here is confusing for the user
],
),
}
return rslt
def _format_transfer_source_log(self, balances_per_account, transfer_move):
transfer_format = _("<li>{amount} ({debit_credit}) from <strong>%s</strong> were transferred to <strong>{account_target_name}</strong> by {link}</li>")
content = ''
for account, balance in balances_per_account.items():
if account != self.destination_account_id:
content += self._format_strings(transfer_format % account.display_name, transfer_move, balance)
return content and '<ul>' + content + '</ul>' or None
if not balances_per_account:
return None
transfer_format = Markup(
_("{amount} ({debit_credit}) from <strong>{account_source_name}</strong> were transferred to <strong>{account_target_name}</strong> by {link}")
)
return Markup("<ul>%s</ul>") % Markup().join([
Markup("<li>%s</li>") % \
self._format_strings(transfer_format, transfer_move, balance, account.display_name)
for account, balance in balances_per_account.items()
if account != self.destination_account_id
])
def _format_move_link(self, move):
return move._get_html_link()
def _format_strings(self, string, move, amount=None):
def _format_strings(self, string, move, amount=None, account_source_name=''):
return string.format(
label=move.name or 'Adjusting Entry',
percent=self.percentage,
label=move.name or _('Adjusting Entry'),
percent=float_repr(self.percentage, 2),
name=move.name,
id=move.id,
amount=formatLang(self.env, abs(amount), currency_obj=self.company_id.currency_id) if amount else '',
@ -500,5 +551,6 @@ class AutomaticEntryWizard(models.TransientModel):
link=self._format_move_link(move),
date=format_date(self.env, move.date),
new_date=self.date and format_date(self.env, self.date) or _('[Not set]'),
account_source_name=account_source_name,
account_target_name=self.destination_account_id.display_name,
)