327 lines
13 KiB
Python
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())],
|
|
}
|