Skip to content
2 changes: 1 addition & 1 deletion sale_order_rename/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

{
"name": "Sale Order Rename",
"version": "12.0.1.0.2",
"version": "16.0.1.0.0",
"category": "Sales",
"website": "https://github.com/OCA/sale-workflow",
"author": "CorporateHub, " "Odoo Community Association (OCA)",
Expand Down
21 changes: 9 additions & 12 deletions sale_order_rename/i18n/sale_order_rename.pot
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_order_rename
# * sale_order_rename
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.0\n"
"Project-Id-Version: Odoo Server 16.0+e-20250113\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: <>\n"
"POT-Creation-Date: 2025-01-30 11:19+0000\n"
"PO-Revision-Date: 2025-01-30 11:19+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: sale_order_rename
#: code:addons/sale_order_rename/models/sale_order.py:26
#. odoo-python
#: code:addons/sale_order_rename/models/sale_order.py:0
#, python-format
msgid "New"
msgid "Sale Order name must be unique within a company!"
msgstr ""

#. module: sale_order_rename
#: model:ir.model,name:sale_order_rename.model_sale_order
msgid "Sale Order"
msgstr ""

#. module: sale_order_rename
#: sql_constraint:sale.order:0
msgid "Sale Order name must be unique within a company!"
msgid "Sales Order"
msgstr ""

48 changes: 22 additions & 26 deletions sale_order_rename/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
# Copyright 2018 Brainbean Apps (https://brainbeanapps.com)
# Copyright 2025 Openforce Srls Unipersonale (www.openforce.it)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging
from psycopg2 import IntegrityError

from odoo import models, api, _, tools
from odoo import _, api, models
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For @api.constrains methods, the Odoo convention is to raise ValidationError instead of UserError.

from odoo.exceptions import ValidationError

And correspondingly change the raise UserError(...) to raise ValidationError(...) on line 32.

from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)


class SaleOrder(models.Model):
_inherit = 'sale.order'
_inherit = "sale.order"

_name_company_uniq_constraint = 'name_company_uniq'
_sql_constraints = [
(
_name_company_uniq_constraint,
'unique(name, company_id)',
'Sale Order name must be unique within a company!'
),
]

