Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
3
account_move_name_sequence/tests/__init__.py
Executable file
3
account_move_name_sequence/tests/__init__.py
Executable file
@@ -0,0 +1,3 @@
|
||||
from . import test_account_move_name_seq
|
||||
from . import test_sequence_concurrency
|
||||
from . import test_account_incoming_supplier_invoice
|
||||
96
account_move_name_sequence/tests/test_account_incoming_supplier_invoice.py
Executable file
96
account_move_name_sequence/tests/test_account_incoming_supplier_invoice.py
Executable file
@@ -0,0 +1,96 @@
|
||||
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):
|
||||
super().setUpClass()
|
||||
|
||||
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}@"
|
||||
f"{self.journal.alias_id.alias_domain}",
|
||||
"body": "You know, that thing that you bought.",
|
||||
"attachments": [b"Hello, invoice"],
|
||||
}
|
||||
|
||||
invoice = (
|
||||
self.env["account.move"]
|
||||
.with_context(
|
||||
tracking_disable=False,
|
||||
mail_create_nolog=False,
|
||||
mail_create_nosubscribe=False,
|
||||
mail_notrack=False,
|
||||
)
|
||||
.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,
|
||||
"<p>Vendor Bill Created</p>",
|
||||
"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, "/")
|
||||
388
account_move_name_sequence/tests/test_account_move_name_seq.py
Executable file
388
account_move_name_sequence/tests/test_account_move_name_seq.py
Executable file
@@ -0,0 +1,388 @@
|
||||
# Copyright 2021 Akretion France (http://www.akretion.com/)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# @author: Moisés López <moylop260@vauxoo.com>
|
||||
# @author: Francisco Luna <fluna@vauxoo.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import Command, fields
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tests import Form, TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestAccountMoveNameSequence(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company = cls.env.ref("base.main_company")
|
||||
cls.partner = cls.env.ref("base.res_partner_3")
|
||||
cls.misc_journal = cls.env["account.journal"].create(
|
||||
{
|
||||
"name": "Test Journal Move name seq",
|
||||
"code": "ADLM",
|
||||
"type": "general",
|
||||
"company_id": cls.company.id,
|
||||
}
|
||||
)
|
||||
cls.sales_seq = cls.env["ir.sequence"].create(
|
||||
{
|
||||
"name": "TB2C",
|
||||
"implementation": "no_gap",
|
||||
"prefix": "TB2CSEQ/%(range_year)s/",
|
||||
"use_date_range": True,
|
||||
"number_increment": 1,
|
||||
"padding": 4,
|
||||
"company_id": cls.company.id,
|
||||
}
|
||||
)
|
||||
cls.sales_journal = cls.env["account.journal"].create(
|
||||
{
|
||||
"name": "TB2C",
|
||||
"code": "TB2C",
|
||||
"type": "sale",
|
||||
"company_id": cls.company.id,
|
||||
"refund_sequence": True,
|
||||
"sequence_id": cls.sales_seq.id,
|
||||
}
|
||||
)
|
||||
cls.purchase_journal = cls.env["account.journal"].create(
|
||||
{
|
||||
"name": "Test Purchase Journal Move name seq",
|
||||
"code": "ADLP",
|
||||
"type": "purchase",
|
||||
"company_id": cls.company.id,
|
||||
"refund_sequence": True,
|
||||
}
|
||||
)
|
||||
cls.accounts = cls.env["account.account"].search(
|
||||
[("company_ids", "=", cls.company.id)], limit=2
|
||||
)
|
||||
cls.account1 = cls.accounts[0]
|
||||
cls.account2 = cls.accounts[1]
|
||||
cls.date = datetime.now()
|
||||
cls.purchase_journal2 = cls.purchase_journal.copy()
|
||||
|
||||
cls.journals = (
|
||||
cls.misc_journal
|
||||
| cls.purchase_journal
|
||||
| cls.sales_journal
|
||||
| cls.purchase_journal2
|
||||
)
|
||||
# This patch was added to avoid test failures in the CI pipeline caused by the
|
||||
# `account_journal_restrict_mode` module. It prevents a validation error when
|
||||
# disabling restrict mode on journals used in the test, allowing moves to be
|
||||
# set to draft and deleted.
|
||||
with patch("odoo.models.BaseModel._validate_fields"):
|
||||
cls.journals.restrict_mode_hash_table = False
|
||||
|
||||
cls.lines = [
|
||||
Command.create({"account_id": cls.account1.id, "debit": 10}),
|
||||
Command.create({"account_id": cls.account2.id, "credit": 10}),
|
||||
]
|
||||
cls.invoice_line = [
|
||||
Command.create(
|
||||
{
|
||||
"account_id": cls.account1.id,
|
||||
"price_unit": 42.0,
|
||||
"quantity": 12,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
def test_seq_creation(self):
|
||||
self.assertTrue(self.misc_journal.sequence_id)
|
||||
seq = self.misc_journal.sequence_id
|
||||
self.assertEqual(seq.company_id, self.company)
|
||||
self.assertEqual(seq.implementation, "no_gap")
|
||||
self.assertEqual(seq.padding, 4)
|
||||
self.assertTrue(seq.use_date_range)
|
||||
self.assertTrue(self.purchase_journal.sequence_id)
|
||||
self.assertTrue(self.purchase_journal.refund_sequence_id)
|
||||
seq = self.purchase_journal.refund_sequence_id
|
||||
self.assertEqual(seq.company_id, self.company)
|
||||
self.assertEqual(seq.implementation, "no_gap")
|
||||
self.assertEqual(seq.padding, 4)
|
||||
self.assertTrue(seq.use_date_range)
|
||||
|
||||
def test_misc_move_name(self):
|
||||
move = self.env["account.move"].create(
|
||||
{
|
||||
"date": self.date,
|
||||
"journal_id": self.misc_journal.id,
|
||||
"line_ids": self.lines,
|
||||
}
|
||||
)
|
||||
self.assertEqual(move.name, "/")
|
||||
move.action_post()
|
||||
seq = self.misc_journal.sequence_id
|
||||
move_name = "{}{}".format(seq.prefix, "1".zfill(seq.padding))
|
||||
move_name = move_name.replace("%(range_year)s", str(self.date.year))
|
||||
self.assertEqual(move.name, move_name)
|
||||
self.assertTrue(seq.date_range_ids)
|
||||
drange_count = self.env["ir.sequence.date_range"].search_count(
|
||||
[
|
||||
("sequence_id", "=", seq.id),
|
||||
("date_from", "=", fields.Date.add(self.date, month=1, day=1)),
|
||||
]
|
||||
)
|
||||
self.assertEqual(drange_count, 1)
|
||||
move.button_draft()
|
||||
move.action_post()
|
||||
self.assertEqual(move.name, move_name)
|
||||
|
||||
def test_prefix_move_name_use_move_date(self):
|
||||
seq = self.misc_journal.sequence_id
|
||||
seq.prefix = "TEST-%(year)s-%(month)s-"
|
||||
self.env["ir.sequence.date_range"].sudo().create(
|
||||
{
|
||||
"date_from": "2021-07-01",
|
||||
"date_to": "2022-06-30",
|
||||
"sequence_id": seq.id,
|
||||
}
|
||||
)
|
||||
with freeze_time("2022-01-01"):
|
||||
move = self.env["account.move"].create(
|
||||
{
|
||||
"date": "2021-12-31",
|
||||
"journal_id": self.misc_journal.id,
|
||||
"line_ids": self.lines,
|
||||
}
|
||||
)
|
||||
move.action_post()
|
||||
self.assertEqual(move.name, "TEST-2021-12-0001")
|
||||
with freeze_time("2022-01-01"):
|
||||
move = self.env["account.move"].create(
|
||||
{
|
||||
"date": "2022-06-30",
|
||||
"journal_id": self.misc_journal.id,
|
||||
"line_ids": self.lines,
|
||||
}
|
||||
)
|
||||
move.action_post()
|
||||
self.assertEqual(move.name, "TEST-2022-06-0002")
|
||||
|
||||
with freeze_time("2022-01-01"):
|
||||
move = self.env["account.move"].create(
|
||||
{
|
||||
"date": "2022-07-01",
|
||||
"journal_id": self.misc_journal.id,
|
||||
"line_ids": self.lines,
|
||||
}
|
||||
)
|
||||
move.action_post()
|
||||
self.assertEqual(move.name, "TEST-2022-07-0001")
|
||||
|
||||
def test_prefix_move_name_use_move_date_2(self):
|
||||
seq = self.misc_journal.sequence_id
|
||||
seq.prefix = "TEST-%(range_month)s-"
|
||||
with freeze_time("2022-01-01"):
|
||||
move = self.env["account.move"].create(
|
||||
{
|
||||
"date": "2022-06-30",
|
||||
"journal_id": self.misc_journal.id,
|
||||
"line_ids": self.lines,
|
||||
}
|
||||
)
|
||||
move.action_post()
|
||||
self.assertEqual(move.name, "TEST-06-0001")
|
||||
|
||||
def test_prefix_move_name_use_move_date_3(self):
|
||||
seq = self.misc_journal.sequence_id
|
||||
seq.prefix = "TEST-%(range_day)s-"
|
||||
with freeze_time("2022-01-01"):
|
||||
move = self.env["account.move"].create(
|
||||
{
|
||||
"date": "2022-01-01",
|
||||
"journal_id": self.misc_journal.id,
|
||||
"line_ids": self.lines,
|
||||
}
|
||||
)
|
||||
move.action_post()
|
||||
self.assertEqual(move.name, "TEST-01-0001")
|
||||
|
||||
def test_in_invoice_and_refund(self):
|
||||
in_invoice = self.env["account.move"].create(
|
||||
{
|
||||
"journal_id": self.purchase_journal.id,
|
||||
"invoice_date": self.date,
|
||||
"partner_id": self.env.ref("base.res_partner_3").id,
|
||||
"move_type": "in_invoice",
|
||||
"invoice_line_ids": self.invoice_line
|
||||
+ [
|
||||
Command.create(
|
||||
{
|
||||
"account_id": self.account1.id,
|
||||
"price_unit": 48.0,
|
||||
"quantity": 10,
|
||||
}
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertEqual(in_invoice.name, "/")
|
||||
in_invoice.action_post()
|
||||
|
||||
in_invoice = in_invoice.copy(
|
||||
{
|
||||
"invoice_date": self.date,
|
||||
}
|
||||
)
|
||||
in_invoice.action_post()
|
||||
|
||||
move_reversal = self.env["account.move.reversal"].create(
|
||||
{
|
||||
"move_ids": in_invoice.ids,
|
||||
"journal_id": in_invoice.journal_id.id,
|
||||
"reason": "no reason",
|
||||
}
|
||||
)
|
||||
reversal = move_reversal.modify_moves()
|
||||
draft_invoice = self.env["account.move"].browse(reversal["res_id"])
|
||||
self.assertTrue(draft_invoice)
|
||||
self.assertEqual(draft_invoice.state, "draft")
|
||||
self.assertEqual(draft_invoice.move_type, "in_invoice")
|
||||
|
||||
in_invoice = in_invoice.copy(
|
||||
{
|
||||
"invoice_date": self.date,
|
||||
}
|
||||
)
|
||||
in_invoice.action_post()
|
||||
|
||||
move_reversal = self.env["account.move.reversal"].create(
|
||||
{
|
||||
"move_ids": in_invoice.ids,
|
||||
"journal_id": in_invoice.journal_id.id,
|
||||
"reason": "no reason",
|
||||
}
|
||||
)
|
||||
reversal = move_reversal.refund_moves()
|
||||
draft_reversed_move = self.env["account.move"].browse(reversal["res_id"])
|
||||
self.assertTrue(draft_reversed_move)
|
||||
self.assertEqual(draft_reversed_move.state, "draft")
|
||||
self.assertEqual(draft_reversed_move.move_type, "in_refund")
|
||||
|
||||
def test_in_refund(self):
|
||||
in_refund_invoice = self.env["account.move"].create(
|
||||
{
|
||||
"journal_id": self.purchase_journal.id,
|
||||
"invoice_date": self.date,
|
||||
"partner_id": self.env.ref("base.res_partner_3").id,
|
||||
"move_type": "in_refund",
|
||||
"invoice_line_ids": self.invoice_line,
|
||||
}
|
||||
)
|
||||
self.assertEqual(in_refund_invoice.name, "/")
|
||||
in_refund_invoice.action_post()
|
||||
seq = self.purchase_journal.refund_sequence_id
|
||||
move_name = "{}{}".format(seq.prefix, "1".zfill(seq.padding))
|
||||
move_name = move_name.replace("%(range_year)s", str(self.date.year))
|
||||
self.assertEqual(in_refund_invoice.name, move_name)
|
||||
in_refund_invoice.button_draft()
|
||||
in_refund_invoice.action_post()
|
||||
self.assertEqual(in_refund_invoice.name, move_name)
|
||||
|
||||
def test_remove_invoice_error_secuence_no_grap(self):
|
||||
invoice = self.env["account.move"].create(
|
||||
{
|
||||
"date": self.date,
|
||||
"journal_id": self.misc_journal.id,
|
||||
"line_ids": self.lines,
|
||||
}
|
||||
)
|
||||
self.assertEqual(invoice.name, "/")
|
||||
invoice.action_post()
|
||||
error_msg = (
|
||||
"You can't delete a posted journal item. "
|
||||
"Don’t play games with your accounting records; "
|
||||
"reset the journal entry to draft before deleting it."
|
||||
)
|
||||
with self.assertRaisesRegex(UserError, error_msg):
|
||||
invoice.unlink()
|
||||
invoice.button_draft()
|
||||
invoice.button_cancel()
|
||||
invoice.unlink()
|
||||
|
||||
def test_remove_invoice_error_secuence_standard(self):
|
||||
implementation = {"implementation": "standard"}
|
||||
self.purchase_journal.sequence_id.write(implementation)
|
||||
self.purchase_journal.refund_sequence_id.write(implementation)
|
||||
in_refund_invoice = self.env["account.move"].create(
|
||||
{
|
||||
"journal_id": self.purchase_journal.id,
|
||||
"invoice_date": self.date,
|
||||
"partner_id": self.env.ref("base.res_partner_3").id,
|
||||
"move_type": "in_refund",
|
||||
"invoice_line_ids": self.invoice_line,
|
||||
}
|
||||
)
|
||||
self.assertEqual(in_refund_invoice.name, "/")
|
||||
in_refund_invoice.action_post()
|
||||
error_msg = (
|
||||
"You can't delete a posted journal item. "
|
||||
"Don’t play games with your accounting records; "
|
||||
"reset the journal entry to draft before deleting it."
|
||||
)
|
||||
with self.assertRaisesRegex(UserError, error_msg):
|
||||
in_refund_invoice.unlink()
|
||||
in_refund_invoice.button_draft()
|
||||
in_refund_invoice.button_cancel()
|
||||
self.assertTrue(in_refund_invoice.unlink())
|
||||
|
||||
def test_journal_check_journal_sequence(self):
|
||||
new_journal = self.purchase_journal2
|
||||
# same sequence_id and refund_sequence_id
|
||||
with self.assertRaises(ValidationError):
|
||||
new_journal.write({"refund_sequence_id": new_journal.sequence_id})
|
||||
|
||||
# company_id in sequence_id or refund_sequence_id to False
|
||||
new_sequence_id = new_journal.sequence_id.copy({"company_id": False})
|
||||
new_refund_sequence_id = new_journal.refund_sequence_id.copy(
|
||||
{"company_id": False}
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
new_journal.write({"sequence_id": new_sequence_id.id})
|
||||
with self.assertRaises(ValidationError):
|
||||
new_journal.write({"refund_sequence_id": new_refund_sequence_id.id})
|
||||
|
||||
def test_constrains_date_sequence_true(self):
|
||||
self.assertTrue(self.env["account.move"]._constrains_date_sequence())
|
||||
|
||||
def test_prefix_move_name_journal_onchange(self):
|
||||
product = self.env["product.product"].create({"name": "Product"})
|
||||
with Form(
|
||||
self.env["account.move"].with_context(default_move_type="out_invoice")
|
||||
) as invoice_form:
|
||||
invoice_form.invoice_date = fields.Date.today()
|
||||
invoice_form.partner_id = self.partner
|
||||
with invoice_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = product
|
||||
invoice = invoice_form.save()
|
||||
self.assertEqual(invoice.name, "/")
|
||||
invoice.journal_id = self.sales_journal
|
||||
self.assertEqual(invoice.name, "/", "name based on journal instead of sequence")
|
||||
invoice.action_post()
|
||||
self.assertIn("TB2CSEQ/", invoice.name, "name was not based on sequence")
|
||||
|
||||
def test_is_end_of_seq_chain(self):
|
||||
self.env.user.groups_id -= self.env.ref("account.group_account_manager")
|
||||
invoice = self.env["account.move"].create(
|
||||
{
|
||||
"date": self.date,
|
||||
"journal_id": self.misc_journal.id,
|
||||
"line_ids": self.lines,
|
||||
}
|
||||
)
|
||||
invoice.action_post()
|
||||
error_msg = (
|
||||
"You cannot delete this entry, as it has already consumed "
|
||||
"a sequence number and is not the last one in the chain. "
|
||||
"You should probably revert it instead."
|
||||
)
|
||||
with self.assertRaisesRegex(UserError, error_msg):
|
||||
invoice._unlink_forbid_parts_of_chain()
|
||||
334
account_move_name_sequence/tests/test_sequence_concurrency.py
Executable file
334
account_move_name_sequence/tests/test_sequence_concurrency.py
Executable file
@@ -0,0 +1,334 @@
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import psycopg2
|
||||
|
||||
from odoo import SUPERUSER_ID, api, fields, tools
|
||||
from odoo.tests import Form, TransactionCase, tagged
|
||||
|
||||
_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):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.product = cls.env.ref("product.product_delivery_01")
|
||||
cls.partner = cls.env.ref("base.res_partner_12")
|
||||
cls.partner2 = cls.env.ref("base.res_partner_1")
|
||||
cls.date = fields.Date.to_date("1985-04-14")
|
||||
cls.journal_sale_std = cls.env.ref(
|
||||
"account_move_name_sequence.journal_sale_std_demo"
|
||||
)
|
||||
cls.journal_cash_std = cls.env.ref(
|
||||
"account_move_name_sequence.journal_cash_std_demo"
|
||||
)
|
||||
|
||||
cls.cr0 = cls.cursor(cls)
|
||||
cls.env0 = api.Environment(cls.cr0, SUPERUSER_ID, {})
|
||||
cls.cr1 = cls.cursor(cls)
|
||||
cls.env1 = api.Environment(cls.cr1, SUPERUSER_ID, {})
|
||||
cls.cr2 = cls.cursor(cls)
|
||||
cls.env2 = api.Environment(cls.cr2, SUPERUSER_ID, {})
|
||||
for cr in [cls.cr0, cls.cr1, cls.cr2]:
|
||||
# Set a 10-second timeout to avoid waiting too long for release locks
|
||||
cr.execute("SET LOCAL statement_timeout = '10s'")
|
||||
cls.registry.enter_test_mode(cls.cursor(cls))
|
||||
cls.addClassCleanup(cls.registry.leave_test_mode)
|
||||
cls.last_existing_move = cls.env["account.move"].search(
|
||||
[], limit=1, order="id desc"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _clean_moves_and_payments(cls, last_move):
|
||||
"""Delete moves and payments created after finish test."""
|
||||
moves = (
|
||||
cls.env["account.move"]
|
||||
.with_context(force_delete=True)
|
||||
.search([("id", ">=", last_move)])
|
||||
)
|
||||
payments = moves.payment_ids
|
||||
moves_without_payments = moves - payments.move_id
|
||||
if payments:
|
||||
payments.action_draft()
|
||||
payments.unlink()
|
||||
if moves_without_payments:
|
||||
moves_without_payments.filtered(
|
||||
lambda move: move.state != "draft"
|
||||
).button_draft()
|
||||
moves_without_payments.unlink()
|
||||
|
||||
def _commit_crs(self, *envs):
|
||||
for env in envs:
|
||||
env.cr.commit()
|
||||
|
||||
def _create_invoice_form(
|
||||
self, env, post=True, partner=None, ir_sequence_standard=False
|
||||
):
|
||||
ctx = {"default_move_type": "out_invoice"}
|
||||
with Form(env["account.move"].with_context(**ctx)) as invoice_form:
|
||||
# Use another partner to bypass "increase_rank" lock error
|
||||
invoice_form.partner_id = partner or 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 ir_sequence_standard:
|
||||
invoice.journal_id = self.journal_sale_std
|
||||
if post:
|
||||
# This patch was added to avoid test failures in the CI pipeline caused by
|
||||
# the `account_journal_restrict_mode` module. It avoids errors when setting
|
||||
# posted moves to draft and deleting them by bypassing the method that
|
||||
# writes the hash field used for validation.
|
||||
with patch(
|
||||
"odoo.addons.account.models.account_move.AccountMove._hash_moves"
|
||||
):
|
||||
invoice.action_post()
|
||||
return invoice
|
||||
|
||||
def _create_payment_form(self, env, partner=None, 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 = partner or self.partner
|
||||
payment_form.amount = 100
|
||||
payment_form.date = self.date
|
||||
if ir_sequence_standard:
|
||||
payment_form.journal_id = self.journal_cash_std
|
||||
payment = payment_form.save()
|
||||
# This patch was added to avoid test failures in the CI pipeline caused by
|
||||
# the `account_journal_restrict_mode` module. It avoids errors when setting
|
||||
# posted moves to draft and deleting them by bypassing the method that
|
||||
# writes the hash field used for validation.
|
||||
with patch("odoo.addons.account.models.account_move.AccountMove._hash_moves"):
|
||||
payment.action_post()
|
||||
return payment
|
||||
|
||||
def _create_invoice_payment(
|
||||
self, deadlock_timeout, payment_first=False, ir_sequence_standard=False
|
||||
):
|
||||
with self.cursor() 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, ir_sequence_standard=ir_sequence_standard
|
||||
)
|
||||
else:
|
||||
_logger.info("Creating invoice cr %s", cr_pid)
|
||||
self._create_invoice_form(
|
||||
env, ir_sequence_standard=ir_sequence_standard
|
||||
)
|
||||
_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"""
|
||||
# Create "last move" to lock
|
||||
self._create_invoice_form(self.env0)
|
||||
self.cr0.commit()
|
||||
with self.cr1.savepoint(), self.cr2.savepoint():
|
||||
invoice1 = self._create_invoice_form(self.env1, post=False)
|
||||
self.assertEqual(invoice1.state, "draft")
|
||||
invoice2 = self._create_invoice_form(self.env2, post=False)
|
||||
self.assertEqual(invoice2.state, "draft")
|
||||
self._commit_crs(self.env0, self.env1, self.env2)
|
||||
|
||||
def test_sequence_concurrency_20_editing_last_invoice(self):
|
||||
"""Edit last invoice and create a new invoice
|
||||
should not raises errors"""
|
||||
# Create "last move" to lock
|
||||
invoice = self._create_invoice_form(self.env0)
|
||||
self.cr0.commit()
|
||||
with self.cr0.savepoint(), self.cr1.savepoint():
|
||||
# Edit something in "last move"
|
||||
invoice.write({"write_uid": self.env0.uid})
|
||||
self.env0.flush_all()
|
||||
self._create_invoice_form(self.env1)
|
||||
self._commit_crs(self.env0, self.env1)
|
||||
|
||||
def test_sequence_concurrency_30_editing_last_payment(self):
|
||||
"""Edit last payment and create a new payment
|
||||
should not raises errors"""
|
||||
# Create "last move" to lock
|
||||
payment = self._create_payment_form(self.env0)
|
||||
payment_move = payment.move_id
|
||||
self.cr0.commit()
|
||||
with self.cr0.savepoint(), self.cr1.savepoint():
|
||||
# Edit something in "last move"
|
||||
payment_move.write({"write_uid": self.env0.uid})
|
||||
self.env0.flush_all()
|
||||
self._create_payment_form(self.env1)
|
||||
self._commit_crs(self.env0, self.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"""
|
||||
# Create "last move" to lock
|
||||
invoice = self._create_invoice_form(self.env0)
|
||||
payment = self._create_payment_form(self.env0)
|
||||
payment_move = payment.move_id
|
||||
self.cr0.commit()
|
||||
lines2reconcile = (
|
||||
(payment_move | invoice)
|
||||
.mapped("line_ids")
|
||||
.filtered(lambda line: line.account_id.account_type == "asset_receivable")
|
||||
)
|
||||
with self.cr0.savepoint(), self.cr1.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
|
||||
self.env0.flush_all()
|
||||
self._create_invoice_form(self.env1)
|
||||
self._commit_crs(self.env0, self.env1)
|
||||
|
||||
def test_sequence_concurrency_50_reconciling_last_payment(self):
|
||||
"""Reconcile last payment and create a new one
|
||||
should not raises errors"""
|
||||
# Create "last move" to lock
|
||||
invoice = self._create_invoice_form(self.env0)
|
||||
payment = self._create_payment_form(self.env0)
|
||||
payment_move = payment.move_id
|
||||
self.cr0.commit()
|
||||
lines2reconcile = (
|
||||
(payment_move | invoice)
|
||||
.mapped("line_ids")
|
||||
.filtered(lambda line: line.account_id.account_type == "asset_receivable")
|
||||
)
|
||||
with self.cr0.savepoint(), self.cr1.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
|
||||
self.env0.flush_all()
|
||||
self._create_payment_form(self.env1)
|
||||
self._commit_crs(self.env0, self.env1)
|
||||
|
||||
def test_sequence_concurrency_90_payments(self):
|
||||
"""Creating concurrent payments should not raises errors"""
|
||||
# Create "last move" to lock
|
||||
self._create_payment_form(self.env0, ir_sequence_standard=True)
|
||||
self.cr0.commit()
|
||||
with self.cr1.savepoint(), self.cr2.savepoint():
|
||||
self._create_payment_form(self.env1, ir_sequence_standard=True)
|
||||
self._create_payment_form(self.env2, ir_sequence_standard=True)
|
||||
self._commit_crs(self.env0, self.env1, self.env2)
|
||||
|
||||
def test_sequence_concurrency_92_invoices(self):
|
||||
"""Creating concurrent invoices should not raises errors"""
|
||||
# Create "last move" to lock
|
||||
self._create_invoice_form(self.env0, ir_sequence_standard=True)
|
||||
self.cr0.commit()
|
||||
with self.cr1.savepoint(), self.cr2.savepoint():
|
||||
self._create_invoice_form(self.env1, ir_sequence_standard=True)
|
||||
# Using another partner to bypass "increase_rank" lock error
|
||||
self._create_invoice_form(
|
||||
self.env2, partner=self.partner2, ir_sequence_standard=True
|
||||
)
|
||||
self._commit_crs(self.env0, self.env1, self.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"""
|
||||
# Create "last move" to lock
|
||||
self._create_invoice_form(self.env0)
|
||||
# Create "last move" to lock
|
||||
self._create_payment_form(self.env0)
|
||||
self.cr0.commit()
|
||||
self.cr0.execute(
|
||||
"SELECT setting FROM pg_settings WHERE name = 'deadlock_timeout'"
|
||||
)
|
||||
deadlock_timeout = int(self.cr0.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
|
||||
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
|
||||
self._thread_join(t_pay_inv, deadlock_timeout + 15)
|
||||
self._thread_join(t_inv_pay, deadlock_timeout + 15)
|
||||
|
||||
def _thread_join(self, thread_obj, timeout):
|
||||
try:
|
||||
thread_obj.join(timeout=timeout) # pragma: no cover
|
||||
self.assertFalse(
|
||||
thread_obj.is_alive(),
|
||||
"The thread wait is over. but the cursor may still be in use!",
|
||||
)
|
||||
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
|
||||
Reference in New Issue
Block a user