-
Notifications
You must be signed in to change notification settings - Fork 23.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ADD] account_sequence: Reintroduce ir.sequence
in account_journal to fix concurrency issues
#104345
Conversation
…to fix concurrency issues This module allows to bind `sequence_id` to selected journals. Account_move names will then be computed based on `ir.sequence` and no longer on `sequence.mixin`. This is necessary in somes corner cases where `sequence.mixin` creates concurrency issues. Task: 3006843
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good
moves_with_ir_sequence = self.filtered('journal_id.sequence_id') | ||
for move in moves_with_ir_sequence: | ||
seq = move._get_journal_sequence() | ||
rec = self.env['account.move'].search([('journal_id.sequence_id', '=', seq.id)], order='name desc', limit=1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ordering with name desc
will not work depending on the padding
In [1]: "0001" < "0002" # expect True
Out[1]: True
In [2]: "10" < "2" # expect False
Out[2]: True
@api.depends('state', 'journal_id', 'date') | ||
def _compute_name(self): | ||
# OVERRIDE | ||
moves_with_ir_sequence = self.filtered('journal_id.sequence_id') | ||
for move in moves_with_ir_sequence: | ||
if not move.posted_before and not move.state == 'posted': | ||
seq = move._get_journal_sequence() | ||
move.name = seq.get_next_char(seq.number_next_actual) | ||
super(AccountMove, self - moves_with_ir_sequence)._compute_name() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we compute the name when it is not posted?
I'm not sure how it can work with ir.sequence
because you can't know when you have to consume a new number
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to keep current behavior as much as possible and keep the feature.
That's why I'm never consuming number in _compute_name()
, only when _post()
ing for the first time.
if not move.posted_before and (move.name == '/' or move.name == next_name): | ||
# We only compute new name if user didn't manually change the name | ||
move.name = seq.with_context(ir_sequence_date=move.date).next_by_id() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we also force sequence_number
and sequence_prefix
from here, so that the gap detection mechanism works too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If i'm correct, sequence_number
and sequence_prefix
are computed from name
so holes detection does work.
See here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it doesn't work for crazy sequences that don't match the prefix, which can be the reason why someone uses ir.sequence
def test_sequence_concurrency_payments(self): | ||
"""Posting concurrent payments should not raise errors | ||
with sequence's standard implementation""" | ||
env0, env1, env2 = self.data['envs'] | ||
|
||
# start the transactions here on cr1 to simulate concurrency with cr2 | ||
env1.cr.execute('SELECT 1') | ||
|
||
# Post new payment in cr2 | ||
payment1 = env2['account.payment'].browse(self.data['payment_ids'][1]) | ||
payment1.action_post() | ||
env2.cr.commit() | ||
|
||
# Post another payment in cr1 | ||
payment2 = env1['account.payment'].browse(self.data['payment_ids'][2]) | ||
payment2.action_post() | ||
env1.cr.commit() | ||
|
||
# Check values | ||
payments = env0['account.payment'].browse(self.data['payment_ids']) | ||
# Everything should be posted | ||
self.assertNotEqual(payments.mapped('name'), ['/', '/', '/']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the point of the concurrency with the payments was to
- post an invoice in journal I in transaction T1
- post a payment in journal P in transaction T2
- post a payment in journal P in transaction T1
- post an invoice in journal I in transaction T2
This would lead to a deadlock, leaving the connection hanging if not detected by PostgreSQL.
At least it is the use case I remember from the original feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I will test that too.
from odoo.addons.account.tests.test_sequence_mixin import TestSequenceMixinConcurrency | ||
|
||
|
||
@tagged('post_install', '-at_install', 'test_ir_sequence_concurrency') | ||
class TestIrSequenceConcurrency(TestSequenceMixinConcurrency): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Importing and inheriting this class will lead to running the tests 3 times
- first time when running the tests of
account
, where the class is defined - second time when importing it in this file, since the test suite will detect the name
TestSequenceMixinConcurrency
in a test file and run it - third time when running all the tests of
TestIrSequenceConcurrency
since it inherited the tests of the parent class
In order to avoid that, split the setup and the tests of TestSequenceMixinConcurrency
in 2 different classes and inherit only the setup from here
seq = move._get_journal_sequence() | ||
next_name = seq.get_next_char(seq.number_next_actual) | ||
if not move.posted_before and (move.name == '/' or move.name == next_name): | ||
# We only compute new name if user didn't manually change the name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand how one can edit manually the number when using ir.sequence
It should even be readonly in the view I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically when the user edited the name it's kept like that on posting (same behavior as mixins), the ir_sequence won't be touched (so yes you can manually create holes, but it will warn you, just like with current mixin).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But then it will give you the same number twice?
sequence_id = fields.Many2one('ir.sequence', string='Entry Sequence', | ||
help='This field contains the information related to the numbering of the journal entries of this journal.', | ||
copy=False, check_company=True) | ||
refund_sequence_id = fields.Many2one('ir.sequence', string='Credit Note Entry Sequence', | ||
help='This field contains the information related to the numbering of the credit note entries of this journal.', | ||
copy=False, check_company=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This field contains...
A bit too technical for a tooltip
Regarding the spacing, just a personal preference:
sequence_id = fields.Many2one('ir.sequence', string='Entry Sequence', | |
help='This field contains the information related to the numbering of the journal entries of this journal.', | |
copy=False, check_company=True) | |
refund_sequence_id = fields.Many2one('ir.sequence', string='Credit Note Entry Sequence', | |
help='This field contains the information related to the numbering of the credit note entries of this journal.', | |
copy=False, check_company=True) | |
sequence_id = fields.Many2one( | |
comodel_name='ir.sequence', | |
string='Entry Sequence', | |
copy=False, | |
check_company=True, | |
help="This field contains the information related to the numbering of the journal entries of this journal.", | |
) | |
refund_sequence_id = fields.Many2one( | |
comodel_name='ir.sequence', | |
string='Credit Note Entry Sequence', | |
copy=False, | |
check_company=True, | |
help="This field contains the information related to the numbering of the credit note entries of this journal.", | |
) |
@@ -0,0 +1,3 @@ | |||
# -*- coding: utf-8 -*- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is always the case since python3, it is generally preferred not to add it
@api.depends('journal_id', 'date') | ||
def _compute_highest_name(self): | ||
# OVERRIDE | ||
moves_with_ir_sequence = self.filtered('journal_id.sequence_id') | ||
for move in moves_with_ir_sequence: | ||
seq = move._get_journal_sequence() | ||
rec = self.env['account.move'].search([('journal_id.sequence_id', '=', seq.id)], order='name desc', limit=1) | ||
move.highest_name = rec.name or False | ||
super(AccountMove, self - moves_with_ir_sequence)._compute_highest_name() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need the highest_name
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To not break that warning.
name = fields.Char(compute='_compute_name') | ||
highest_name = fields.Char(compute='_compute_highest_name') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is useless as it is not changing anything from the original declaration
Only curiosity, What happen with latam modules(l10n_*) where use 1 only journal but many latam.documents, this module is not compatible? |
That seems over-engineered to me. Two mechanisms just set a sequence. People have to choose between:
As people won't understand both approaches, they will still have concurrency without understanding what to do. Instead I proposed an alternative to fix the root cause of the issue (concurrency due to lock on preceding invoice), based on unique constraints properties: Why did we dismiss this approach? (It's not an issue for latam, just do the constraint on out invoice for latam) |
@fpodoo The current implementation causes more issues than just concurrency. Take for example, change the type of a document, say Credit Note to Invoice and like magic all your Invoices now take credit note sequences. Don't notice for an hour, or a day or a week, you can't fix new ones because at least in nearly every tax jurisdiction I've ever known, invoices must have a unique identifier that does not change once they are in the hands of an external party (primarily to prevent VAT fraud). Same for changing journals. At scale, with lots of users this whole move sequencing thing is a bit of a mess tbh. |
What do you mean? That's not even a real use case as Odoo does not allow to change the type of a document through the interface. (e.g. you probably messed up with SQL directly, but whatever the implementation, if you break consistency of data through SQL, you'll have issues) ir.sequence have real issues that you can break from UI:
compared to that, the drawbacks of the mixin are very limited (the only issue I see is when you change prefix / numbering which is a weird use case) |
Lol, such a basic function I assume it was part of core. Absolutely necessary for emailed in invoices going direct to journal. Lol again, I checked, it is part of odoo account module
But in any case, the other one we face which sucks even harder is users accidentally overtyping the invoice number with vendor invoice number. But actually that doesn't make such a mess as you never send supplier invoices.
We always use Standard instead of no gap sequences. Weirdly we find we get far fewer gaps using standard.
|
Replaced by completely different approach |
This module allows to bind
sequence_id
to selected journals. Account_move names will then be computed based onir.sequence
and no longer onsequence.mixin
. When sequence is set to no_gap implementation, the behavior will be the same as before however standard implementation of sequences will allow to solve concurrency issues that happened in some cases with mixins.Task: 3006843