From 34ea8f97aebed444e243270ba55813116e67a478 Mon Sep 17 00:00:00 2001 From: wan Date: Fri, 14 Aug 2020 11:42:53 +0000 Subject: [PATCH] [FIX] account: concurency in sequence.mixin The SELECT FOR UPDATE clause stopped concurent executions but did not prevent from writing on other rows. By doing a real UPDATE, we now have the desired behaviour with a SerializationFailure error. closes odoo/odoo#58118 X-original-commit: ba6ee0b75d66d6f22a592b1b6a7c5158461bec41 Signed-off-by: Olivier Dony (odo) Signed-off-by: wan --- addons/account/models/sequence_mixin.py | 13 +++-- .../account/tests/test_account_move_entry.py | 51 ++++++++++++++++++- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/addons/account/models/sequence_mixin.py b/addons/account/models/sequence_mixin.py index 03579ece21a..7e84d0bc469 100644 --- a/addons/account/models/sequence_mixin.py +++ b/addons/account/models/sequence_mixin.py @@ -136,11 +136,14 @@ class SequenceMixin(models.AbstractModel): param['id'] = self.id or self.id.origin query = """ - SELECT {field} FROM {table} - {where_string} - AND sequence_prefix = (SELECT sequence_prefix FROM account_move {where_string} ORDER BY id DESC LIMIT 1) - ORDER BY sequence_number DESC - LIMIT 1 FOR UPDATE + UPDATE {table} SET write_date = write_date WHERE id = ( + SELECT id FROM {table} + {where_string} + AND sequence_prefix = (SELECT sequence_prefix FROM {table} {where_string} ORDER BY id DESC LIMIT 1) + ORDER BY sequence_number DESC + LIMIT 1 + ) + RETURNING {field}; """.format( table=self._table, where_string=where_string, diff --git a/addons/account/tests/test_account_move_entry.py b/addons/account/tests/test_account_move_entry.py index 9cd3bb71419..2b1b5bd412a 100644 --- a/addons/account/tests/test_account_move_entry.py +++ b/addons/account/tests/test_account_move_entry.py @@ -2,12 +2,14 @@ from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests import tagged, new_test_user from odoo.tests.common import Form -from odoo import fields +from odoo import fields, api, SUPERUSER_ID from odoo.exceptions import ValidationError, UserError, RedirectWarning +from odoo.tools import mute_logger from dateutil.relativedelta import relativedelta from functools import reduce import json +import psycopg2 @tagged('post_install', '-at_install') @@ -548,6 +550,53 @@ class TestAccountMove(AccountTestInvoicingCommon): self.assertEqual(copies[5].name, 'XMISC/2019/10005') self.assertEqual(copies[5].state, 'draft') + def test_sequence_concurency(self): + with self.env.registry.cursor() as cr0,\ + self.env.registry.cursor() as cr1,\ + self.env.registry.cursor() as cr2: + env0 = api.Environment(cr0, SUPERUSER_ID, {}) + env1 = api.Environment(cr1, SUPERUSER_ID, {}) + env2 = api.Environment(cr2, SUPERUSER_ID, {}) + + journal = env0['account.journal'].create({ + 'name': 'concurency_test', + 'code': 'CT', + 'type': 'general', + }) + account = env0['account.account'].create({ + 'code': 'CT', + 'name': 'CT', + 'user_type_id': env0.ref('account.data_account_type_fixed_assets').id, + }) + moves = env0['account.move'].create([{ + 'journal_id': journal.id, + 'date': fields.Date.from_string('2016-01-01'), + 'line_ids': [(0, 0, {'name': 'name', 'account_id': account.id})] + }] * 3) + moves.name = '/' + moves[0].action_post() + self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', '/', '/']) + env0.cr.commit() + + # start the transactions here on cr2 to simulate concurency with cr1 + env2.cr.execute('SELECT 1') + + move = env1['account.move'].browse(moves[1].id) + move.action_post() + env1.cr.commit() + + move = env2['account.move'].browse(moves[2].id) + with self.assertRaises(psycopg2.OperationalError), env2.cr.savepoint(), mute_logger('odoo.sql_db'): + move.action_post() + + self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', 'CT/2016/01/0002', '/']) + moves.button_draft() + moves.posted_before = False + moves.unlink() + journal.unlink() + account.unlink() + env0.cr.commit() + def test_add_followers_on_post(self): # Add some existing partners, some from another company company = self.env['res.company'].create({'name': 'Oopo'})