Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
3
account_lock_to_date/models/__init__.py
Executable file
3
account_lock_to_date/models/__init__.py
Executable file
@@ -0,0 +1,3 @@
|
||||
from . import res_company
|
||||
from . import account_move
|
||||
from . import account_lock_exception
|
||||
90
account_lock_to_date/models/account_lock_exception.py
Executable file
90
account_lock_to_date/models/account_lock_exception.py
Executable file
@@ -0,0 +1,90 @@
|
||||
# Copyright 2025 ForgeFlow S.L.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
|
||||
from .res_company import SOFT_LOCK_TO_DATE_FIELDS
|
||||
|
||||
|
||||
class AccountLockException(models.Model):
|
||||
_inherit = "account.lock_exception"
|
||||
|
||||
# The changed lock date
|
||||
lock_date_field = fields.Selection(
|
||||
selection_add=[
|
||||
("fiscalyear_lock_to_date", "Global Lock To Date"),
|
||||
("sale_lock_to_date", "Sales Lock To Date"),
|
||||
("purchase_lock_to_date", "Purchase Lock To Date"),
|
||||
],
|
||||
ondelete={
|
||||
"fiscalyear_lock_to_date": "cascade",
|
||||
"sale_lock_to_date": "cascade",
|
||||
"purchase_lock_to_date": "cascade",
|
||||
},
|
||||
)
|
||||
|
||||
# (Non-stored) computed lock to date fields; c.f. res.company
|
||||
fiscalyear_lock_to_date = fields.Date(
|
||||
string="Global Lock To Date",
|
||||
compute="_compute_lock_dates",
|
||||
search="_search_fiscalyear_lock_to_date",
|
||||
help="The date the Global Lock To Date is set to by this exception. "
|
||||
"If the lock to date is not changed it is set to False.",
|
||||
)
|
||||
sale_lock_to_date = fields.Date(
|
||||
string="Sales Lock To Date",
|
||||
compute="_compute_lock_dates",
|
||||
search="_search_sale_lock_to_date",
|
||||
help="The date the Sale Lock To Date is set to by this exception. "
|
||||
"If the lock to date is not changed it is set to False.",
|
||||
)
|
||||
purchase_lock_to_date = fields.Date(
|
||||
compute="_compute_lock_dates",
|
||||
search="_search_purchase_lock_to_date",
|
||||
help="The date the Purchase Lock To Date is set to by this exception. "
|
||||
"If the lock to date is not changed it is set to False.",
|
||||
)
|
||||
|
||||
@api.depends("lock_date_field", "lock_date")
|
||||
def _compute_lock_dates(self):
|
||||
res = super()._compute_lock_dates()
|
||||
for exception in self:
|
||||
for field in SOFT_LOCK_TO_DATE_FIELDS:
|
||||
if field == exception.lock_date_field:
|
||||
exception[field] = exception.lock_date
|
||||
else:
|
||||
exception[field] = False
|
||||
return res
|
||||
|
||||
def _search_lock_to_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_to_date(self, operator, value):
|
||||
return self._search_lock_to_date("fiscalyear_lock_to_date", operator, value)
|
||||
|
||||
def _search_sale_lock_to_date(self, operator, value):
|
||||
return self._search_lock_to_date("sale_lock_to_date", operator, value)
|
||||
|
||||
def _search_purchase_lock_to_date(self, operator, value):
|
||||
return self._search_lock_to_date("purchase_lock_to_date", operator, value)
|
||||
|
||||
@api.model
|
||||
def _get_active_exceptions_to_domain(self, company, soft_lock_to_date_fields):
|
||||
return [
|
||||
*expression.OR(
|
||||
[(field, ">", company[field])]
|
||||
for field in soft_lock_to_date_fields
|
||||
if company[field]
|
||||
),
|
||||
("company_id", "=", company.id),
|
||||
("state", "=", "active"), # checks the datetime
|
||||
]
|
||||
46
account_lock_to_date/models/account_move.py
Executable file
46
account_lock_to_date/models/account_move.py
Executable file
@@ -0,0 +1,46 @@
|
||||
# Copyright 2019 ForgeFlow S.L.
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.account.models.account_move import BYPASS_LOCK_CHECK
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
def _check_lock_to_dates(self):
|
||||
if self.env.context.get("bypass_lock_check") is BYPASS_LOCK_CHECK:
|
||||
return
|
||||
for move in self:
|
||||
journal = move.journal_id
|
||||
violated_lock_to_dates = move.company_id._get_lock_to_date_violations(
|
||||
move.date,
|
||||
fiscalyear=True,
|
||||
sale=journal and journal.type == "sale",
|
||||
purchase=journal and journal.type == "purchase",
|
||||
hard=True,
|
||||
)
|
||||
if violated_lock_to_dates:
|
||||
message = _(
|
||||
"You cannot add/modify entries posterior to "
|
||||
"and inclusive of: %(lock_date_info)s.",
|
||||
lock_date_info=self.env["res.company"]._format_lock_dates(
|
||||
violated_lock_to_dates
|
||||
),
|
||||
)
|
||||
raise ValidationError(message)
|
||||
return True
|
||||
|
||||
def action_post(self):
|
||||
self._check_lock_to_dates()
|
||||
return super().action_post()
|
||||
|
||||
def button_cancel(self):
|
||||
self._check_lock_to_dates()
|
||||
return super().button_cancel()
|
||||
|
||||
def button_draft(self):
|
||||
self._check_lock_to_dates()
|
||||
return super().button_draft()
|
||||
264
account_lock_to_date/models/res_company.py
Executable file
264
account_lock_to_date/models/res_company.py
Executable file
@@ -0,0 +1,264 @@
|
||||
# Copyright 2019 ForgeFlow S.L.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
SOFT_LOCK_TO_DATE_FIELDS = [
|
||||
"fiscalyear_lock_to_date",
|
||||
"sale_lock_to_date",
|
||||
"purchase_lock_to_date",
|
||||
]
|
||||
|
||||
LOCK_TO_DATE_FIELDS = [
|
||||
*SOFT_LOCK_TO_DATE_FIELDS,
|
||||
"hard_lock_to_date",
|
||||
]
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
sale_lock_to_date = fields.Date(
|
||||
string="Sales Lock To Date",
|
||||
tracking=True,
|
||||
help="Prevents creation and modification of entries in sales journals"
|
||||
" posterior to the defined date inclusive.",
|
||||
)
|
||||
purchase_lock_to_date = fields.Date(
|
||||
tracking=True,
|
||||
help="Prevents creation and modification of entries in purchase journals"
|
||||
" posterior to the defined date inclusive.",
|
||||
)
|
||||
fiscalyear_lock_to_date = fields.Date(
|
||||
string="Global Lock To Date",
|
||||
tracking=True,
|
||||
help="No users can edit accounts posterior to this date."
|
||||
" Use it for fiscal year locking for example.",
|
||||
)
|
||||
hard_lock_to_date = fields.Date(
|
||||
tracking=True,
|
||||
help='Like the "Global Lock Date", but no exceptions are possible.',
|
||||
)
|
||||
|
||||
user_fiscalyear_lock_to_date = fields.Date(
|
||||
compute="_compute_user_fiscalyear_lock_to_date"
|
||||
)
|
||||
user_sale_lock_to_date = fields.Date(compute="_compute_user_sale_lock_to_date")
|
||||
user_purchase_lock_to_date = fields.Date(
|
||||
compute="_compute_user_purchase_lock_to_date"
|
||||
)
|
||||
user_hard_lock_to_date = fields.Date(compute="_compute_user_hard_lock_to_date")
|
||||
|
||||
@api.depends("fiscalyear_lock_to_date")
|
||||
@api.depends_context("uid", "ignore_exceptions")
|
||||
def _compute_user_fiscalyear_lock_to_date(self):
|
||||
ignore_exceptions = bool(self.env.context.get("ignore_exceptions", False))
|
||||
for company in self:
|
||||
company.user_fiscalyear_lock_to_date = company._get_user_lock_to_date(
|
||||
"fiscalyear_lock_to_date", ignore_exceptions
|
||||
)
|
||||
|
||||
@api.depends("sale_lock_to_date")
|
||||
@api.depends_context("uid", "ignore_exceptions")
|
||||
def _compute_user_sale_lock_to_date(self):
|
||||
ignore_exceptions = bool(self.env.context.get("ignore_exceptions", False))
|
||||
for company in self:
|
||||
company.user_sale_lock_to_date = company._get_user_lock_to_date(
|
||||
"sale_lock_to_date", ignore_exceptions
|
||||
)
|
||||
|
||||
@api.depends("purchase_lock_to_date")
|
||||
@api.depends_context("uid", "ignore_exceptions")
|
||||
def _compute_user_purchase_lock_to_date(self):
|
||||
ignore_exceptions = bool(self.env.context.get("ignore_exceptions", False))
|
||||
for company in self:
|
||||
company.user_purchase_lock_to_date = company._get_user_lock_to_date(
|
||||
"purchase_lock_to_date", ignore_exceptions
|
||||
)
|
||||
|
||||
@api.depends("hard_lock_to_date")
|
||||
def _compute_user_hard_lock_to_date(self):
|
||||
for company in self:
|
||||
company.user_hard_lock_to_date = (
|
||||
min(
|
||||
c.hard_lock_to_date
|
||||
for c in company.with_context(active_test=False).sudo().parent_ids
|
||||
if c.hard_lock_to_date
|
||||
)
|
||||
if any(
|
||||
c.hard_lock_to_date
|
||||
for c in company.with_context(active_test=False).sudo().parent_ids
|
||||
)
|
||||
else False
|
||||
)
|
||||
|
||||
def _validate_locks(self, values):
|
||||
res = super()._validate_locks(values)
|
||||
if "hard_lock_to_date" in values:
|
||||
hard_lock_to_date = fields.Date.to_date(values["hard_lock_to_date"])
|
||||
for company in self:
|
||||
if not company.hard_lock_to_date:
|
||||
continue
|
||||
if not hard_lock_to_date:
|
||||
raise ValidationError(_("The Hard Lock Date cannot be removed."))
|
||||
if hard_lock_to_date > company.hard_lock_to_date:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"A new Hard Lock To Date must be prior "
|
||||
"(or equal) to the previous one."
|
||||
)
|
||||
)
|
||||
nb_draft_entries = self.env["account.move"].search(
|
||||
[
|
||||
("company_id", "child_of", self.ids),
|
||||
("state", "=", "draft"),
|
||||
("date", ">=", hard_lock_to_date),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if nb_draft_entries:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"There are still unposted entries in the period to date"
|
||||
" you want to hard lock. "
|
||||
"You should either post or delete them."
|
||||
)
|
||||
)
|
||||
self.env["res.company"].invalidate_model(
|
||||
fnames=[f"user_{field}" for field in LOCK_TO_DATE_FIELDS if field in values]
|
||||
)
|
||||
return res
|
||||
|
||||
def _get_user_lock_to_date(self, soft_lock_to_date_field, ignore_exceptions=False):
|
||||
self.ensure_one()
|
||||
soft_lock_to_date = False
|
||||
# We need to use sudo, since we might not have access to a parent company.
|
||||
for company in self.sudo().parent_ids:
|
||||
if company[soft_lock_to_date_field]:
|
||||
if ignore_exceptions:
|
||||
exception = None
|
||||
else:
|
||||
exception = self.env["account.lock_exception"].search(
|
||||
[
|
||||
("state", "=", "active"), # checks the datetime
|
||||
"|",
|
||||
("user_id", "=", None),
|
||||
("user_id", "=", self.env.user.id),
|
||||
(
|
||||
soft_lock_to_date_field,
|
||||
">",
|
||||
company[soft_lock_to_date_field],
|
||||
),
|
||||
("company_id", "=", company.id),
|
||||
],
|
||||
order="lock_date asc NULLS FIRST",
|
||||
limit=1,
|
||||
)
|
||||
if exception:
|
||||
# The search domain of the exception ensures
|
||||
# `exception[
|
||||
# soft_lock_to_date_field] > company[
|
||||
# soft_lock_to_date_field]`
|
||||
# or `exception[soft_lock_to_date_field] is False`
|
||||
soft_lock_to_date = (
|
||||
min(soft_lock_to_date, exception[soft_lock_to_date_field])
|
||||
if soft_lock_to_date and exception[soft_lock_to_date_field]
|
||||
else soft_lock_to_date
|
||||
or exception[soft_lock_to_date_field]
|
||||
or False
|
||||
)
|
||||
else:
|
||||
soft_lock_to_date = (
|
||||
min(soft_lock_to_date, company[soft_lock_to_date_field])
|
||||
if soft_lock_to_date and company[soft_lock_to_date_field]
|
||||
else soft_lock_to_date
|
||||
or company[soft_lock_to_date_field]
|
||||
or False
|
||||
)
|
||||
return soft_lock_to_date
|
||||
|
||||
def _get_user_fiscal_lock_to_date(self, journal, ignore_exceptions=False):
|
||||
self.ensure_one()
|
||||
company = self.with_context(ignore_exceptions=ignore_exceptions)
|
||||
lock = (
|
||||
min(company.user_fiscalyear_lock_to_date, company.user_hard_lock_to_date)
|
||||
if company.user_fiscalyear_lock_to_date and company.user_hard_lock_to_date
|
||||
else company.user_fiscalyear_lock_to_date
|
||||
or company.user_hard_lock_to_date
|
||||
or False
|
||||
)
|
||||
if journal.type == "sale":
|
||||
lock = (
|
||||
min(company.user_sale_lock_to_date, lock)
|
||||
if company.user_sale_lock_to_date and lock
|
||||
else company.user_sale_lock_to_date or lock or False
|
||||
)
|
||||
elif journal.type == "purchase":
|
||||
lock = (
|
||||
min(company.user_purchase_lock_to_date, lock)
|
||||
if company.user_purchase_lock_to_date and lock
|
||||
else company.user_purchase_lock_to_date or lock or False
|
||||
)
|
||||
return lock
|
||||
|
||||
def _get_violated_soft_lock_to_date(self, soft_lock_to_date_field, date):
|
||||
violated_date = None
|
||||
if not self:
|
||||
return violated_date
|
||||
self.ensure_one()
|
||||
user_lock_to_date_field = f"user_{soft_lock_to_date_field}"
|
||||
regular_lock_to_date = self.with_context(ignore_exceptions=True)[
|
||||
user_lock_to_date_field
|
||||
]
|
||||
if regular_lock_to_date and date >= regular_lock_to_date:
|
||||
user_lock_to_date = self.with_context(ignore_exceptions=False)[
|
||||
user_lock_to_date_field
|
||||
]
|
||||
violated_date = (
|
||||
None
|
||||
if regular_lock_to_date and date < user_lock_to_date
|
||||
else user_lock_to_date
|
||||
)
|
||||
return violated_date
|
||||
|
||||
def _get_lock_to_date_violations(
|
||||
self, accounting_date, fiscalyear=True, sale=True, purchase=True, hard=True
|
||||
):
|
||||
self.ensure_one()
|
||||
locks = []
|
||||
if not accounting_date:
|
||||
return locks
|
||||
soft_lock_to_date_fields_to_check = [
|
||||
# (field, "to check")
|
||||
("fiscalyear_lock_to_date", fiscalyear),
|
||||
("sale_lock_to_date", sale),
|
||||
("purchase_lock_to_date", purchase),
|
||||
]
|
||||
for field, to_check in soft_lock_to_date_fields_to_check:
|
||||
if not to_check:
|
||||
continue
|
||||
violated_date = self._get_violated_soft_lock_to_date(field, accounting_date)
|
||||
if violated_date:
|
||||
locks.append((violated_date, field))
|
||||
if hard:
|
||||
hard_lock_date = self.user_hard_lock_to_date
|
||||
if hard_lock_date and accounting_date >= hard_lock_date:
|
||||
locks.append((hard_lock_date, "hard_lock_date"))
|
||||
return locks
|
||||
|
||||
def write(self, values):
|
||||
companies = super().write(values)
|
||||
# We revoke all active exceptions affecting the changed lock to dates
|
||||
# and recreate them (with the updated lock to dates)
|
||||
changed_soft_lock_fields = [
|
||||
field for field in SOFT_LOCK_TO_DATE_FIELDS if field in values
|
||||
]
|
||||
for company in self:
|
||||
active_exceptions = self.env["account.lock_exception"].search(
|
||||
self.env["account.lock_exception"]._get_active_exceptions_to_domain(
|
||||
company, changed_soft_lock_fields
|
||||
),
|
||||
)
|
||||
active_exceptions._recreate()
|
||||
return companies
|
||||
Reference in New Issue
Block a user