Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,517 @@
# Fusion Accounting - Lock Date Management Wizard
# Provides UI for managing fiscal, tax, sales, and purchase lock dates
# with support for temporary exceptions and draft entry warnings.
from datetime import date, timedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools import date_utils
from odoo.addons.account.models.company import SOFT_LOCK_DATE_FIELDS, LOCK_DATE_FIELDS
class AccountChangeLockDate(models.TransientModel):
"""Wizard that enables administrators to update accounting lock dates
and manage exceptions for individual users or the entire organization."""
_name = 'account.change.lock.date'
_description = 'Change Lock Date'
# --- Core Company Reference ---
company_id = fields.Many2one(
comodel_name='res.company',
required=True,
readonly=True,
default=lambda self: self.env.company,
)
# --- Fiscal Year Lock ---
fiscalyear_lock_date = fields.Date(
string='Lock Everything',
default=lambda self: self.env.company.fiscalyear_lock_date,
help="Entries on or before this date are locked and will be "
"rescheduled according to the journal's sequence.",
)
fiscalyear_lock_date_for_me = fields.Date(
string='Lock Everything For Me',
compute='_compute_lock_date_exceptions',
)
fiscalyear_lock_date_for_everyone = fields.Date(
string='Lock Everything For Everyone',
compute='_compute_lock_date_exceptions',
)
min_fiscalyear_lock_date_exception_for_me_id = fields.Many2one(
comodel_name='account.lock_exception',
compute='_compute_lock_date_exceptions',
)
min_fiscalyear_lock_date_exception_for_everyone_id = fields.Many2one(
comodel_name='account.lock_exception',
compute='_compute_lock_date_exceptions',
)
# --- Tax Lock ---
tax_lock_date = fields.Date(
string="Lock Tax Return",
default=lambda self: self.env.company.tax_lock_date,
help="Tax entries on or before this date are locked. This date "
"updates automatically when a tax closing entry is posted.",
)
tax_lock_date_for_me = fields.Date(
string='Lock Tax Return For Me',
compute='_compute_lock_date_exceptions',
)
tax_lock_date_for_everyone = fields.Date(
string='Lock Tax Return For Everyone',
compute='_compute_lock_date_exceptions',
)
min_tax_lock_date_exception_for_me_id = fields.Many2one(
comodel_name='account.lock_exception',
compute='_compute_lock_date_exceptions',
)
min_tax_lock_date_exception_for_everyone_id = fields.Many2one(
comodel_name='account.lock_exception',
compute='_compute_lock_date_exceptions',
)
# --- Sales Lock ---
sale_lock_date = fields.Date(
string='Lock Sales',
default=lambda self: self.env.company.sale_lock_date,
help="Sales entries on or before this date are locked and will "
"be postponed per the journal's sequence.",
)
sale_lock_date_for_me = fields.Date(
string='Lock Sales For Me',
compute='_compute_lock_date_exceptions',
)
sale_lock_date_for_everyone = fields.Date(
string='Lock Sales For Everyone',
compute='_compute_lock_date_exceptions',
)
min_sale_lock_date_exception_for_me_id = fields.Many2one(
comodel_name='account.lock_exception',
compute='_compute_lock_date_exceptions',
)
min_sale_lock_date_exception_for_everyone_id = fields.Many2one(
comodel_name='account.lock_exception',
compute='_compute_lock_date_exceptions',
)
# --- Purchase Lock ---
purchase_lock_date = fields.Date(
string='Lock Purchases',
default=lambda self: self.env.company.purchase_lock_date,
help="Purchase entries on or before this date are locked and "
"will be postponed per the journal's sequence.",
)
purchase_lock_date_for_me = fields.Date(
string='Lock Purchases For Me',
compute='_compute_lock_date_exceptions',
)
purchase_lock_date_for_everyone = fields.Date(
string='Lock Purchases For Everyone',
compute='_compute_lock_date_exceptions',
)
min_purchase_lock_date_exception_for_me_id = fields.Many2one(
comodel_name='account.lock_exception',
compute='_compute_lock_date_exceptions',
)
min_purchase_lock_date_exception_for_everyone_id = fields.Many2one(
comodel_name='account.lock_exception',
compute='_compute_lock_date_exceptions',
)
# --- Hard (Irreversible) Lock ---
hard_lock_date = fields.Date(
string='Hard Lock',
default=lambda self: self.env.company.hard_lock_date,
help="Entries on or before this date are permanently locked. "
"This lock cannot be removed and allows no exceptions.",
)
current_hard_lock_date = fields.Date(
string='Current Hard Lock',
related='company_id.hard_lock_date',
readonly=True,
)
# --- Exception Configuration ---
exception_needed = fields.Boolean(
string='Exception needed',
compute='_compute_exception_needed',
)
exception_needed_fields = fields.Char(
compute='_compute_exception_needed_fields',
)
exception_applies_to = fields.Selection(
string='Exception applies',
selection=[
('me', "for me"),
('everyone', "for everyone"),
],
default='me',
required=True,
)
exception_duration = fields.Selection(
string='Exception Duration',
selection=[
('5min', "for 5 minutes"),
('15min', "for 15 minutes"),
('1h', "for 1 hour"),
('24h', "for 24 hours"),
('forever', "forever"),
],
default='5min',
required=True,
)
exception_reason = fields.Char(
string='Exception Reason',
)
# --- Warning for draft entries ---
show_draft_entries_warning = fields.Boolean(
string="Show Draft Entries Warning",
compute='_compute_show_draft_entries_warning',
)
# -------------------------------------------------------------------------
# Compute Methods
# -------------------------------------------------------------------------
@api.depends('company_id')
@api.depends_context('user', 'company')
def _compute_lock_date_exceptions(self):
"""Retrieve active lock date exceptions for each soft lock field
and determine the minimum exception per user scope."""
LockException = self.env['account.lock_exception']
for wiz in self:
active_domain = LockException._get_active_exceptions_domain(
wiz.company_id, SOFT_LOCK_DATE_FIELDS,
)
active_exceptions = LockException.search(active_domain)
current_uid = self.env.user.id
for lock_field in SOFT_LOCK_DATE_FIELDS:
# Partition exceptions by scope (current user vs global)
my_exceptions = active_exceptions.filtered(
lambda exc: exc.lock_date_field == lock_field and exc.user_id.id == current_uid
)
global_exceptions = active_exceptions.filtered(
lambda exc: exc.lock_date_field == lock_field and not exc.user_id.id
)
# Find the earliest exception in each scope
earliest_mine = (
min(my_exceptions, key=lambda e: e[lock_field] or date.min)
if my_exceptions else False
)
earliest_global = (
min(global_exceptions, key=lambda e: e[lock_field] or date.min)
if global_exceptions else False
)
wiz[f"min_{lock_field}_exception_for_me_id"] = earliest_mine
wiz[f"min_{lock_field}_exception_for_everyone_id"] = earliest_global
wiz[f"{lock_field}_for_me"] = earliest_mine.lock_date if earliest_mine else False
wiz[f"{lock_field}_for_everyone"] = earliest_global.lock_date if earliest_global else False
def _build_draft_moves_domain(self):
"""Build a domain to find draft moves that would fall within
any of the configured lock periods."""
self.ensure_one()
period_domains = []
if self.hard_lock_date:
period_domains.append([('date', '<=', self.hard_lock_date)])
if self.fiscalyear_lock_date:
period_domains.append([('date', '<=', self.fiscalyear_lock_date)])
if self.sale_lock_date:
period_domains.append([
('date', '<=', self.sale_lock_date),
('journal_id.type', '=', 'sale'),
])
if self.purchase_lock_date:
period_domains.append([
('date', '<=', self.purchase_lock_date),
('journal_id.type', '=', 'purchase'),
])
return [
('company_id', 'child_of', self.env.company.id),
('state', '=', 'draft'),
*expression.OR(period_domains),
]
# Keep backward-compatible alias
_get_draft_moves_in_locked_period_domain = _build_draft_moves_domain
@api.depends('fiscalyear_lock_date', 'tax_lock_date', 'sale_lock_date',
'purchase_lock_date', 'hard_lock_date')
def _compute_show_draft_entries_warning(self):
"""Flag whether any draft journal entries exist in the locked period."""
AccountMove = self.env['account.move']
for wiz in self:
has_drafts = bool(AccountMove.search(
wiz._build_draft_moves_domain(), limit=1,
))
wiz.show_draft_entries_warning = has_drafts
def _get_changes_needing_exception(self):
"""Identify soft lock fields that are being loosened
(i.e. the new date is earlier than the current company setting)."""
self.ensure_one()
company = self.env.company
relaxed = {}
for fld in SOFT_LOCK_DATE_FIELDS:
current_val = company[fld]
new_val = self[fld]
if current_val and (not new_val or new_val < current_val):
relaxed[fld] = new_val
return relaxed
@api.depends(*SOFT_LOCK_DATE_FIELDS)
def _compute_exception_needed(self):
for wiz in self:
wiz.exception_needed = bool(wiz._get_changes_needing_exception())
@api.depends(*SOFT_LOCK_DATE_FIELDS)
def _compute_exception_needed_fields(self):
for wiz in self:
relaxed_fields = wiz._get_changes_needing_exception()
wiz.exception_needed_fields = ','.join(relaxed_fields)
# -------------------------------------------------------------------------
# Lock Date Value Preparation
# -------------------------------------------------------------------------
def _prepare_lock_date_values(self, exception_vals_list=None):
"""Return a dict of lock-date fields that have changed and
are not covered by a newly created exception."""
self.ensure_one()
company = self.env.company
# Hard lock date can never decrease
if company.hard_lock_date and (
not self.hard_lock_date or self.hard_lock_date < company.hard_lock_date
):
raise UserError(_(
'The Hard Lock Date cannot be decreased or removed once set.'
))
changed_vals = {}
for fld in LOCK_DATE_FIELDS:
if self[fld] != company[fld]:
changed_vals[fld] = self[fld]
# No lock date may be set in the future
today = fields.Date.context_today(self)
for fld, val in changed_vals.items():
if val and val > today:
raise UserError(_(
'A Lock Date cannot be set to a future date.'
))
# Exclude fields that are being handled via exception creation
if exception_vals_list:
for exc_vals in exception_vals_list:
for fld in LOCK_DATE_FIELDS:
if fld in exc_vals:
changed_vals.pop(fld, None)
return changed_vals
def _prepare_exception_values(self):
"""Construct a list of dicts suitable for creating
account.lock_exception records for any loosened lock dates."""
self.ensure_one()
relaxed = self._get_changes_needing_exception()
if not relaxed:
return False
# Relaxing for everyone forever is equivalent to simply updating the date
if self.exception_applies_to == 'everyone' and self.exception_duration == 'forever':
return False
# Validate that scope and duration are set
validation_issues = []
if not self.exception_applies_to:
validation_issues.append(_('Please select who the exception applies to.'))
if not self.exception_duration:
validation_issues.append(_('Please select a duration for the exception.'))
if validation_issues:
raise UserError('\n'.join(validation_issues))
# Build shared exception values
shared_vals = {
'company_id': self.env.company.id,
}
# Determine target user
scope_to_user = {
'me': self.env.user.id,
'everyone': False,
}
shared_vals['user_id'] = scope_to_user[self.exception_applies_to]
# Determine expiration
duration_map = {
'5min': timedelta(minutes=5),
'15min': timedelta(minutes=15),
'1h': timedelta(hours=1),
'24h': timedelta(hours=24),
'forever': False,
}
delta = duration_map[self.exception_duration]
if delta:
shared_vals['end_datetime'] = self.env.cr.now() + delta
if self.exception_reason:
shared_vals['reason'] = self.exception_reason
return [
{**shared_vals, fld: val}
for fld, val in relaxed.items()
]
# -------------------------------------------------------------------------
# Period Date Helpers
# -------------------------------------------------------------------------
def _get_current_period_dates(self, lock_date_field):
"""Determine the start and end of the current period relative
to the selected lock date (using the prior lock date or fiscal year start)."""
self.ensure_one()
existing_lock = self.env.company[lock_date_field]
if existing_lock:
period_start = existing_lock + timedelta(days=1)
else:
period_start = date_utils.get_fiscal_year(self[lock_date_field])[0]
return period_start, self[lock_date_field]
def _create_default_report_external_values(self, lock_date_field):
"""Hook for generating default report external values when lock
dates change. Extended by account reporting modules."""
date_from, date_to = self._get_current_period_dates(lock_date_field)
self.env['account.report']._generate_default_external_values(
date_from, date_to, lock_date_field == 'tax_lock_date',
)
# -------------------------------------------------------------------------
# Core Lock Date Application
# -------------------------------------------------------------------------
def _change_lock_date(self, lock_date_values=None):
"""Apply the given lock date values to the company, generating
default report external values as needed."""
self.ensure_one()
if lock_date_values is None:
lock_date_values = self._prepare_lock_date_values()
# Handle tax lock date report externals
new_tax_lock = lock_date_values.get('tax_lock_date')
if new_tax_lock and new_tax_lock != self.env.company['tax_lock_date']:
self._create_default_report_external_values('tax_lock_date')
# Handle fiscal year / hard lock date report externals
new_fy_lock = lock_date_values.get('fiscalyear_lock_date')
new_hard_lock = lock_date_values.get('hard_lock_date')
if new_fy_lock or new_hard_lock:
candidate_lock, candidate_field = max(
[(new_fy_lock, 'fiscalyear_lock_date'),
(new_hard_lock, 'hard_lock_date')],
key=lambda pair: pair[0] or date.min,
)
existing_fy_max = max(
self.env.company.fiscalyear_lock_date or date.min,
self.env.company.hard_lock_date or date.min,
)
if candidate_lock != existing_fy_max:
self._create_default_report_external_values(candidate_field)
self.env.company.sudo().write(lock_date_values)
def change_lock_date(self):
"""Main action: validate permissions, create exceptions if needed,
and apply new lock dates to the company."""
self.ensure_one()
if not self.env.user.has_group('account.group_account_manager'):
raise UserError(_(
'Only Billing Administrators are allowed to change lock dates!'
))
exc_vals_list = self._prepare_exception_values()
updated_lock_vals = self._prepare_lock_date_values(
exception_vals_list=exc_vals_list,
)
if exc_vals_list:
self.env['account.lock_exception'].create(exc_vals_list)
self._change_lock_date(updated_lock_vals)
return {'type': 'ir.actions.act_window_close'}
# -------------------------------------------------------------------------
# UI Actions
# -------------------------------------------------------------------------
def action_show_draft_moves_in_locked_period(self):
"""Open a list view showing draft moves within the locked period."""
self.ensure_one()
return {
'view_mode': 'list',
'name': _('Draft Entries'),
'res_model': 'account.move',
'type': 'ir.actions.act_window',
'domain': self._build_draft_moves_domain(),
'search_view_id': [
self.env.ref('account.view_account_move_filter').id, 'search',
],
'views': [
[self.env.ref('account.view_move_tree_multi_edit').id, 'list'],
[self.env.ref('account.view_move_form').id, 'form'],
],
}
def action_reopen_wizard(self):
"""Return an action that reopens this wizard in a dialog."""
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
def _action_revoke_min_exception(self, exception_field):
"""Revoke a specific exception and refresh the wizard."""
self.ensure_one()
target_exception = self[exception_field]
if target_exception:
target_exception.action_revoke()
self._compute_lock_date_exceptions()
return self.action_reopen_wizard()
# --- Per-field revoke actions (current user) ---
def action_revoke_min_sale_lock_date_exception_for_me(self):
return self._action_revoke_min_exception('min_sale_lock_date_exception_for_me_id')
def action_revoke_min_purchase_lock_date_exception_for_me(self):
return self._action_revoke_min_exception('min_purchase_lock_date_exception_for_me_id')
def action_revoke_min_tax_lock_date_exception_for_me(self):
return self._action_revoke_min_exception('min_tax_lock_date_exception_for_me_id')
def action_revoke_min_fiscalyear_lock_date_exception_for_me(self):
return self._action_revoke_min_exception('min_fiscalyear_lock_date_exception_for_me_id')
# --- Per-field revoke actions (everyone) ---
def action_revoke_min_sale_lock_date_exception_for_everyone(self):
return self._action_revoke_min_exception('min_sale_lock_date_exception_for_everyone_id')
def action_revoke_min_purchase_lock_date_exception_for_everyone(self):
return self._action_revoke_min_exception('min_purchase_lock_date_exception_for_everyone_id')
def action_revoke_min_tax_lock_date_exception_for_everyone(self):
return self._action_revoke_min_exception('min_tax_lock_date_exception_for_everyone_id')
def action_revoke_min_fiscalyear_lock_date_exception_for_everyone(self):
return self._action_revoke_min_exception('min_fiscalyear_lock_date_exception_for_everyone_id')