@api.model
def create(self, vals):
is_name_generated = vals.get('name', _('New')) != _('New')
duplicate_key_msg = 'duplicate key value violates unique constraint'
while True:
try:
with self._cr.savepoint(), tools.mute_logger('odoo.sql_db'):
return super().create(vals.copy())
except IntegrityError as e:
e_msg = str(e)
if is_name_generated or duplicate_key_msg not in e_msg or \
self._name_company_uniq_constraint not in e_msg:
raise e
_logger.debug('Duplicate sale.order name, retrying creation')
@api.constrains("name")
def _check_unique_name_in_company(self):
so_obj = self.env["sale.order"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop executes one search_count per record, which is an N+1 query pattern. For batch operations (e.g., importing multiple sale orders), this becomes costly.

Consider using read_group or a single SQL query to detect duplicates across all records in self at once, as suggested by @rousseldenis in an earlier comment.

for so in self:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patrickt-oforce Have you tried something like this ?

result = self._read_group(
            domain=[('name', 'in', self.mapped('name')],
            fields=['company_id', 'ids:array_agg(id)'],
            groupby=['company_id'],
        )
for res in result:
    if res.get('company_id_count', 0) >= 2:
        duplicate_orders = self.browse(res['ids'])
        raise (...)

In order to not do as much queries as recordset amount.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @rousseldenis, sorry i've seen the comment only now; i try and write here if ok

domain = [
("name", "=", so.name),
("company_id", "=", so.company_id.id),
("id", "!=", so.id),
]
if so_obj.search_count(domain):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_logger.error() is too noisy here -- this is a normal validation failure, not an unexpected system error. Odoo will log the exception anyway. Consider _logger.debug() or remove the logging entirely.

_logger.error(
"Sale Order name %(so_name)s exists for company:" " %(company)s",
{
"so_name": so.name,
"company": so.company_id.name,
},
)
raise UserError(_("Sale Order name must be unique within a company!"))
165 changes: 119 additions & 46 deletions sale_order_rename/tests/test_sale_order_rename.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,129 @@
# Copyright 2018 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

from psycopg2 import IntegrityError
from unittest import mock

from odoo.exceptions import UserError
from odoo.tests import common
from odoo.tools.misc import mute_logger

_ir_sequence_class = 'odoo.addons.base.models.ir_sequence.IrSequence'


class TestSaleOrderRename(common.TransactionCase):

def setUp(self):
super().setUp()

self.SaleOrder = self.env['sale.order']
self.SudoSaleOrder = self.SaleOrder.sudo()

def test_1(self):
self.SudoSaleOrder.create({
'name': 'Test #1',
'partner_id': self.env.ref('base.res_partner_1').id,
})

with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'):
self.SudoSaleOrder.create({
'name': 'Test #1',
'partner_id': self.env.ref('base.res_partner_1').id,
})

def test_2(self):
sale_order_1 = self.SudoSaleOrder.create({
'partner_id': self.env.ref('base.res_partner_1').id,
})
sale_order_2 = self.SudoSaleOrder.create({
'partner_id': self.env.ref('base.res_partner_1').id,
})

self.assertNotEqual(sale_order_1.name, sale_order_2.name)

def test_3(self):
sale_order_1 = self.SudoSaleOrder.create({
'name': 'Test #3-1',
'partner_id': self.env.ref('base.res_partner_1').id,
})

with mock.patch(
_ir_sequence_class + '.next_by_code',
side_effect=['Test #3-1', 'Test #3-2']):
sale_order_2 = self.SudoSaleOrder.create({
'partner_id': self.env.ref('base.res_partner_1').id,
})

self.assertNotEqual(sale_order_1.name, sale_order_2.name)
self.sale_order = self.env["sale.order"]
self.sale_order_sudo = self.sale_order.sudo()
self.base_company = self.env.ref("base.main_company")
self.other_company = self._create_company()

def _create_company(self):
return self.env["res.company"].create(
{
"name": "Test Company",
}
)

def test_01_two_sale_order_with_same_name_in_different_companies(self):
so_vals = {
"name": "Test #1",
"partner_id": self.env.ref("base.res_partner_1").id,
}
self.sale_order.create(
[
{
**so_vals,
"company_id": self.base_company.id,
}
]
)
self.sale_order.create(
[
{
**so_vals,
"company_id": self.other_company.id,
}
]
)

def test_02_raise_exception_two_sale_order_with_same_name_in_same_company(self):
so_vals = {
"name": "Test #1",
"partner_id": self.env.ref("base.res_partner_1").id,
}
self.sale_order.create(
[
{
**so_vals,
"company_id": self.base_company.id,
}
]
)
with self.assertRaises(UserError):
self.sale_order.create(
[
{
**so_vals,
"company_id": self.base_company.id,
}
]
)

def test_03_raise_exception_renaming_two_so_in_same_company_with_same_name(self):
so_vals = {
"company_id": self.base_company.id,
"partner_id": self.env.ref("base.res_partner_1").id,
}
so1 = self.sale_order.create(
[
{
**so_vals,
}
]
)
so2 = self.sale_order.create(
[
{
**so_vals,
}
]
)
so1.write(
{
"name": "Test #1",
}
)
with self.assertRaises(UserError):
so2.write(
{
"name": "Test #1",
}
)

def test_04_allowed_renaming_two_so_in_different_company_with_same_name(self):
so_vals = {
"partner_id": self.env.ref("base.res_partner_1").id,
}
so1 = self.sale_order.create(
[
{
**so_vals,
"company_id": self.base_company.id,
}
]
)
so2 = self.sale_order.create(
[
{
**so_vals,
"company_id": self.other_company.id,
}
]
)
so1.write(
{
"name": "Test #1",
}
)
so2.write(
{
"name": "Test #1",
}
)
9 changes: 4 additions & 5 deletions sale_order_rename/views/sale_order.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
<record id="sale_order_form_view" model="ir.ui.view">
<field name="name">sale.order.form</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="inherit_id" ref="sale.view_order_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="readonly">0</attribute>
<attribute name="attrs">{'readonly': [('state', 'not in', ['draft'])]}</attribute>
</xpath>
<xpath expr="//field[@name='name']/.." position="before">
<label for="name" class="oe_edit_only" attrs="{'invisible': [('state', 'in', ['draft'])]}"/>
<attribute
name="attrs"
>{'readonly': [('state', 'not in', ['draft'])]}</attribute>
</xpath>
</field>
</record>
Expand Down