[REF] account: make alias usage standard

Current usage of aliases in 'account.journal' model is a bit low-level and
is done by hand. This is notably due to some specific requirements for
accounting: batch install of journals with aliases with names holding
non-ascii characters, easy support of multi-company when creating aliases
with similar names, specific heuristic to generate alias name to be
understandable if sanitize / encoding leads to stripping lot of name parts.

Inherit from the 'mail.alias.mixin.optional' that automates management of
alias without requiring the "alias_id" field. It handles aliases like other
Odoo models, notably for security, readable / writable field, alias reset
or values synchronization. It allows to remove unwanted sudo().{write/unlink}
and have code more aligned with current standards.

Using this mixin allows to skip the inactive alias creation and instead keep
a void m2o. When having a lot of journals to create for some COA this helps
saving DB space and reduce number of inactive and useless aliases.

Task-3453343 (Mail: Cleanup Alias Usage)
Prepares Task-36879 (Mail: Support MultiCompany Aliases)

Part-of: odoo/odoo#130632
This commit is contained in:
Thibault Delavallée
2023-08-09 17:04:26 +02:00
parent 638e0f658d
commit 5747139122
3 changed files with 97 additions and 76 deletions
+74 -54
View File
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
from ast import literal_eval
from odoo import api, Command, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.addons.base.models.res_bank import sanitize_account_number
@@ -30,7 +33,11 @@ class AccountJournal(models.Model):
_name = "account.journal"
_description = "Journal"
_order = 'sequence, type, code'
_inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin']
_inherit = ['portal.mixin',
'mail.alias.mixin.optional',
'mail.thread',
'mail.activity.mixin',
]
_check_company_auto = True
_check_company_domain = models.check_company_domain_parent_of
_rec_names_search = ['name', 'code']
@@ -74,7 +81,6 @@ class AccountJournal(models.Model):
('bank', 'Bank'),
('general', 'Miscellaneous'),
], required=True,
inverse='_inverse_type',
help="Select 'Sale' for customer invoices journals.\n"\
"Select 'Purchase' for vendor bills journals.\n"\
"Select 'Cash' or 'Bank' for journals that are used in customer or vendor payments.\n"\
@@ -179,11 +185,9 @@ class AccountJournal(models.Model):
sale_activity_note = fields.Text('Activity Summary')
# alias configuration for journals
alias_id = fields.Many2one('mail.alias', string='Email Alias', help="Send one separate email for each invoice.\n\n"
"Any file extension will be accepted.\n\n"
"Only PDF and XML files will be interpreted by Odoo", copy=False)
alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain')
alias_name = fields.Char('Alias Name', copy=False, compute='_compute_alias_name', inverse='_inverse_type', help="It creates draft invoices and bills by sending an email.")
alias_id = fields.Many2one(help="Send one separate email for each invoice.\n\n"
"Any file extension will be accepted.\n\n"
"Only PDF and XML files will be interpreted by Odoo")
journal_group_ids = fields.Many2many('account.journal.group',
check_company=True,
@@ -351,52 +355,14 @@ class AccountJournal(models.Model):
else:
journal.suspense_account_id = False
def _inverse_type(self):
# Create an alias for purchase/sales journals
for journal in self:
if journal.type not in ('purchase', 'sale'):
if journal.alias_id:
journal.alias_id.sudo().unlink()
continue
alias_name = next(string for string in (
journal.alias_name,
journal.name,
journal.code,
journal.type,
) if string and self.env['mail.alias']._is_encodable(string))
if journal.company_id != self.env.ref('base.main_company'):
if self.env['mail.alias']._is_encodable(journal.company_id.name):
alias_name = f"{alias_name}-{journal.company_id.name}"
else:
alias_name = f"{alias_name}-{journal.company_id.id}"
alias_values = {
'alias_defaults': {
'move_type': 'in_invoice' if journal.type == 'purchase' else 'out_invoice',
'company_id': journal.company_id.id,
'journal_id': journal.id,
},
'alias_parent_thread_id': journal.id,
'alias_name': alias_name,
}
if journal.alias_id:
journal.alias_id.sudo().write(alias_values)
else:
alias_values['alias_model_id'] = self.env['ir.model']._get('account.move').id
alias_values['alias_parent_model_id'] = self.env['ir.model']._get('account.journal').id
journal.alias_id = self.env['mail.alias'].sudo().create(alias_values)
self.invalidate_recordset(['alias_name'])
@api.depends('name')
def _compute_alias_domain(self):
self.alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")
@api.depends('alias_id', 'alias_id.alias_name')
def _compute_alias_name(self):
for journal in self:
journal.alias_name = journal.alias_id.alias_name
@api.onchange('type')
def _onchange_type_for_alias(self):
self.filtered(lambda journal: journal.type not in {'sale', 'purchase'}).alias_name = False
for journal in self.filtered(lambda journal: (
not journal.alias_name and journal.type in {'sale', 'purchase'})
):
journal.alias_name = self._alias_prepare_alias_name(
False, journal.name, journal.code, journal.type, journal.company_id)
@api.constrains('account_control_ids')
def _constrains_account_control_ids(self):
@@ -505,7 +471,6 @@ class AccountJournal(models.Model):
accounts = self.search([('bank_account_id', '=', bank_account.id)])
if accounts <= self:
bank_accounts += bank_account
self.mapped('alias_id').sudo().unlink()
ret = super(AccountJournal, self).unlink()
bank_accounts.unlink()
return ret
@@ -542,6 +507,14 @@ class AccountJournal(models.Model):
return super(AccountJournal, self).copy(default)
def write(self, vals):
# for journals, force a readable name instead of a sanitized name e.g. non ascii in journal names
if vals.get('alias_name') and 'type' not in vals:
# will raise if writing name on more than 1 record, using self[0] is safe
if (not self.env['mail.alias']._is_encodable(vals['alias_name']) or
not self.env['mail.alias']._sanitize_alias_name(vals['alias_name'])):
vals['alias_name'] = self._alias_prepare_alias_name(
False, vals.get('name', self.name), vals.get('code', self.code), self[0].type, self[0].company_id)
for journal in self:
company = journal.company_id
if ('company_id' in vals and journal.company_id.id != vals['company_id']):
@@ -566,6 +539,16 @@ class AccountJournal(models.Model):
raise UserError(_("You cannot modify the field %s of a journal that already has accounting entries.", field_string))
result = super(AccountJournal, self).write(vals)
# Ensure alias coherency when changing type
if 'type' in vals:
for journal in self:
alias_vals = journal._alias_get_creation_values()
alias_vals = {
'alias_defaults': alias_vals['alias_defaults'],
'alias_name': alias_vals['alias_name'],
}
journal.update(alias_vals)
# Ensure the liquidity accounts are sharing the same foreign currency.
if 'currency_id' in vals:
for journal in self.filtered(lambda journal: journal.type in ('bank', 'cash')):
@@ -581,6 +564,37 @@ class AccountJournal(models.Model):
return result
def _alias_get_creation_values(self):
values = super()._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get_id('account.move')
if self.id:
values['alias_name'] = self._alias_prepare_alias_name(self.alias_name, self.name, self.code, self.type, self.company_id)
values['alias_defaults'] = defaults = literal_eval(self.alias_defaults or "{}")
defaults['company_id'] = self.company_id.id
defaults['move_type'] = 'in_invoice' if self.type == 'purchase' else 'out_invoice'
defaults['journal_id'] = self.id
return values
@api.model
def _alias_prepare_alias_name(self, alias_name, name, code, jtype, company):
""" Tool method generating standard journal alias, to ensure uniqueness
and readability; reset for other journals than purchase / sale """
if jtype not in ('purchase', 'sale'):
return False
alias_name = next(
(
string for string in (alias_name, name, code, jtype)
if (string and self.env['mail.alias']._is_encodable(string) and
self.env['mail.alias']._sanitize_alias_name(string))
), False
)
if company != self.env.ref('base.main_company'):
company_identifier = company.name if self.env['mail.alias']._is_encodable(company.name) else company.id
if f'-{company_identifier}' not in alias_name:
alias_name = f"{alias_name}-{company_identifier}"
return self.env['mail.alias']._sanitize_alias_name(alias_name)
@api.model
def get_next_bank_cash_default_code(self, journal_type, company, cache=None):
journal_code_base = (journal_type == 'cash' and 'CSH' or 'BNK')
@@ -654,6 +668,12 @@ class AccountJournal(models.Model):
if 'refund_sequence' not in vals:
vals['refund_sequence'] = vals['type'] in ('sale', 'purchase')
# === Fill missing alias name for sale / purchase, to force alias creation ===
if journal_type in {'sale', 'purchase'} and 'alias_name' not in vals:
vals['alias_name'] = self._alias_prepare_alias_name(
False, vals.get('name'), vals.get('code'), journal_type, company
)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
@@ -28,17 +28,6 @@ class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon):
cls.journal = cls.company_data['default_journal_purchase']
journal_alias = cls.env['mail.alias'].create({
'alias_name': 'test-bill',
'alias_model_id': cls.env.ref('account.model_account_move').id,
'alias_defaults': json.dumps({
'move_type': 'in_invoice',
'company_id': cls.env.user.company_id.id,
'journal_id': cls.journal.id,
}),
})
cls.journal.write({'alias_id': journal_alias.id})
def test_supplier_invoice_mailed_from_supplier(self):
message_parsed = {
'message_id': 'message-id-dead-beef',
+23 -11
View File
@@ -6,7 +6,7 @@ from unittest.mock import patch
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.account.models.account_payment_method import AccountPaymentMethod
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests import tagged
from odoo.tests import Form, tagged
from odoo.exceptions import UserError, ValidationError
@@ -195,9 +195,19 @@ class TestAccountJournalAlias(AccountTestInvoicingCommon, MailCommon):
'type': jtype}
for jtype in ('general', 'cash', 'bank')
])
self.assertFalse(journals.alias_id, 'Account: no mail support via no alias for those journal types')
self.assertFalse(journals.alias_id, 'Do not create useless aliases')
self.assertFalse(list(filter(None, journals.mapped('alias_name'))))
def test_alias_name_form(self):
""" Test alias name update using Form tool (onchange) """
journal = Form(self.env['account.journal'])
journal.name = 'Test With Form'
self.assertFalse(journal.alias_name)
journal.type = 'sale'
self.assertEqual(journal.alias_name, f'test-with-form-{self.env.company.name}')
journal.type = 'cash'
self.assertFalse(journal.alias_name)
def test_alias_from_type(self):
""" Test alias behavior on journal, especially alias_name management as
well as defaults update, see odoo/odoo@400b6860271a11b9914166ff7e42939c4c6192dc """
@@ -232,15 +242,15 @@ class TestAccountJournalAlias(AccountTestInvoicingCommon, MailCommon):
self.assertEqual(journal_alias.alias_parent_thread_id, journal.id,
'Journal alias owned by journal itself')
# update alias_name, various tests because it is sometimes actually funny
# update alias_name, ensure a fallback on a real name when not explicit reset
for alias_name, expected in [
(False, f'vendor-bills-{company_name}'), # do not want to reset
('', f'vendor-bills-{company_name}'), # still does not want to reset
(' ', f'-{company_name}'), # but why ?
('.', f'-{company_name}'), # but why ?
(False, False),
('', False),
(' ', f'vendor-bills-{company_name}'), # error recuperation
('.', f'vendor-bills-{company_name}'), # error recuperation
('😊', f'vendor-bills-{company_name}'), # resets, unicode not supported
('ぁ', f'vendor-bills-{company_name}'), # resets, ascii not supported
('Youpie Boum', f'youpie-boum-{company_name}'),
('ぁ', f'vendor-bills-{company_name}'), # resets, non ascii not supported
('Youpie Boum', 'youpie-boum'),
]:
with self.subTest(alias_name=alias_name):
journal.write({'alias_name': alias_name})
@@ -250,8 +260,10 @@ class TestAccountJournalAlias(AccountTestInvoicingCommon, MailCommon):
# changing type should void if not purchase or sale
for jtype in ('general', 'cash', 'bank'):
journal.write({'type': jtype})
self.assertFalse(journal_alias.exists())
self.assertFalse(journal.alias_id)
self.assertEqual(journal.alias_id, journal_alias,
'Dà not unlink aliases, just reset their value')
self.assertFalse(journal.alias_name)
self.assertFalse(journal_alias.alias_name)
# changing type should reset if sale or purchase
journal.company_id.write({'name': 'New Company Name'})