From d69c2425eeb4a6e66bc73e9865d00db78b52e69e Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Wed, 10 Sep 2025 10:16:27 -0400 Subject: [PATCH] [MIG] customer_applications to 18.0 --- customer_applications/__init__.py | 1 + customer_applications/__manifest__.py | 40 +++++++++ customer_applications/data/menus_actions.xml | 43 ++++++++++ customer_applications/models/__init__.py | 5 ++ customer_applications/models/application.py | 36 ++++++++ .../models/application_specification.py | 42 +++++++++ .../models/application_specification_key.py | 11 +++ .../models/application_type.py | 54 ++++++++++++ customer_applications/models/res_partner.py | 42 +++++++++ customer_applications/security/groups.xml | 14 +++ .../security/ir.model.access.csv | 7 ++ customer_applications/tests/__init__.py | 1 + .../tests/test_application.py | 59 +++++++++++++ .../views/application_type_views.xml | 74 ++++++++++++++++ .../views/application_views.xml | 86 +++++++++++++++++++ .../views/res_partner_views.xml | 45 ++++++++++ 16 files changed, 560 insertions(+) create mode 100644 customer_applications/__init__.py create mode 100644 customer_applications/__manifest__.py create mode 100644 customer_applications/data/menus_actions.xml create mode 100644 customer_applications/models/__init__.py create mode 100644 customer_applications/models/application.py create mode 100644 customer_applications/models/application_specification.py create mode 100644 customer_applications/models/application_specification_key.py create mode 100644 customer_applications/models/application_type.py create mode 100644 customer_applications/models/res_partner.py create mode 100644 customer_applications/security/groups.xml create mode 100644 customer_applications/security/ir.model.access.csv create mode 100644 customer_applications/tests/__init__.py create mode 100644 customer_applications/tests/test_application.py create mode 100644 customer_applications/views/application_type_views.xml create mode 100644 customer_applications/views/application_views.xml create mode 100644 customer_applications/views/res_partner_views.xml diff --git a/customer_applications/__init__.py b/customer_applications/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/customer_applications/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/customer_applications/__manifest__.py b/customer_applications/__manifest__.py new file mode 100644 index 0000000..2bdd658 --- /dev/null +++ b/customer_applications/__manifest__.py @@ -0,0 +1,40 @@ +# +# Bemade Inc. +# +# Copyright (C) 2023-June Bemade Inc. (). +# Author: Marc Durepos (Contact : marc@bemade.org) +# +# This program is under the terms of the GNU Lesser General Public License, +# version 3. +# +# For full license details, see https://www.gnu.org/licenses/lgpl-3.0.en.html. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +{ + "name": "Customer Applications", + "version": "18.0.1.0.0", + "summary": "Adds the notion of applications to partners.", + "category": "Contacts", + "author": "Bemade Inc.", + "website": "http://www.bemade.org", + "license": "LGPL-3", + "depends": ["contacts", "incrementing_sequence_mixin"], + "data": [ + "security/groups.xml", + "security/ir.model.access.csv", + "data/menus_actions.xml", + "views/application_type_views.xml", + "views/res_partner_views.xml", + "views/application_views.xml", + ], + "assets": {}, + "installable": True, + "auto_install": False, +} diff --git a/customer_applications/data/menus_actions.xml b/customer_applications/data/menus_actions.xml new file mode 100644 index 0000000..87dcda3 --- /dev/null +++ b/customer_applications/data/menus_actions.xml @@ -0,0 +1,43 @@ + + + + Application Types + partner.application.type + list,form + +

+ No application types found. Create one? +

