From 7a58b443f4db88c3a8e21502c42b6a59d675ff03 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 9 Mar 2023 14:21:57 +0000 Subject: [PATCH 1/5] [14.0][ADD] project_sequence --- .copier-answers.yml | 3 +- .github/workflows/test.yml | 12 ++++ project_sequence/README.rst | 35 +++++++++++ project_sequence/__init__.py | 1 + project_sequence/__manifest__.py | 21 +++++++ project_sequence/data/ir_sequence.xml | 13 +++++ project_sequence/i18n/es.po | 48 +++++++++++++++ project_sequence/models/__init__.py | 1 + project_sequence/models/project_project.py | 55 ++++++++++++++++++ project_sequence/readme/CONTRIBUTORS.rst | 2 + project_sequence/readme/CREDITS.rst | 6 ++ project_sequence/readme/DESCRIPTION.rst | 4 ++ project_sequence/readme/USAGE.rst | 15 +++++ project_sequence/static/description/icon.png | Bin 0 -> 9455 bytes project_sequence/tests/__init__.py | 1 + .../tests/test_project_sequence.py | 42 +++++++++++++ project_sequence/views/project_project.xml | 53 +++++++++++++++++ .../odoo/addons/project_sequence | 1 + setup/project_sequence/setup.py | 6 ++ 19 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 project_sequence/README.rst create mode 100644 project_sequence/__init__.py create mode 100644 project_sequence/__manifest__.py create mode 100644 project_sequence/data/ir_sequence.xml create mode 100644 project_sequence/i18n/es.po create mode 100644 project_sequence/models/__init__.py create mode 100644 project_sequence/models/project_project.py create mode 100644 project_sequence/readme/CONTRIBUTORS.rst create mode 100644 project_sequence/readme/CREDITS.rst create mode 100644 project_sequence/readme/DESCRIPTION.rst create mode 100644 project_sequence/readme/USAGE.rst create mode 100644 project_sequence/static/description/icon.png create mode 100644 project_sequence/tests/__init__.py create mode 100644 project_sequence/tests/test_project_sequence.py create mode 100644 project_sequence/views/project_project.xml create mode 120000 setup/project_sequence/odoo/addons/project_sequence create mode 100644 setup/project_sequence/setup.py diff --git a/.copier-answers.yml b/.copier-answers.yml index 1e77801a2c..b50d0de945 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -14,7 +14,8 @@ include_wkhtmltopdf: false odoo_version: 14.0 org_name: Odoo Community Association (OCA) org_slug: OCA -rebel_module_groups: [] +rebel_module_groups: +- project_sequence repo_description: 'TODO: add repo description.' repo_name: project repo_slug: project diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 215b84b0f8..d1a0d01fbd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,9 +36,18 @@ jobs: matrix: include: - container: ghcr.io/oca/oca-ci/py3.6-odoo14.0:latest + include: "project_sequence" makepot: "true" name: test with Odoo - container: ghcr.io/oca/oca-ci/py3.6-ocb14.0:latest + include: "project_sequence" + name: test with OCB + - container: ghcr.io/oca/oca-ci/py3.6-odoo14.0:latest + exclude: "project_sequence" + makepot: "true" + name: test with Odoo + - container: ghcr.io/oca/oca-ci/py3.6-ocb14.0:latest + exclude: "project_sequence" name: test with OCB services: postgres: @@ -49,6 +58,9 @@ jobs: POSTGRES_DB: odoo ports: - 5432:5432 + env: + INCLUDE: "${{ matrix.include }}" + EXCLUDE: "${{ matrix.exclude }}" steps: - uses: actions/checkout@v2 with: diff --git a/project_sequence/README.rst b/project_sequence/README.rst new file mode 100644 index 0000000000..38929e8775 --- /dev/null +++ b/project_sequence/README.rst @@ -0,0 +1,35 @@ +**This file is going to be generated by oca-gen-addon-readme.** + +*Manual changes will be overwritten.* + +Please provide content in the ``readme`` directory: + +* **DESCRIPTION.rst** (required) +* INSTALL.rst (optional) +* CONFIGURE.rst (optional) +* **USAGE.rst** (optional, highly recommended) +* DEVELOP.rst (optional) +* ROADMAP.rst (optional) +* HISTORY.rst (optional, recommended) +* **CONTRIBUTORS.rst** (optional, highly recommended) +* CREDITS.rst (optional) + +Content of this README will also be drawn from the addon manifest, +from keys such as name, authors, maintainers, development_status, +and license. + +A good, one sentence summary in the manifest is also highly recommended. + + +Automatic changelog generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`HISTORY.rst` can be auto generated using `towncrier `_. + +Just put towncrier compatible changelog fragments into `readme/newsfragments` +and the changelog file will be automatically generated and updated when a new fragment is added. + +Please refer to `towncrier` documentation to know more. + +NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. +If you need to run it manually, refer to `OCA/maintainer-tools README `_. diff --git a/project_sequence/__init__.py b/project_sequence/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/project_sequence/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_sequence/__manifest__.py b/project_sequence/__manifest__.py new file mode 100644 index 0000000000..ce7f8ba571 --- /dev/null +++ b/project_sequence/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2023 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +{ + "name": "Project Sequence", + "summary": "Add a sequence field to projects, filled automatically", + "version": "14.0.0.1.0", + "development_status": "Alpha", + "category": "Services/Project", + "website": "https://github.com/OCA/project", + "author": "Moduon, Odoo Community Association (OCA)", + "maintainers": ["yajo", "anddago78"], + "license": "LGPL-3", + "application": False, + "installable": True, + "depends": ["project"], + "data": [ + "data/ir_sequence.xml", + "views/project_project.xml", + ], +} diff --git a/project_sequence/data/ir_sequence.xml b/project_sequence/data/ir_sequence.xml new file mode 100644 index 0000000000..935d009508 --- /dev/null +++ b/project_sequence/data/ir_sequence.xml @@ -0,0 +1,13 @@ + + + + + Project sequence + project.sequence + %(range_y)s- + True + 5 + + + diff --git a/project_sequence/i18n/es.po b/project_sequence/i18n/es.po new file mode 100644 index 0000000000..012aa0356c --- /dev/null +++ b/project_sequence/i18n/es.po @@ -0,0 +1,48 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-04-10 10:29+0000\n" +"PO-Revision-Date: 2023-04-19 11:22+0200\n" +"Last-Translator: Andrea Cattalani \n" +"Language-Team: \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.1.1\n" + +#. module: project_sequence +#: model:ir.model.fields,field_description:project_sequence.field_project_project__code +msgid "Code" +msgstr "Código" + +#. module: project_sequence +#: model:ir.model.fields,field_description:project_sequence.field_project_project__display_name +msgid "Display Name" +msgstr "Nombre" + +#. module: project_sequence +#: model:ir.model.fields,field_description:project_sequence.field_project_project__id +msgid "ID" +msgstr "ID" + +#. module: project_sequence +#: model:ir.model.fields,field_description:project_sequence.field_project_project____last_update +msgid "Last Modified on" +msgstr "Última modificación el" + +#. module: project_sequence +#: model:ir.model.fields,field_description:project_sequence.field_project_project__name +msgid "Name" +msgstr "Nombre" + +#. module: project_sequence +#: model:ir.model,name:project_sequence.model_project_project +msgid "Project" +msgstr "Proyecto" diff --git a/project_sequence/models/__init__.py b/project_sequence/models/__init__.py new file mode 100644 index 0000000000..56545d0d4f --- /dev/null +++ b/project_sequence/models/__init__.py @@ -0,0 +1 @@ +from . import project_project diff --git a/project_sequence/models/project_project.py b/project_sequence/models/project_project.py new file mode 100644 index 0000000000..3a02296dbd --- /dev/null +++ b/project_sequence/models/project_project.py @@ -0,0 +1,55 @@ +# Copyright 2023 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + + +from odoo import api, fields, models + + +class ProjectProject(models.Model): + _inherit = "project.project" + + sequence_code = fields.Char( + readonly=True, + copy=False, + ) + name = fields.Char( + required=False, + ) + + def _sync_name(self): + """Set name if empty.""" + for rec in self - self.filtered("name"): + rec.name = rec.sequence_code + + def name_get(self): + """Prefix name with sequence code if they are different.""" + old_result = super().name_get() + result = [] + for id_, name in old_result: + project = self.browse(id_) + if project.sequence_code != name: + name = "{} - {}".format(project.sequence_code, name) + result.append((id_, name)) + return result + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + """Allow searching by sequence code by default.""" + # Do not add any domain when user just clicked on search widget + if not (name == "" and operator == "ilike"): + # The dangling | is needed to combine with the domain added by super() + args = (args or []) + ["|", ("sequence_code", operator, name)] + return super().name_search(name, args, operator, limit) + + @api.model + def create(self, vals): + res = super().create(vals) + res.sequence_code = self.env["ir.sequence"].next_by_code("project.sequence") + res._sync_name() + return res + + def write(self, vals): + res = super().write(vals) + if "name" in vals: + self._sync_name() + return res diff --git a/project_sequence/readme/CONTRIBUTORS.rst b/project_sequence/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..39e3103515 --- /dev/null +++ b/project_sequence/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Andrea Cattalani (`Moduon `__) +* Jairo Llopis (`Moduon `__) diff --git a/project_sequence/readme/CREDITS.rst b/project_sequence/readme/CREDITS.rst new file mode 100644 index 0000000000..3fed1f0673 --- /dev/null +++ b/project_sequence/readme/CREDITS.rst @@ -0,0 +1,6 @@ +.. This file is optional and contains additional credits, other than + authors, contributors, and maintainers. + +The development of this module has been financially supported by: + +* Moduon diff --git a/project_sequence/readme/DESCRIPTION.rst b/project_sequence/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..2ff654993d --- /dev/null +++ b/project_sequence/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +.. This file must be max 2-3 paragraphs, and is required. + It should explain *why* this module exists. + +Add a sequence field to projects, filled automatically and add a code sequence filter in tree view project. diff --git a/project_sequence/readme/USAGE.rst b/project_sequence/readme/USAGE.rst new file mode 100644 index 0000000000..9a8a277c1c --- /dev/null +++ b/project_sequence/readme/USAGE.rst @@ -0,0 +1,15 @@ +.. This file must be present. It contains the usage instructions + for end-users. As all other rst files included in the README, + it MUST NOT contain reStructuredText sections + only body text (paragraphs, lists, tables, etc). Should you need + a more elaborate structure to explain the addon, please create a + Sphinx documentation (which may include this file as a "quick start" + section). + +To use this module, you need to: + +#. Go to the project icon. +#. Click the button "create" to create a new project +#. Fill in the field Project name and click the "create" button +#. Now in the Kanban view see the project name when you are created +#. Repeat this operation creating another project without the name. diff --git a/project_sequence/static/description/icon.png b/project_sequence/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/project_sequence/tests/__init__.py b/project_sequence/tests/__init__.py new file mode 100644 index 0000000000..fd6799dc71 --- /dev/null +++ b/project_sequence/tests/__init__.py @@ -0,0 +1 @@ +from . import test_project_sequence diff --git a/project_sequence/tests/test_project_sequence.py b/project_sequence/tests/test_project_sequence.py new file mode 100644 index 0000000000..4dabc49035 --- /dev/null +++ b/project_sequence/tests/test_project_sequence.py @@ -0,0 +1,42 @@ +# Copyright 2023 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) +from freezegun import freeze_time + +from odoo.tests.common import Form, SavepointCase, new_test_user, tagged, users + + +@tagged("-at_install", "post_install") +@freeze_time("2023-01-01 12:00:00") +class TestProjectSequence(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + new_test_user(cls.env, "manager", "project.group_project_manager") + cls.pjr_seq = cls.env.ref("project_sequence.seq_project_sequence") + cls.pjr_seq.date_range_ids.unlink() + cls.pjr_seq._get_current_sequence().number_next = 11 + + @users("manager") + def test_sequence_after_creation(self): + """Sequence is applied only after project creation.""" + prj_f = Form(self.env["project.project"]) + self.assertFalse(prj_f.name) + self.assertFalse(prj_f.sequence_code) + proj = prj_f.save() + self.assertTrue(proj.sequence_code) + self.assertEqual(proj.name, proj.sequence_code) + self.assertEqual(proj.sequence_code, "23-00011") + self.assertEqual(proj.display_name, "23-00011") + + @users("manager") + def test_sequence_copied_to_name_if_emptied(self): + """Sequence is copied to project name if user removes it.""" + proj = self.env["project.project"].create({"name": "whatever"}) + self.assertEqual(proj.name, "whatever") + self.assertEqual(proj.sequence_code, "23-00012") + self.assertEqual(proj.display_name, "23-00012 - whatever") + with Form(proj) as prj_f: + prj_f.name = False + self.assertEqual(proj.name, "23-00012") + self.assertEqual(proj.sequence_code, "23-00012") + self.assertEqual(proj.display_name, "23-00012") diff --git a/project_sequence/views/project_project.xml b/project_sequence/views/project_project.xml new file mode 100644 index 0000000000..68621eb98d --- /dev/null +++ b/project_sequence/views/project_project.xml @@ -0,0 +1,53 @@ + + + + + Project.sequence.project.edit + project.project + + + + + + + + + Project_sequence_project_view + project.project + + + + + + + + + Project.sequence.project.kanban + project.project + + + + + + + 1 + + + + + + + + Project.sequence.project.view.search + project.project + + + + ['|', ('name', 'ilike', self), ('sequence_code', 'ilike', self)] + + + + diff --git a/setup/project_sequence/odoo/addons/project_sequence b/setup/project_sequence/odoo/addons/project_sequence new file mode 120000 index 0000000000..46dd191731 --- /dev/null +++ b/setup/project_sequence/odoo/addons/project_sequence @@ -0,0 +1 @@ +../../../../project_sequence \ No newline at end of file diff --git a/setup/project_sequence/setup.py b/setup/project_sequence/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/project_sequence/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 8c9e29c4a03682da2c263ba15b4dd74d57b8c0e2 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Wed, 10 May 2023 10:23:19 +0100 Subject: [PATCH 2/5] [IMP] project_sequence: move sequence below name When a project has a name, still the sequence is a very important field to be displayed. I move it below the project name. @moduon MT-1506 --- project_sequence/views/project_project.xml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/project_sequence/views/project_project.xml b/project_sequence/views/project_project.xml index 68621eb98d..483b447c43 100644 --- a/project_sequence/views/project_project.xml +++ b/project_sequence/views/project_project.xml @@ -7,8 +7,17 @@ project.project - - + +
+
+
+
From 113593b2a0a5d21720cc30e78a6b743ca3fa5b45 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Wed, 10 May 2023 11:22:35 +0100 Subject: [PATCH 3/5] [FIX] project_wbs: make compatible with project_sequence Support the use case of a `False` name coming in, which can happen when installing `project_sequence`. @moduon MT-1506 --- project_wbs/models/project_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project_wbs/models/project_project.py b/project_wbs/models/project_project.py index a459e6ad74..c9232b2011 100644 --- a/project_wbs/models/project_project.py +++ b/project_wbs/models/project_project.py @@ -146,7 +146,7 @@ def _resolve_analytic_account_id_from_context(self): def prepare_analytics_vals(self, vals): return { - "name": vals.get("name", _("Unknown Analytic Account")), + "name": vals.get("name") or _("Unknown Analytic Account"), "company_id": vals.get("company_id", self.env.user.company_id.id), "partner_id": vals.get("partner_id"), "active": True, From 6835c70509ea18859fc791c13a25f45aa4448311 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Wed, 10 May 2023 10:22:01 +0100 Subject: [PATCH 4/5] [FIX] project_sequence: make compatible with hr_timesheet `hr_timesheet` creates an analytic account by default. The method it uses to create it expects a preexisting name. But since we're making name not required, we're breaking other module's expectations. To fix this problem, now the name sync is done before writing or creating records, and not after. To make sure the problem doesn't happen anymore, we keep the `NOT NULL` requirement on project names. We just do it with a manual SQL constraint. This extra protection ensures compatibility with any other modules that expect always a value on the name. Any possibly misconfigured sequence could produce sequence duplicates. I also add protection against that. In tests, the project sequence was wrongly reset to 11 only once. Turns out that it survives the test savepoint, so I now reset it before each test instead. Tests are more reliable now. Besides, I added some more. @moduon MT-1506 wip wip --- project_sequence/models/project_project.py | 60 +++++++++++---- .../tests/test_project_sequence.py | 73 ++++++++++++++++--- 2 files changed, 109 insertions(+), 24 deletions(-) diff --git a/project_sequence/models/project_project.py b/project_sequence/models/project_project.py index 3a02296dbd..03babbc328 100644 --- a/project_sequence/models/project_project.py +++ b/project_sequence/models/project_project.py @@ -7,19 +7,33 @@ class ProjectProject(models.Model): _inherit = "project.project" + _sql_constraints = [ + # Ensure compatibility with other modules that always expect a value in name + ("name_required", "CHECK(name IS NOT NULL)", "Project name is required"), + ( + "sequence_code_unique", + "UNIQUE(sequence_code)", + "Sequence code must be unique", + ), + ] sequence_code = fields.Char( - readonly=True, copy=False, + readonly=True, ) name = fields.Char( + # We actually require it with the SQL constraint, but it is disabled + # here to let users create/write projects without name, and let this module + # add a default name if needed required=False, ) - def _sync_name(self): - """Set name if empty.""" - for rec in self - self.filtered("name"): - rec.name = rec.sequence_code + def _sync_analytic_account_name(self): + """Set analytic account name equal to project's display name.""" + for rec in self: + if not rec.analytic_account_id: + continue + rec.analytic_account_id.name = rec.display_name def name_get(self): """Prefix name with sequence code if they are different.""" @@ -41,15 +55,33 @@ def name_search(self, name="", args=None, operator="ilike", limit=100): args = (args or []) + ["|", ("sequence_code", operator, name)] return super().name_search(name, args, operator, limit) - @api.model - def create(self, vals): - res = super().create(vals) - res.sequence_code = self.env["ir.sequence"].next_by_code("project.sequence") - res._sync_name() + @api.model_create_multi + def create(self, vals_list): + """Apply sequence code and a default name if not set.""" + # It is important to set sequence_code before calling super() because + # other modules such as hr_timesheet expect the name to always have a value + for vals in vals_list: + if not vals.get("sequence_code"): + vals["sequence_code"] = self.env["ir.sequence"].next_by_code( + "project.sequence" + ) + if not vals.get("name"): + vals["name"] = vals["sequence_code"] + res = super().create(vals_list) + # The analytic account is created with just the project name, but + # it is more useful to let it contain the project sequence too + res._sync_analytic_account_name() return res def write(self, vals): - res = super().write(vals) - if "name" in vals: - self._sync_name() - return res + """Sync name and analytic account name when name is changed.""" + # If name isn't changing, nothing special to do + if "name" not in vals and "sequence_name" not in vals: + return super().write(vals) + # When changing name, we need to update the analytic account name too + for one in self: + sequence_code = vals.get("sequence_code", one.sequence_code) + name = vals.get("name") or sequence_code + super().write(dict(vals, name=name)) + self._sync_analytic_account_name() + return True diff --git a/project_sequence/tests/test_project_sequence.py b/project_sequence/tests/test_project_sequence.py index 4dabc49035..f74ad23e3d 100644 --- a/project_sequence/tests/test_project_sequence.py +++ b/project_sequence/tests/test_project_sequence.py @@ -1,20 +1,31 @@ # Copyright 2023 Moduon Team S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) from freezegun import freeze_time +from psycopg2 import IntegrityError -from odoo.tests.common import Form, SavepointCase, new_test_user, tagged, users +from odoo.tests.common import Form, SavepointCase, new_test_user, users +from odoo.tools import mute_logger -@tagged("-at_install", "post_install") @freeze_time("2023-01-01 12:00:00") class TestProjectSequence(SavepointCase): @classmethod def setUpClass(cls): super().setUpClass() - new_test_user(cls.env, "manager", "project.group_project_manager") + new_test_user( + cls.env, + "manager", + "project.group_project_manager,analytic.group_analytic_accounting", + ) cls.pjr_seq = cls.env.ref("project_sequence.seq_project_sequence") cls.pjr_seq.date_range_ids.unlink() - cls.pjr_seq._get_current_sequence().number_next = 11 + cls.analytic_account = cls.env["account.analytic.account"].create( + {"name": "aaa"} + ) + + def setUp(self): + super().setUp() + self.pjr_seq._get_current_sequence().number_next = 11 @users("manager") def test_sequence_after_creation(self): @@ -28,15 +39,57 @@ def test_sequence_after_creation(self): self.assertEqual(proj.sequence_code, "23-00011") self.assertEqual(proj.display_name, "23-00011") + def test_analytic_account_after_creation_no_name(self): + """Project's analytic account is named like project's default name.""" + proj = self.env["project.project"].create( + {"analytic_account_id": self.analytic_account.id} + ) + self.assertEqual(proj.sequence_code, "23-00011") + self.assertEqual(proj.name, "23-00011") + self.assertEqual(proj.display_name, "23-00011") + self.assertEqual(proj.analytic_account_id.name, "23-00011") + + def test_analytic_account_after_creation_named(self): + """Project's analytic account is named like project's display name.""" + proj = self.env["project.project"].create( + {"name": "whatever", "analytic_account_id": self.analytic_account.id} + ) + self.assertEqual(proj.sequence_code, "23-00011") + self.assertEqual(proj.name, "whatever") + self.assertEqual(proj.display_name, "23-00011 - whatever") + self.assertEqual(proj.analytic_account_id.name, "23-00011 - whatever") + @users("manager") def test_sequence_copied_to_name_if_emptied(self): """Sequence is copied to project name if user removes it.""" - proj = self.env["project.project"].create({"name": "whatever"}) + proj = self.env["project.project"].create( + {"name": "whatever", "analytic_account_id": self.analytic_account.id} + ) self.assertEqual(proj.name, "whatever") - self.assertEqual(proj.sequence_code, "23-00012") - self.assertEqual(proj.display_name, "23-00012 - whatever") + self.assertEqual(proj.sequence_code, "23-00011") + self.assertEqual(proj.display_name, "23-00011 - whatever") + self.assertEqual(proj.analytic_account_id.name, "23-00011 - whatever") with Form(proj) as prj_f: prj_f.name = False - self.assertEqual(proj.name, "23-00012") - self.assertEqual(proj.sequence_code, "23-00012") - self.assertEqual(proj.display_name, "23-00012") + self.assertEqual(proj.name, "23-00011") + self.assertEqual(proj.sequence_code, "23-00011") + self.assertEqual(proj.display_name, "23-00011") + self.assertEqual(proj.analytic_account_id.name, "23-00011") + + @users("manager") + def test_sequence_not_copied_to_another_project(self): + """Sequence is not duplicated to another project.""" + proj1 = self.env["project.project"].create({"name": "whatever"}) + proj2 = proj1.copy() + self.assertEqual(proj1.sequence_code, "23-00011") + self.assertEqual(proj2.sequence_code, "23-00012") + + @users("manager") + @mute_logger("odoo.sql_db") + def test_sequence_unique(self): + """Sequence cannot have duplicates.""" + proj1 = self.env["project.project"].create({"name": "one"}) + self.assertEqual(proj1.sequence_code, "23-00011") + self.pjr_seq._get_current_sequence().number_next = 11 + with self.assertRaises(IntegrityError), self.env.cr.savepoint(): + proj1 = self.env["project.project"].create({"name": "two"}) From 6ac7e102a99f65ef3bf209807723194ecf408b71 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Thu, 11 May 2023 12:22:16 +0100 Subject: [PATCH 5/5] [FIX] project_sequence: support preexisting sequenceless projects These projects shouldn't display "False - Project name" in their display names. @moduon MT-1506 --- project_sequence/models/project_project.py | 4 ++-- project_sequence/tests/test_project_sequence.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/project_sequence/models/project_project.py b/project_sequence/models/project_project.py index 03babbc328..ffa02d83d7 100644 --- a/project_sequence/models/project_project.py +++ b/project_sequence/models/project_project.py @@ -41,7 +41,7 @@ def name_get(self): result = [] for id_, name in old_result: project = self.browse(id_) - if project.sequence_code != name: + if project.sequence_code and project.sequence_code != name: name = "{} - {}".format(project.sequence_code, name) result.append((id_, name)) return result @@ -61,7 +61,7 @@ def create(self, vals_list): # It is important to set sequence_code before calling super() because # other modules such as hr_timesheet expect the name to always have a value for vals in vals_list: - if not vals.get("sequence_code"): + if "sequence_code" not in vals: vals["sequence_code"] = self.env["ir.sequence"].next_by_code( "project.sequence" ) diff --git a/project_sequence/tests/test_project_sequence.py b/project_sequence/tests/test_project_sequence.py index f74ad23e3d..e6f7563192 100644 --- a/project_sequence/tests/test_project_sequence.py +++ b/project_sequence/tests/test_project_sequence.py @@ -93,3 +93,16 @@ def test_sequence_unique(self): self.pjr_seq._get_current_sequence().number_next = 11 with self.assertRaises(IntegrityError), self.env.cr.savepoint(): proj1 = self.env["project.project"].create({"name": "two"}) + + @users("manager") + def test_project_without_sequence(self): + """Preexisting projects had no sequence, and they should display fine.""" + proj1 = self.env["project.project"].create( + {"name": "one", "sequence_code": False} + ) + self.assertEqual(proj1.display_name, "one") + self.assertFalse(proj1.sequence_code) + # Make sure that the sequence is not increased + proj2 = self.env["project.project"].create({"name": "two"}) + self.assertEqual(proj2.sequence_code, "23-00011") + self.assertEqual(proj2.display_name, "23-00011 - two")