diff --git a/.copier-answers.yml b/.copier-answers.yml
index f399794a7..e3275c67b 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,5 +1,5 @@
# Do NOT update manually; changes here will be overwritten by Copier
-_commit: v1.19
+_commit: v1.20
_src_path: gh:oca/oca-addons-repo-template
additional_ruff_rules: []
ci: GitHub
@@ -16,7 +16,8 @@ odoo_test_flavor: Both
odoo_version: 17.0
org_name: Odoo Community Association (OCA)
org_slug: OCA
-rebel_module_groups: []
+rebel_module_groups:
+- account_move_name_sequence
repo_description: 'TODO: add repo description.'
repo_name: account-financial-tools
repo_slug: account-financial-tools
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 36c22a87b..f1efe6ba1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -36,8 +36,17 @@ jobs:
matrix:
include:
- container: ghcr.io/oca/oca-ci/py3.10-odoo17.0:latest
+ include: "account_move_name_sequence"
name: test with Odoo
- container: ghcr.io/oca/oca-ci/py3.10-ocb17.0:latest
+ include: "account_move_name_sequence"
+ name: test with OCB
+ makepot: "true"
+ - container: ghcr.io/oca/oca-ci/py3.10-odoo17.0:latest
+ exclude: "account_move_name_sequence"
+ name: test with Odoo
+ - container: ghcr.io/oca/oca-ci/py3.10-ocb17.0:latest
+ exclude: "account_move_name_sequence"
name: test with OCB
makepot: "true"
services:
@@ -49,6 +58,9 @@ jobs:
POSTGRES_DB: odoo
ports:
- 5432:5432
+ env:
+ INCLUDE: "${{ matrix.include }}"
+ EXCLUDE: "${{ matrix.exclude }}"
steps:
- uses: actions/checkout@v3
with:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2403da513..4523057d7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -41,7 +41,7 @@ repos:
hooks:
- id: whool-init
- repo: https://github.com/oca/maintainer-tools
- rev: f71041f22b8cd68cf7c77b73a14ca8d8cd190a60
+ rev: 9a170331575a265c092ee6b24b845ec508e8ef75
hooks:
# update the NOT INSTALLABLE ADDONS section above
- id: oca-update-pre-commit-excluded-addons
@@ -110,7 +110,7 @@ repos:
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/OCA/pylint-odoo
- rev: v8.0.19
+ rev: v9.0.4
hooks:
- id: pylint_odoo
name: pylint with optional checks
diff --git a/account_move_name_sequence/__manifest__.py b/account_move_name_sequence/__manifest__.py
index 0feb2f643..e07526e24 100644
--- a/account_move_name_sequence/__manifest__.py
+++ b/account_move_name_sequence/__manifest__.py
@@ -17,6 +17,9 @@
"depends": [
"account",
],
+ "demo": [
+ "demo/ir_sequence_demo.xml",
+ ],
"data": [
"views/account_journal.xml",
"views/account_move.xml",
diff --git a/account_move_name_sequence/demo/ir_sequence_demo.xml b/account_move_name_sequence/demo/ir_sequence_demo.xml
new file mode 100644
index 000000000..34f9c5f56
--- /dev/null
+++ b/account_move_name_sequence/demo/ir_sequence_demo.xml
@@ -0,0 +1,11 @@
+
+
+
+ Standard Sequence Demo
+ demo/%(range_year)s/
+
+ 1
+ 1
+
+
+
diff --git a/account_move_name_sequence/i18n/sl.po b/account_move_name_sequence/i18n/sl.po
new file mode 100644
index 000000000..a215b199c
--- /dev/null
+++ b/account_move_name_sequence/i18n/sl.po
@@ -0,0 +1,156 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * account_move_name_sequence
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 14.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2023-03-30 12:22+0000\n"
+"Last-Translator: Matjaz Mozetic \n"
+"Language-Team: none\n"
+"Language: sl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
+"%100==4 ? 2 : 3;\n"
+"X-Generator: Weblate 4.14.1\n"
+
+#. module: account_move_name_sequence
+#: model:ir.model.constraint,message:account_move_name_sequence.constraint_account_move_name_state_diagonal
+msgid ""
+"A move can not be posted with name \"/\" or empty value\n"
+"Check the journal sequence, please"
+msgstr ""
+"Temeljnice ni mogoče knjižiti z nazivom \"/\" ali prazno vrednostjo\n"
+"Prosimo, da preverite zaporedje v dnevniku"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,help:account_move_name_sequence.field_account_journal__refund_sequence
+msgid ""
+"Check this box if you don't want to share the same sequence for invoices and"
+" credit notes made from this journal"
+msgstr ""
+"Označite, če želite v tem dnevniku uporabljati isto zaporedje za račune in "
+"dobropise"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__refund_sequence_id
+msgid "Credit Note Entry Sequence"
+msgstr "Zaporedje za dobropise"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__refund_sequence
+msgid "Dedicated Credit Note Sequence"
+msgstr "Ločeno zaporedje za dobropise"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__display_name
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__display_name
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_ir_sequence__display_name
+msgid "Display Name"
+msgstr "Prikazani naziv"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__sequence_id
+msgid "Entry Sequence"
+msgstr "Zaporedje vnosa"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__highest_name
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__highest_name
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_payment__highest_name
+msgid "Highest Name"
+msgstr "Najvišji naziv"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__id
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__id
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_ir_sequence__id
+msgid "ID"
+msgstr "ID"
+
+#. module: account_move_name_sequence
+#: model:ir.model,name:account_move_name_sequence.model_account_journal
+msgid "Journal"
+msgstr "Dnevnik"
+
+#. module: account_move_name_sequence
+#: model:ir.model,name:account_move_name_sequence.model_account_move
+msgid "Journal Entry"
+msgstr "Dnevniški vnos"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal____last_update
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move____last_update
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_ir_sequence____last_update
+msgid "Last Modified on"
+msgstr "Zadnjič spremenjeno"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__name
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__name
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_payment__name
+msgid "Number"
+msgstr "Številka"
+
+#. module: account_move_name_sequence
+#: code:addons/account_move_name_sequence/models/account_journal.py:0
+#, python-format
+msgid ""
+"On journal '%s', the same sequence is used as Entry Sequence and Credit Note"
+" Entry Sequence."
+msgstr "V dnevniku '%s' se uporablja isto zaporedje za vnose in dobropise."
+
+#. module: account_move_name_sequence
+#: code:addons/account_move_name_sequence/models/account_journal.py:0
+#, python-format
+msgid "Refund"
+msgstr "Povračilo"
+
+#. module: account_move_name_sequence
+#: code:addons/account_move_name_sequence/models/account_journal.py:0
+#: model:ir.model,name:account_move_name_sequence.model_ir_sequence
+#, python-format
+msgid "Sequence"
+msgstr "Zaporedje"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__sequence_number
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__sequence_number
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_payment__sequence_number
+msgid "Sequence Number"
+msgstr "Številka zaporedja"
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__sequence_prefix
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__sequence_prefix
+#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_payment__sequence_prefix
+msgid "Sequence Prefix"
+msgstr "Predpona zaporedja"
+
+#. module: account_move_name_sequence
+#: code:addons/account_move_name_sequence/models/account_journal.py:0
+#, python-format
+msgid ""
+"The company is not set on sequence '%s' configured as credit note sequence "
+"of journal '%s'."
+msgstr "Pri zaporedju '%s' za dobropise v dnevniku '%s' ni nastavljena družba."
+
+#. module: account_move_name_sequence
+#: code:addons/account_move_name_sequence/models/account_journal.py:0
+#, python-format
+msgid "The company is not set on sequence '%s' configured on journal '%s'."
+msgstr "Pri zaporedju '%s' nastavljenem v dnevniku '%s' ni nastavljena družba."
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,help:account_move_name_sequence.field_account_journal__refund_sequence_id
+msgid ""
+"This sequence will be used to generate the journal entry number for refunds."
+msgstr "To zaporedje bo v rabi za knjigovodske vnose dobropisov."
+
+#. module: account_move_name_sequence
+#: model:ir.model.fields,help:account_move_name_sequence.field_account_journal__sequence_id
+msgid "This sequence will be used to generate the journal entry number."
+msgstr "To zaporedje bo v rabi za knjigovodske vnose dobropisov."
diff --git a/account_move_name_sequence/models/account_move.py b/account_move_name_sequence/models/account_move.py
index f99bc9fde..1514b6cb8 100644
--- a/account_move_name_sequence/models/account_move.py
+++ b/account_move_name_sequence/models/account_move.py
@@ -77,3 +77,20 @@ class AccountMove(models.Model):
def _get_last_sequence(self, relaxed=False, with_prefix=None):
return super()._get_last_sequence(relaxed, None)
+
+ @api.onchange("journal_id")
+ def _onchange_journal_id(self):
+ if not self.quick_edit_mode:
+ self.name = "/"
+ self._compute_name_by_sequence()
+
+ def _post(self, soft=True):
+ self.flush_recordset()
+ return super()._post(soft=soft)
+
+ def _compute_name(self):
+ """Overwrite account module method in order to
+ avoid side effect if legacy code call it directly
+ like when creating entry from email.
+ """
+ return self._compute_name_by_sequence()
diff --git a/account_move_name_sequence/tests/__init__.py b/account_move_name_sequence/tests/__init__.py
index 5de02aafc..7f2d011b8 100644
--- a/account_move_name_sequence/tests/__init__.py
+++ b/account_move_name_sequence/tests/__init__.py
@@ -1 +1,3 @@
from . import test_account_move_name_seq
+from . import test_sequence_concurrency
+from . import test_account_incoming_supplier_invoice
diff --git a/account_move_name_sequence/tests/test_account_incoming_supplier_invoice.py b/account_move_name_sequence/tests/test_account_incoming_supplier_invoice.py
new file mode 100644
index 000000000..7545ac0be
--- /dev/null
+++ b/account_move_name_sequence/tests/test_account_incoming_supplier_invoice.py
@@ -0,0 +1,85 @@
+import json
+
+from odoo.tests import tagged
+
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+
+
+@tagged("post_install", "-at_install")
+class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon):
+ """Testing creating account move fetching mail.alias"""
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.env["ir.config_parameter"].sudo().set_param(
+ "mail.catchall.domain", "test-company.odoo.com"
+ )
+
+ cls.internal_user = cls.env["res.users"].create(
+ {
+ "name": "Internal User",
+ "login": "internal.user@test.odoo.com",
+ "email": "internal.user@test.odoo.com",
+ }
+ )
+
+ cls.supplier_partner = cls.env["res.partner"].create(
+ {
+ "name": "Your Supplier",
+ "email": "supplier@other.company.com",
+ "supplier_rank": 10,
+ }
+ )
+
+ 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):
+ """this test is mainly inspired from
+ addons.account.tests.test_account_incoming_supplier_invoice
+ python module but we make sure account move is draft without
+ name
+ """
+ message_parsed = {
+ "message_id": "message-id-dead-beef",
+ "subject": "Incoming bill",
+ "from": f"{self.supplier_partner.name} <{self.supplier_partner.email}>",
+ "to": f"{self.journal.alias_id.alias_name}@{self.journal.alias_id.alias_domain}",
+ "body": "You know, that thing that you bought.",
+ "attachments": [b"Hello, invoice"],
+ }
+
+ invoice = self.env["account.move"].message_new(
+ message_parsed, {"move_type": "in_invoice", "journal_id": self.journal.id}
+ )
+
+ message_ids = invoice.message_ids
+ self.assertEqual(
+ len(message_ids), 1, "Only one message should be posted in the chatter"
+ )
+ self.assertEqual(
+ message_ids.body,
+ "Vendor Bill Created
",
+ "Only the invoice creation should be posted",
+ )
+
+ following_partners = invoice.message_follower_ids.mapped("partner_id")
+ self.assertEqual(following_partners, self.env.user.partner_id)
+ self.assertEqual(invoice.state, "draft")
+ self.assertEqual(invoice.name, "/")
diff --git a/account_move_name_sequence/tests/test_sequence_concurrency.py b/account_move_name_sequence/tests/test_sequence_concurrency.py
new file mode 100644
index 000000000..0892e5da4
--- /dev/null
+++ b/account_move_name_sequence/tests/test_sequence_concurrency.py
@@ -0,0 +1,328 @@
+import logging
+import threading
+import time
+
+import psycopg2
+
+import odoo
+from odoo import SUPERUSER_ID, api, fields, tools
+from odoo.tests import tagged
+from odoo.tests.common import Form, TransactionCase
+
+_logger = logging.getLogger(__name__)
+
+
+class ThreadRaiseJoin(threading.Thread):
+ """Custom Thread Class to raise the exception to main thread in the join"""
+
+ def run(self, *args, **kwargs):
+ self.exc = None
+ try:
+ return super().run(*args, **kwargs)
+ except BaseException as e:
+ self.exc = e
+
+ def join(self, *args, **kwargs):
+ res = super().join(*args, **kwargs)
+ # Wait for the thread finishes
+ while self.is_alive():
+ pass
+ # raise exception in the join
+ # to raise it in the main thread
+ if self.exc:
+ raise self.exc
+ return res
+
+
+@tagged("post_install", "-at_install", "test_move_sequence")
+class TestSequenceConcurrency(TransactionCase):
+ def setUp(self):
+ super().setUp()
+ self.product = self.env.ref("product.product_delivery_01")
+ self.partner = self.env.ref("base.res_partner_12")
+ self.date = fields.Date.to_date("1985-04-14")
+
+ def _new_cr(self):
+ return self.env.registry.cursor()
+
+ def _create_invoice_form(self, env, post=True):
+ ctx = {"default_move_type": "out_invoice"}
+ with Form(env["account.move"].with_context(**ctx)) as invoice_form:
+ invoice_form.partner_id = self.partner
+ invoice_form.invoice_date = self.date
+
+ with invoice_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product
+ line_form.price_unit = 100.0
+ line_form.tax_ids.clear()
+ invoice = invoice_form.save()
+ if post:
+ invoice.action_post()
+ return invoice
+
+ def _create_payment_form(self, env, ir_sequence_standard=False):
+ with Form(
+ env["account.payment"].with_context(
+ default_payment_type="inbound",
+ default_partner_type="customer",
+ default_move_journal_types=("bank", "cash"),
+ )
+ ) as payment_form:
+ payment_form.partner_id = env.ref("base.res_partner_12")
+ payment_form.amount = 100
+ payment_form.date = self.date
+
+ payment = payment_form.save()
+ if ir_sequence_standard:
+ payment.move_id.journal_id.sequence_id = self.env.ref(
+ "account_move_name_sequence.ir_sequence_demo"
+ )
+ payment.action_post()
+ return payment
+
+ def _clean_moves(self, move_ids, payment=None):
+ """Delete moves created after finish unittest using
+ self.addCleanup(
+ self._clean_moves, self.env, (invoices | payments.mapped('move_id')).ids
+ )"""
+ with self._new_cr() as cr:
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ moves = env["account.move"].browse(move_ids)
+ moves.button_draft()
+ moves = moves.with_context(force_delete=True)
+ moves.unlink()
+ # TODO: Delete payment and journal
+ env.cr.commit()
+
+ def _create_invoice_payment(
+ self, deadlock_timeout, payment_first=False, ir_sequence_standard=False
+ ):
+ odoo.registry(self.env.cr.dbname)
+ with self._new_cr() as cr, cr.savepoint():
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ cr_pid = cr.connection.get_backend_pid()
+ # Avoid waiting for a long time and it needs to be less than deadlock
+ cr.execute("SET LOCAL statement_timeout = '%ss'", (deadlock_timeout + 10,))
+ if payment_first:
+ _logger.info("Creating payment cr %s", cr_pid)
+ self._create_payment_form(
+ env, ir_sequence_standard=ir_sequence_standard
+ )
+ _logger.info("Creating invoice cr %s", cr_pid)
+ self._create_invoice_form(env)
+ else:
+ _logger.info("Creating invoice cr %s", cr_pid)
+ self._create_invoice_form(env)
+ _logger.info("Creating payment cr %s", cr_pid)
+ self._create_payment_form(
+ env, ir_sequence_standard=ir_sequence_standard
+ )
+ # sleep in order to avoid release the locks too faster
+ # It could be many methods called after creating these
+ # kind of records e.g. reconcile
+ _logger.info("Finishing waiting %s" % (deadlock_timeout + 12))
+ time.sleep(deadlock_timeout + 12)
+
+ def test_sequence_concurrency_10_draft_invoices(self):
+ """Creating 2 DRAFT invoices not should raises errors"""
+ with self._new_cr() as cr0, self._new_cr() as cr1, self._new_cr() as cr2:
+ env0 = api.Environment(cr0, SUPERUSER_ID, {})
+ env1 = api.Environment(cr1, SUPERUSER_ID, {})
+ env2 = api.Environment(cr2, SUPERUSER_ID, {})
+ for cr in [cr0, cr1, cr2]:
+ # Set 10s timeout in order to avoid waiting for release locks a long time
+ cr.execute("SET LOCAL statement_timeout = '10s'")
+
+ # Create "last move" to lock
+ invoice = self._create_invoice_form(env0)
+ self.addCleanup(self._clean_moves, invoice.ids)
+ env0.cr.commit()
+ with env1.cr.savepoint(), env2.cr.savepoint():
+ invoice1 = self._create_invoice_form(env1, post=False)
+ self.assertEqual(invoice1.state, "draft")
+ invoice2 = self._create_invoice_form(env2, post=False)
+ self.assertEqual(invoice2.state, "draft")
+
+ def test_sequence_concurrency_20_editing_last_invoice(self):
+ """Edit last invoice and create a new invoice
+ should not raises errors"""
+ with self._new_cr() as cr0, self._new_cr() as cr1:
+ env0 = api.Environment(cr0, SUPERUSER_ID, {})
+ env1 = api.Environment(cr1, SUPERUSER_ID, {})
+ for cr in [cr0, cr1]:
+ # Set 10s timeout in order to avoid waiting for release locks a long time
+ cr.execute("SET LOCAL statement_timeout = '10s'")
+
+ # Create "last move" to lock
+ invoice = self._create_invoice_form(env0)
+
+ self.addCleanup(self._clean_moves, invoice.ids)
+ env0.cr.commit()
+ with env0.cr.savepoint(), env1.cr.savepoint():
+ # Edit something in "last move"
+ invoice.write({"write_uid": env0.uid})
+ env0.flush_all()
+ self._create_invoice_form(env1)
+
+ def test_sequence_concurrency_30_editing_last_payment(self):
+ """Edit last payment and create a new payment
+ should not raises errors"""
+ with self._new_cr() as cr0, self._new_cr() as cr1:
+ env0 = api.Environment(cr0, SUPERUSER_ID, {})
+ env1 = api.Environment(cr1, SUPERUSER_ID, {})
+ for cr in [cr0, cr1]:
+ # Set 10s timeout in order to avoid waiting for release locks a long time
+ cr.execute("SET LOCAL statement_timeout = '10s'")
+
+ # Create "last move" to lock
+ payment = self._create_payment_form(env0)
+ payment_move = payment.move_id
+ self.addCleanup(self._clean_moves, payment_move.ids)
+ env0.cr.commit()
+ with env0.cr.savepoint(), env1.cr.savepoint():
+ # Edit something in "last move"
+ payment_move.write({"write_uid": env0.uid})
+ env0.flush_all()
+ self._create_payment_form(env1)
+
+ @tools.mute_logger("odoo.sql_db")
+ def test_sequence_concurrency_40_reconciling_last_invoice(self):
+ """Reconcile last invoice and create a new one
+ should not raises errors"""
+ with self._new_cr() as cr0, self._new_cr() as cr1:
+ env0 = api.Environment(cr0, SUPERUSER_ID, {})
+ env1 = api.Environment(cr1, SUPERUSER_ID, {})
+ for cr in [cr0, cr1]:
+ # Set 10s timeout in order to avoid waiting for release locks a long time
+ cr.execute("SET LOCAL statement_timeout = '10s'")
+
+ # Create "last move" to lock
+ invoice = self._create_invoice_form(env0)
+ payment = self._create_payment_form(env0)
+ payment_move = payment.move_id
+ self.addCleanup(self._clean_moves, invoice.ids + payment_move.ids)
+ env0.cr.commit()
+ lines2reconcile = (
+ (payment_move | invoice)
+ .mapped("line_ids")
+ .filtered(
+ lambda line: line.account_id.account_type == "asset_receivable"
+ )
+ )
+ with env0.cr.savepoint(), env1.cr.savepoint():
+ # Reconciling "last move"
+ # reconcile a payment with many invoices spend a lot so it could
+ # lock records too many time
+ lines2reconcile.reconcile()
+ # Many pieces of code call flush directly
+ env0.flush_all()
+ self._create_invoice_form(env1)
+
+ def test_sequence_concurrency_50_reconciling_last_payment(self):
+ """Reconcile last payment and create a new one
+ should not raises errors"""
+ with self._new_cr() as cr0, self._new_cr() as cr1:
+ env0 = api.Environment(cr0, SUPERUSER_ID, {})
+ env1 = api.Environment(cr1, SUPERUSER_ID, {})
+ for cr in [cr0, cr1]:
+ # Set 10s timeout in order to avoid waiting for release locks a long time
+ cr.execute("SET LOCAL statement_timeout = '10s'")
+
+ # Create "last move" to lock
+ invoice = self._create_invoice_form(env0)
+ payment = self._create_payment_form(env0)
+ payment_move = payment.move_id
+ self.addCleanup(self._clean_moves, invoice.ids + payment_move.ids)
+ env0.cr.commit()
+ lines2reconcile = (
+ (payment_move | invoice)
+ .mapped("line_ids")
+ .filtered(
+ lambda line: line.account_id.account_type == "asset_receivable"
+ )
+ )
+ with env0.cr.savepoint(), env1.cr.savepoint():
+ # Reconciling "last move"
+ # reconcile a payment with many invoices spend a lot so it could
+ # lock records too many time
+ lines2reconcile.reconcile()
+ # Many pieces of code call flush directly
+ env0.flush_all()
+ self._create_payment_form(env1)
+
+ def test_sequence_concurrency_90_payments(self):
+ """Creating concurrent payments should not raises errors"""
+ with self._new_cr() as cr0, self._new_cr() as cr1, self._new_cr() as cr2:
+ env0 = api.Environment(cr0, SUPERUSER_ID, {})
+ env1 = api.Environment(cr1, SUPERUSER_ID, {})
+ env2 = api.Environment(cr2, SUPERUSER_ID, {})
+ for cr in [cr0, cr1, cr2]:
+ # Set 10s timeout in order to avoid waiting for release locks a long time
+ cr.execute("SET LOCAL statement_timeout = '10s'")
+
+ # Create "last move" to lock
+ payment = self._create_payment_form(env0, ir_sequence_standard=True)
+ payment_move_ids = payment.move_id.ids
+ self.addCleanup(self._clean_moves, payment_move_ids)
+ env0.cr.commit()
+ with env1.cr.savepoint(), env2.cr.savepoint():
+ self._create_payment_form(env1)
+ self._create_payment_form(env2)
+
+ @tools.mute_logger("odoo.sql_db")
+ def test_sequence_concurrency_95_pay2inv_inv2pay(self):
+ """Creating concurrent payment then invoice and invoice then payment
+ should not raises errors
+ It raises deadlock sometimes"""
+ with self._new_cr() as cr0:
+ env0 = api.Environment(cr0, SUPERUSER_ID, {})
+
+ # Create "last move" to lock
+ invoice = self._create_invoice_form(env0)
+
+ # Create "last move" to lock
+ payment = self._create_payment_form(env0)
+ payment_move_ids = payment.move_id.ids
+ self.addCleanup(self._clean_moves, invoice.ids + payment_move_ids)
+ env0.cr.commit()
+ env0.cr.execute(
+ "SELECT setting FROM pg_settings WHERE name = 'deadlock_timeout'"
+ )
+ deadlock_timeout = int(env0.cr.fetchone()[0]) # ms
+ # You could not have permission to set this parameter
+ # psycopg2.errors.InsufficientPrivilege
+ self.assertTrue(
+ deadlock_timeout,
+ "You need to configure PG parameter deadlock_timeout='1s'",
+ )
+ deadlock_timeout = int(deadlock_timeout / 1000) # s
+
+ try:
+ t_pay_inv = ThreadRaiseJoin(
+ target=self._create_invoice_payment,
+ args=(deadlock_timeout, True, True),
+ name="Thread payment invoice",
+ )
+ t_inv_pay = ThreadRaiseJoin(
+ target=self._create_invoice_payment,
+ args=(deadlock_timeout, False, True),
+ name="Thread invoice payment",
+ )
+ t_pay_inv.start()
+ t_inv_pay.start()
+ # the thread could raise the error before to wait for it so disable coverage
+ t_pay_inv.join(timeout=deadlock_timeout + 15) # pragma: no cover
+ t_inv_pay.join(timeout=deadlock_timeout + 15) # pragma: no cover
+ except psycopg2.OperationalError as e:
+ if e.pgcode in [
+ psycopg2.errorcodes.SERIALIZATION_FAILURE,
+ psycopg2.errorcodes.LOCK_NOT_AVAILABLE,
+ ]: # pragma: no cover
+ # Concurrency error is expected but not deadlock so ok
+ pass
+ elif (
+ e.pgcode == psycopg2.errorcodes.DEADLOCK_DETECTED
+ ): # pragma: no cover
+ self.assertFalse(True, "Deadlock detected.")
+ else: # pragma: no cover
+ raise