diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index 8f7bc0741d5..6520a567b35 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -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: diff --git a/addons/account/tests/test_account_incoming_supplier_invoice.py b/addons/account/tests/test_account_incoming_supplier_invoice.py index 47a8efa31da..40fac722805 100644 --- a/addons/account/tests/test_account_incoming_supplier_invoice.py +++ b/addons/account/tests/test_account_incoming_supplier_invoice.py @@ -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', diff --git a/addons/account/tests/test_account_journal.py b/addons/account/tests/test_account_journal.py index 241ed690b4e..c3d7a35b008 100644 --- a/addons/account/tests/test_account_journal.py +++ b/addons/account/tests/test_account_journal.py @@ -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'})