+
+
+ + + Partner Applications + partner.application + list,form + [('partner_id', '=', context.get('partner_id'))] + {'default_partner_id': context.get('partner_id')} + + + + Partner Applications by Type + partner.application + list,form + [ + ('application_type_id', '=', context.get('application_type_id')) + ] + + { + 'default_application_type_id': context.get('application_type_id') + } + + + + + + +
\ No newline at end of file diff --git a/customer_applications/models/__init__.py b/customer_applications/models/__init__.py new file mode 100644 index 0000000..dd86493 --- /dev/null +++ b/customer_applications/models/__init__.py @@ -0,0 +1,5 @@ +from . import application +from . import application_specification +from . import application_type +from . import res_partner +from . import application_specification_key diff --git a/customer_applications/models/application.py b/customer_applications/models/application.py new file mode 100644 index 0000000..389fe6b --- /dev/null +++ b/customer_applications/models/application.py @@ -0,0 +1,36 @@ +from odoo import models, fields, Command + + +class Application(models.Model): + _name = "partner.application" + _description = "Partner Application" + _inherit = ["mail.thread", "mail.activity.mixin"] + + name = fields.Char(tracking=1) + description = fields.Text(tracking=2) + partner_id = fields.Many2one( + comodel_name="res.partner", + required=True, + tracking=3, + copy=False, + ) + application_type_id = fields.Many2one( + comodel_name="partner.application.type", + required=True, + tracking=4, + ondelete="restrict", + ) + specification_ids = fields.One2many( + comodel_name="partner.application.specification", + inverse_name="application_id", + tracking=5, + ) + + def copy(self, default=None): + self.ensure_one() # This logic won't work for batches, and it doesn't need to + default = default or {} + if "specification_ids" not in default: + default["specification_ids"] = [ + Command.create(line.copy_data()[0]) for line in self.specification_ids + ] + return super().copy(default) diff --git a/customer_applications/models/application_specification.py b/customer_applications/models/application_specification.py new file mode 100644 index 0000000..e694e01 --- /dev/null +++ b/customer_applications/models/application_specification.py @@ -0,0 +1,42 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class PartnerApplicationSpecification(models.Model): + _name = "partner.application.specification" + _description = "Partner Application Specification" + _inherit = ["mail.thread", "mail.activity.mixin", "incrementing.sequence.mixin"] + _sequence_group = "application_id" + + key_id = fields.Many2one( + comodel_name="partner.application.specification.key", + tracking=1, + ondelete="restrict", + domain="[('id', 'in', allowed_specification_keys)]", + string="Specification Name", + required=True, + ) + name = fields.Char( + related="key_id.name", + ) + value = fields.Text( + tracking=2, + ) + application_id = fields.Many2one( + comodel_name="partner.application", + tracking=1, + ondelete="cascade", + ) + allowed_specification_keys = fields.Many2many( + related="application_id.application_type_id.allowed_specification_keys", + ) + + @api.constrains("key_id") + def _constrain_key_id(self): + for rec in self: + if rec.key_id not in rec.allowed_specification_keys: + raise ValidationError( + _( + f"Key '{rec.key_id.name}' is not allowed for this application type." + ) + ) diff --git a/customer_applications/models/application_specification_key.py b/customer_applications/models/application_specification_key.py new file mode 100644 index 0000000..41ef8c4 --- /dev/null +++ b/customer_applications/models/application_specification_key.py @@ -0,0 +1,11 @@ +from odoo import models, fields, api + + +class ApplicationSpecificationKey(models.Model): + _name = "partner.application.specification.key" + _description = "Application Specification Key" + + name = fields.Char(required=True, index="trigram") + _sql_constraints = [ + ("name_uniq", "unique (name)", "Specification key name must be unique."), + ] diff --git a/customer_applications/models/application_type.py b/customer_applications/models/application_type.py new file mode 100644 index 0000000..a34f94e --- /dev/null +++ b/customer_applications/models/application_type.py @@ -0,0 +1,54 @@ +from odoo import models, fields, api + + +class PartnerApplicationType(models.Model): + _name = "partner.application.type" + _description = "Partner Application Type" + + _inherit = ["mail.thread", "mail.activity.mixin"] + + color = fields.Integer() + name = fields.Char( + required=True, + tracking=1, + ) + description = fields.Text(tracking=2) + application_ids = fields.One2many( + comodel_name="partner.application", + inverse_name="application_type_id", + tracking=3, + ) + + applications_count = fields.Integer( + string="Applications Count", + compute="_compute_applications_count", + ) + + partner_ids = fields.One2many( + comodel_name="res.partner", + compute="_compute_partner_ids", + search="_search_partner_ids", + string="Partners", + readonly=True, + ) + allowed_specification_keys = fields.Many2many( + comodel_name="partner.application.specification.key", + relation="application_specification_key_application_type_rel", + column1="application_type_id", + column2="application_specification_key_id", + ) + + @api.depends("application_ids", "application_ids.partner_id") + def _compute_partner_ids(self): + for application_type in self: + application_type.partner_ids = application_type.application_ids.mapped( + "partner_id" + ) + + def _search_partner_ids(self, operator, value): + return [("application_ids.partner_id", operator, value)] + + @api.depends("application_ids") + def _compute_applications_count(self): + for record in self: + record.applications_count = len(record.application_ids) diff --git a/customer_applications/models/res_partner.py b/customer_applications/models/res_partner.py new file mode 100644 index 0000000..53cdf2c --- /dev/null +++ b/customer_applications/models/res_partner.py @@ -0,0 +1,42 @@ +from odoo import models, fields, api + + +class ResPartner(models.Model): + _inherit = "res.partner" + + application_ids = fields.One2many( + "partner.application", + "partner_id", + string="Applications", + ) + applications_count = fields.Integer( + string="Applications Count", + compute="_compute_applications_count", + ) + application_type_ids = fields.One2many( + comodel_name="partner.application.type", + compute="_compute_application_type_ids", + string="Application Types", + readonly=True, + search="_search_application_type_ids", + ) + + @api.depends("application_ids.application_type_id") + def _compute_application_type_ids(self): + for partner in self: + partner.application_type_ids = partner.application_ids.mapped( + "application_type_id" + ) + + def _search_application_type_ids(self, operator, value): + return [("application_ids.application_type_id", operator, value)] + + @api.depends("application_ids") + def _compute_applications_count(self): + for partner in self: + partner.applications_count = len(partner.application_ids) + + @api.depends("application_ids") + def _compute_applications_count(self): + for rec in self: + rec.applications_count = len(rec.application_ids) diff --git a/customer_applications/security/groups.xml b/customer_applications/security/groups.xml new file mode 100644 index 0000000..23e3fbc --- /dev/null +++ b/customer_applications/security/groups.xml @@ -0,0 +1,14 @@ + + + + + Applications User + + + Applications Admin + + + + + + \ No newline at end of file diff --git a/customer_applications/security/ir.model.access.csv b/customer_applications/security/ir.model.access.csv new file mode 100644 index 0000000..b7ce421 --- /dev/null +++ b/customer_applications/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_partner_application_user,access.partner.application.user,model_partner_application,group_applications_user,1,1,1,1 +access_partner_application_type_user,access.partner.application.type.user,model_partner_application_type,group_applications_user,1,0,0,0 +access_partner_application_type_manager,access.partner.application.type.manager,model_partner_application_type,group_applications_admin,1,1,1,1 +access_partner_application_specification_user,access.partner.application.specification.user,model_partner_application_specification,group_applications_user,1,1,1,1 +access_partner_application_specification_key_user,access.partner.application.specification.key.user,model_partner_application_specification_key,group_applications_user,1,0,0,0 +access_partner_application_specification_key_admin,access.partner.application.specification.key.admin,model_partner_application_specification_key,group_applications_admin,1,1,1,1 \ No newline at end of file diff --git a/customer_applications/tests/__init__.py b/customer_applications/tests/__init__.py new file mode 100644 index 0000000..7abb9fe --- /dev/null +++ b/customer_applications/tests/__init__.py @@ -0,0 +1 @@ +from . import test_application diff --git a/customer_applications/tests/test_application.py b/customer_applications/tests/test_application.py new file mode 100644 index 0000000..8c3f77b --- /dev/null +++ b/customer_applications/tests/test_application.py @@ -0,0 +1,59 @@ +from odoo.tests import TransactionCase + + +class TestApplication(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner_1, cls.partner_2 = cls.env["res.partner"].create( + [ + { + "name": "Test Partner", + }, + { + "name": "Test Partner 2", + }, + ] + ) + cls.application_type = cls.env["partner.application.type"].create( + { + "name": "application type", + } + ) + + def test_copy_correctly_creates_specification_lines(self): + application = self.env["partner.application"].create( + { + "partner_id": self.partner_1.id, + "application_type_id": self.application_type.id, + } + ) + specifications = self.env["partner.application.specification"].create( + [ + { + "name": "Spec 1", + "value": "Spec 1 value", + "application_id": application.id, + }, + { + "name": "Spec 2", + "value": "Spec 2 value", + "application_id": application.id, + }, + ] + ) + application_copy = application.copy( + default={ + "partner_id": self.partner_2.id, + } + ) + self.assertEqual(len(application_copy.specification_ids), 2) + self.assertEqual(len(application.specification_ids), 2) + self.assertNotEqual( + application.specification_ids, application_copy.specification_ids + ) + + # TODO: move this to a test in the mixin module once we figure out how + # to dynamically create and load models + self.assertEqual(specifications[0].sequence, 1) + self.assertEqual(specifications[1].sequence, 2) diff --git a/customer_applications/views/application_type_views.xml b/customer_applications/views/application_type_views.xml new file mode 100644 index 0000000..8e78309 --- /dev/null +++ b/customer_applications/views/application_type_views.xml @@ -0,0 +1,74 @@ + + + + partner.application.type.list + partner.application.type + + + + + + + + + + + partner.application.type.form + partner.application.type + +
+
+ +
+ +
+
+
+
+ Application Type +
+

+ +

+
+
+ + + + + + + + + +
+
+ + + +
+ + + + + + partner.application.type.search + partner.application.type + + + + + + + + + \ No newline at end of file diff --git a/customer_applications/views/application_views.xml b/customer_applications/views/application_views.xml new file mode 100644 index 0000000..ebc1c5c --- /dev/null +++ b/customer_applications/views/application_views.xml @@ -0,0 +1,86 @@ + + + + partner.application.form + partner.application + +
+
+ +
+
+
+
+ Application +
+

+ +

+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + partner.application.list + partner.application + + + + + + + + + + + partner.application.search + partner.application + + + + + + + + + + + Partner Applications + partner.application + list,form + + + + + \ No newline at end of file diff --git a/customer_applications/views/res_partner_views.xml b/customer_applications/views/res_partner_views.xml new file mode 100644 index 0000000..8cbbfb4 --- /dev/null +++ b/customer_applications/views/res_partner_views.xml @@ -0,0 +1,45 @@ + + + + view.partner.form + res.partner + + +
+ +
+
+
+ + + view.partner.list + res.partner + + + + + + + + + + view.partner.search + res.partner + + + + + + + + +
\ No newline at end of file