[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:
@@ -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',
|
||||
|
||||
@@ -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'})
|
||||
|
||||
Reference in New Issue
Block a user