Skip to content
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

Closed
wants to merge 1 commit into from

Conversation

clbr-odoo
Copy link
Contributor

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. 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

…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
@robodoo
Copy link
Contributor

robodoo commented Oct 27, 2022

Pull request status dashboard

@C3POdoo C3POdoo requested review from a team and Levizar and removed request for a team October 27, 2022 14:30
@clbr-odoo
Copy link
Contributor Author

@william-andre @qdp-odoo

@C3POdoo C3POdoo added the RD research & development, internal work label Oct 27, 2022
Copy link
Contributor

@william-andre william-andre left a 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)
Copy link
Contributor

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

Comment on lines +19 to +27
@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()
Copy link
Contributor

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

Copy link
Contributor Author

@clbr-odoo clbr-odoo Oct 27, 2022

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.

Comment on lines +35 to +37
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()
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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

Comment on lines +266 to +287
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'), ['/', '/', '/'])
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines +8 to +12
from odoo.addons.account.tests.test_sequence_mixin import TestSequenceMixinConcurrency


@tagged('post_install', '-at_install', 'test_ir_sequence_concurrency')
class TestIrSequenceConcurrency(TestSequenceMixinConcurrency):
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor Author

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).

Copy link
Contributor

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?

Comment on lines +9 to +14
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)
Copy link
Contributor

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:

Suggested change
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 -*-
Copy link
Contributor

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

Comment on lines +40 to +48
@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()
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines +9 to +10
name = fields.Char(compute='_compute_name')
highest_name = fields.Char(compute='_compute_highest_name')
Copy link
Contributor

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

@celm1990
Copy link
Contributor

Only curiosity, What happen with latam modules(l10n_*) where use 1 only journal but many latam.documents, this module is not compatible?

https://github.com/odoo/odoo/blob/16.0/addons/l10n_latam_invoice_document/models/account_move.py#L127

@moylop260
Copy link
Contributor

@fpodoo
Copy link
Contributor

fpodoo commented Oct 27, 2022

That seems over-engineered to me. Two mechanisms just set a sequence. People have to choose between:

  • one that creates concurrency issues, or
  • one with all the issues we had before the mixin: if you import PG sequence is not updated, if you resequence/rename invoices you might not create new invoices anymore (name conflict), holes in sequences...

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:
#55930 (comment)

Why did we dismiss this approach? (It's not an issue for latam, just do the constraint on out invoice for latam)

@gdgellatly
Copy link
Contributor

@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.

@fpodoo
Copy link
Contributor

fpodoo commented Oct 28, 2022

change the type of a document, say Credit Note to Invoice

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:

  • resequence wizard will fail
  • cancel and delete last invoice before sending to customer --> hole in numbers
  • too much concurrency --> hole in invoice numbers
  • import old invoices --> creation will fail as numbers already exists (pg sequence not sync with imported numbers)
  • edit invoices number --> no more match with postgresql sequence

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)

@gdgellatly
Copy link
Contributor

change the type of a document, say Credit Note to Invoice

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)

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

<record model="ir.actions.server" id="action_move_switch_invoice_to_credit_note">
            <field name="name">Switch into refund/credit note</field>
            <field name="model_id" ref="account.model_account_move"/>
            <field name="groups_id" eval="[(4, ref('account.group_account_invoice'))]"/>
            <field name="binding_model_id" ref="account.model_account_move" />
            <field name="state">code</field>
            <field name="binding_view_types">form</field>
            <field name="code">
if records:
    action = records.action_switch_invoice_into_refund_credit_note()
            </field>
        </record>

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.

ir.sequence have real issues that you can break from UI:

  • resequence wizard will fail
    non issue as no need to resequence.
  • cancel and delete last invoice before sending to customer --> hole in numbers
    non issue in many jurisdictions incl NZ, Oz, UK
  • too much concurrency --> hole in invoice numbers
    non issue in many jurisdictions incl NZ, Oz, UK
  • import old invoices --> creation will fail as numbers already exists (pg sequence not sync with imported numbers)
    at a guess I've imported 100k invoices over the years - never realized this was an issue.
  • edit invoices number --> no more match with postgresql sequence
    again non issue, once issued, the invoice number is the number.

We always use Standard instead of no gap sequences. Weirdly we find we get far fewer gaps using standard.

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)

@clbr-odoo clbr-odoo closed this Oct 31, 2022
@william-andre
Copy link
Contributor

Replaced by completely different approach

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
RD research & development, internal work
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants