[ADD] Introduce client applications

- Created models for client applications, application specifications and
  application types.
- Created a small technical module for adding a self-incrementing
  sequence field to other models. See incrementing_sequence_mixin.
This commit is contained in:
Marc Durepos 2024-10-16 07:10:16 -04:00
parent 947b03e125
commit cf7a59ad98
13 changed files with 261 additions and 0 deletions

View file

@ -458,6 +458,7 @@ class CalendarEvent(models.Model):
partner = self.env["res.partner"].search(
[("email", "=", _extract_vcal_email(organizer))]
)
# TODO: prioritize partner with a user if there is one
return partner[0] if partner else partner # partner[0] in case many matches
else:
return self.env["res.partner"]

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,33 @@
#
# Bemade Inc.
#
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
# 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": "17.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": [],
"assets": {},
"installable": True,
"auto_install": False,
}

View file

@ -0,0 +1,3 @@
from . import application
from . import application_specification
from . import application_type

View file

@ -0,0 +1,34 @@
from odoo import models, fields, Command
class Application(models.Model):
_name = "partner.application"
_description = "Partner Application"
_inherit = ["mail.thread", "mail.activity.mixin"]
partner_id = fields.Many2one(
comodel_name="res.partner",
string="Location",
required=True,
tracking=1,
copy=False,
)
application_type_id = fields.Many2one(
comodel_name="partner.application.type",
required=True,
tracking=2,
)
specification_ids = fields.One2many(
comodel_name="partner.application.specification",
inverse_name="application_id",
tracking=3,
)
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)

View file

@ -0,0 +1,20 @@
from odoo import models, fields
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"
name = fields.Char(
tracking=2,
required=True,
)
value = fields.Text(
tracking=2,
)
application_id = fields.Many2one(
comodel_name="partner.application",
tracking=1,
)

View file

@ -0,0 +1,14 @@
from odoo import models, fields
class PartnerApplicationType(models.Model):
_name = "partner.application.type"
_description = "Partner Application Type"
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char(
required=True,
tracking=1,
)
description = fields.Text(tracking=2)

View file

@ -0,0 +1 @@
from . import test_application

View file

@ -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)

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,45 @@
#
# Bemade Inc.
#
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
# 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": "Incrementing Sequence Mixin",
"version": "17.0.1.0.0",
"summary": "Adds an incrementing.sequence.mixin model to inherit from.",
"description": """
Adds an incrementing.sequence.mixin model to inherit from. This model adds a
sequence field to the models that inherit from it, including the logic to set it to
the highest current sequence number + 1 by default and to leave it otherwise
changeable by the user (i.e. with a drag and drop via the handle).
Specify the _sequence_group attribute on the model to indicate which field to use
to determine if records belong to the same group. For example, if one were to use
this on sale.order.line, they could specify "order_id" as the _sequence_group such
that newly created lines take on the highest sequence number of all lines on the
same sales order.
""",
"category": "",
"author": "Bemade Inc.",
"website": "http://www.bemade.org",
"license": "LGPL-3",
"depends": [],
"data": [],
"assets": {},
"installable": True,
"auto_install": False,
}

View file

@ -0,0 +1 @@
from . import incrementing_sequence_mixin

View file

@ -0,0 +1,48 @@
from odoo import models, fields, api
class IncrementingSequenceMixin(models.AbstractModel):
_name = "incrementing.sequence.mixin"
_description = "Incrementing Sequence Mixin"
_order = "sequence, id asc"
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if "incrementing.sequence.mixin" in cls._inherit:
if not hasattr(cls, "_sequence_group"):
raise ValueError(
f"Model {cls._name} inheriting from incrementing.sequence.mixin must define a '_sequence_group' attribute."
)
sequence = fields.Integer()
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
for rec in res:
if rec.sequence == 0:
group_field = rec._sequence_group
group_field_data = getattr(rec, group_field)
if hasattr(group_field_data, "id"):
group_field_data = group_field_data.id
group = self.env[rec._name].search(
[(group_field, "=", group_field_data)]
)
max_seq = max(group.mapped("sequence")) if group else 0
rec.sequence = max_seq + 1
return res
def _default_sequence(self):
group_field = self._sequence_group
group_field_data = getattr(self, group_field)
if hasattr(group_field_data, "id"):
group_field_data = group_field_data.id
group = self.env[self._name].search([(group_field, "=", group_field_data)])
max_seq = max(group.mapped("sequence")) if group else 0
# Don't recalculate if already set
for rec in self.filtered(lambda r: r.sequence == 0):
max_seq += 1
rec.sequence = max_seq
def _inverse_sequence(self):
pass