diff --git a/caldav_sync/models/calendar_event.py b/caldav_sync/models/calendar_event.py index 620abb0..a3a5ae8 100644 --- a/caldav_sync/models/calendar_event.py +++ b/caldav_sync/models/calendar_event.py @@ -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"] 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..54b25ef --- /dev/null +++ b/customer_applications/__manifest__.py @@ -0,0 +1,33 @@ +# +# 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": "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, +} diff --git a/customer_applications/models/__init__.py b/customer_applications/models/__init__.py new file mode 100644 index 0000000..dc2e897 --- /dev/null +++ b/customer_applications/models/__init__.py @@ -0,0 +1,3 @@ +from . import application +from . import application_specification +from . import application_type diff --git a/customer_applications/models/application.py b/customer_applications/models/application.py new file mode 100644 index 0000000..807caa7 --- /dev/null +++ b/customer_applications/models/application.py @@ -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) diff --git a/customer_applications/models/application_specification.py b/customer_applications/models/application_specification.py new file mode 100644 index 0000000..443177b --- /dev/null +++ b/customer_applications/models/application_specification.py @@ -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, + ) diff --git a/customer_applications/models/application_type.py b/customer_applications/models/application_type.py new file mode 100644 index 0000000..4c9300e --- /dev/null +++ b/customer_applications/models/application_type.py @@ -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) 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/incrementing_sequence_mixin/__init__.py b/incrementing_sequence_mixin/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/incrementing_sequence_mixin/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/incrementing_sequence_mixin/__manifest__.py b/incrementing_sequence_mixin/__manifest__.py new file mode 100644 index 0000000..cd62f03 --- /dev/null +++ b/incrementing_sequence_mixin/__manifest__.py @@ -0,0 +1,45 @@ +# +# 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": "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, +} diff --git a/incrementing_sequence_mixin/models/__init__.py b/incrementing_sequence_mixin/models/__init__.py new file mode 100644 index 0000000..9dcaa0e --- /dev/null +++ b/incrementing_sequence_mixin/models/__init__.py @@ -0,0 +1 @@ +from . import incrementing_sequence_mixin diff --git a/incrementing_sequence_mixin/models/incrementing_sequence_mixin.py b/incrementing_sequence_mixin/models/incrementing_sequence_mixin.py new file mode 100644 index 0000000..c1822f2 --- /dev/null +++ b/incrementing_sequence_mixin/models/incrementing_sequence_mixin.py @@ -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