Odoo18-Base/addons/account/models/account_lock_exception.py
2025-01-06 10:57:38 +07:00

327 lines
13 KiB
Python

from odoo import _, api, fields, models, Command
from odoo.osv import expression
from odoo.tools import create_index
from odoo.tools.misc import format_datetime
from odoo.exceptions import UserError, ValidationError
from odoo.addons.account.models.company import SOFT_LOCK_DATE_FIELDS
from datetime import date
class AccountLockException(models.Model):
_name = "account.lock_exception"
_description = "Account Lock Exception"
active = fields.Boolean(
string='Active',
default=True,
)
state = fields.Selection(
selection=[
('active', 'Active'),
('revoked', 'Revoked'),
('expired', 'Expired'),
],
string="State",
compute='_compute_state',
search='_search_state'
)
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
readonly=True,
default=lambda self: self.env.company,
)
# An exception w/o user_id is an exception for everyone
user_id = fields.Many2one(
'res.users',
string='User',
default=lambda self: self.env.user,
)
reason = fields.Char(
string='Reason',
)
# An exception without `end_datetime` is valid forever
end_datetime = fields.Datetime(
string='End Date',
)
# The changed lock date
lock_date_field = fields.Selection(
selection=[
('fiscalyear_lock_date', 'Global Lock Date'),
('tax_lock_date', 'Tax Return Lock Date'),
('sale_lock_date', 'Sales Lock Date'),
('purchase_lock_date', 'Purchase Lock Date'),
],
string="Lock Date Field",
required=True,
help="Technical field identifying the changed lock date",
)
lock_date = fields.Date(
string="Changed Lock Date",
help="Technical field giving the date the lock date was changed to.",
)
company_lock_date = fields.Date(
string="Original Lock Date",
copy=False,
help="Technical field giving the date the company lock date at the time the exception was created.",
)
# (Non-stored) computed lock date fields; c.f. res.company
fiscalyear_lock_date = fields.Date(
string="Global Lock Date",
compute="_compute_lock_dates",
search="_search_fiscalyear_lock_date",
help="The date the Global Lock Date is set to by this exception. If the lock date is not changed it is set to the maximal date.",
)
tax_lock_date = fields.Date(
string="Tax Return Lock Date",
compute="_compute_lock_dates",
search="_search_tax_lock_date",
help="The date the Tax Lock Date is set to by this exception. If the lock date is not changed it is set to the maximal date.",
)
sale_lock_date = fields.Date(
string='Sales Lock Date',
compute="_compute_lock_dates",
search="_search_sale_lock_date",
help="The date the Sale Lock Date is set to by this exception. If the lock date is not changed it is set to the maximal date.",
)
purchase_lock_date = fields.Date(
string='Purchase Lock Date',
compute="_compute_lock_dates",
search="_search_purchase_lock_date",
help="The date the Purchase Lock Date is set to by this exception. If the lock date is not changed it is set to the maximal date.",
)
def init(self):
super().init()
create_index(
self.env.cr,
indexname='account_lock_exception_company_id_end_datetime_idx',
tablename=self._table,
expressions=['company_id', 'user_id', 'end_datetime'],
where="active = TRUE"
)
def _compute_display_name(self):
for record in self:
record.display_name = _("Lock Date Exception %s", record.id)
@api.depends('active', 'end_datetime')
def _compute_state(self):
for record in self:
if not record.active:
record.state = 'revoked'
elif record.end_datetime and record.end_datetime < self.env.cr.now():
record.state = 'expired'
else:
record.state = 'active'
@api.depends('lock_date_field', 'lock_date')
def _compute_lock_dates(self):
for exception in self:
for field in SOFT_LOCK_DATE_FIELDS:
if field == exception.lock_date_field:
exception[field] = exception.lock_date
else:
exception[field] = date.max
def _search_state(self, operator, value):
if operator not in ['=', '!='] or value not in ['revoked', 'expired', 'active']:
raise UserError(_('Operation not supported'))
normal_domain_for_equals = []
if value == 'revoked':
normal_domain_for_equals = [
('active', '=', False),
]
elif value == 'expired':
normal_domain_for_equals = [
'&',
('active', '=', True),
('end_datetime', '<', self.env.cr.now()),
]
elif value == 'active':
normal_domain_for_equals = [
'&',
('active', '=', True),
'|',
('end_datetime', '=', None),
('end_datetime', '>=', self.env.cr.now()),
]
if operator == '=':
return normal_domain_for_equals
else:
return ['!'] + normal_domain_for_equals
def _search_lock_date(self, field, operator, value):
if operator not in ['<', '<='] or not value:
raise UserError(_('Operation not supported'))
return ['&',
('lock_date_field', '=', field),
'|',
('lock_date', '=', False),
('lock_date', operator, value),
]
def _search_fiscalyear_lock_date(self, operator, value):
return self._search_lock_date('fiscalyear_lock_date', operator, value)
def _search_tax_lock_date(self, operator, value):
return self._search_lock_date('tax_lock_date', operator, value)
def _search_sale_lock_date(self, operator, value):
return self._search_lock_date('sale_lock_date', operator, value)
def _search_purchase_lock_date(self, operator, value):
return self._search_lock_date('purchase_lock_date', operator, value)
def _invalidate_affected_user_lock_dates(self):
affected_lock_date_fields = {exception.lock_date_field for exception in self}
self.env['res.company'].invalidate_model(
fnames=[f'user_{field}' for field in list(affected_lock_date_fields)],
)
@api.model_create_multi
def create(self, vals_list):
# Preprocess arguments:
# 1. Parse lock date arguments
# E.g. to create an exception for 'fiscalyear_lock_date' to '2024-01-01' put
# {'fiscalyear_lock_date': '2024-01-01'} in the create vals.
# The same thing works for all other fields in SOFT_LOCK_DATE_FIELDS.
# 2. Fetch company lock date
for vals in vals_list:
if 'lock_date' not in vals or 'lock_date_field' not in vals:
# Use vals[field] (for field in SOFT_LOCK_DATE_FIELDS) to init the data
changed_fields = [field for field in SOFT_LOCK_DATE_FIELDS if field in vals]
if len(changed_fields) != 1:
raise ValidationError(_("A single exception must change exactly one lock date field."))
field = changed_fields[0]
vals['lock_date_field'] = field
vals['lock_date'] = vals.pop(field)
company = self.env['res.company'].browse(vals.get('company_id', self.env.company.id))
if 'company_lock_date' not in vals:
vals['company_lock_date'] = company[vals['lock_date_field']]
exceptions = super().create(vals_list)
# Log the creation of the exception and the changed field on the company chatter
for exception in exceptions:
company = exception.company_id
# Create tracking values to display the lock date change in the chatter
field = exception.lock_date_field
value = exception.lock_date
field_info = exception.fields_get([field])[field]
tracking_values = self.env['mail.tracking.value']._create_tracking_values(
company[field], value, field, field_info, exception,
)
tracking_value_ids = [Command.create(tracking_values)]
# In case there is no explicit end datetime "forever" is implied by not mentioning an end datetime
end_datetime_string = _(" valid until %s", format_datetime(self.env, exception.end_datetime)) if exception.end_datetime else ""
reason_string = _(" for '%s'", exception.reason) if exception.reason else ""
company_chatter_message = _(
"%(exception)s for %(user)s%(end_datetime_string)s%(reason)s.",
exception=exception._get_html_link(title=_("Exception")),
user=exception.user_id.display_name if exception.user_id else _("everyone"),
end_datetime_string=end_datetime_string,
reason=reason_string,
)
company.sudo().message_post(
body=company_chatter_message,
tracking_value_ids=tracking_value_ids,
)
exceptions._invalidate_affected_user_lock_dates()
return exceptions
def copy(self, default=None):
raise UserError(_('You cannot duplicate a Lock Date Exception.'))
def _recreate(self):
"""
1. Copy all exceptions in self but update the company lock date.
2. Revoke all exceptions in self.
3. Return the new records from step 1.
"""
if not self:
return self.env['account.lock_exception']
vals_list = self.with_context(active_test=False).copy_data()
new_records = self.create(vals_list)
self.sudo().action_revoke()
return new_records
def action_revoke(self):
"""Revokes an active exception."""
if not self.env.user.has_group('account.group_account_manager'):
raise UserError(_("You cannot revoke Lock Date Exceptions. Ask someone with the 'Adviser' role."))
for record in self:
if record.state == 'active':
record_sudo = record.sudo()
record_sudo.active = False
record_sudo.end_datetime = fields.Datetime.now()
record._invalidate_affected_user_lock_dates()
@api.model
def _get_active_exceptions_domain(self, company, soft_lock_date_fields):
return [
*expression.OR([(field, '<', company[field])] for field in soft_lock_date_fields if company[field]),
('company_id', '=', company.id),
('state', '=', 'active'), # checks the datetime
]
def _get_audit_trail_during_exception_domain(self):
self.ensure_one()
common_message_domain = [
('date', '>=', self.create_date),
]
if self.user_id:
common_message_domain.append(('create_uid', '=', self.user_id.id))
if self.end_datetime:
common_message_domain.append(('date', '<=', self.end_datetime))
# Add restrictions on the accounting date to avoid unnecessary entries
min_date = self.lock_date
max_date = self.company_lock_date
move_date_domain = []
tracking_old_datetime_domain = []
tracking_new_datetime_domain = []
if min_date:
move_date_domain.append([('date', '>=', min_date)])
tracking_old_datetime_domain.append([('tracking_value_ids.old_value_datetime', '>=', min_date)])
tracking_new_datetime_domain.append([('tracking_value_ids.new_value_datetime', '>=', min_date)])
if max_date:
move_date_domain.append([('date', '<=', max_date)])
tracking_old_datetime_domain.append([('tracking_value_ids.old_value_datetime', '<=', max_date)])
tracking_new_datetime_domain.append([('tracking_value_ids.new_value_datetime', '<=', max_date)])
return [
('company_id', 'child_of', self.company_id.id),
('audit_trail_message_ids', 'any', common_message_domain),
'|',
# The date was changed from or to a value inside the excepted period
('audit_trail_message_ids', 'any', [
('tracking_value_ids.field_id', '=', self.env['ir.model.fields']._get('account.move', 'date').id),
'|',
*expression.AND(tracking_old_datetime_domain),
*expression.AND(tracking_new_datetime_domain),
]),
# The date of the move is inside the excepted period and sth. was changed on the move
*expression.AND(move_date_domain),
]
def action_show_audit_trail_during_exception(self):
self.ensure_one()
return {
'name': _("Journal Items"),
'type': 'ir.actions.act_window',
'res_model': 'account.move.line',
'view_mode': 'list,form',
'domain': [('move_id', 'any', self._get_audit_trail_during_exception_domain())],
}