From beb674896ea4442db585cc219169c753a67af420 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Mon, 19 Jan 2026 16:14:04 +0100 Subject: [PATCH 01/18] [ADD] estate: initialize the estate module --- estate/__init__.py | 0 estate/__manifest__.py | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..d97cc4d8574 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,6 @@ +{ + 'name': "Real Estate", + 'version': '1.0', + 'depends': ['base'], + 'application': True +} From eeaae6837eadfde097d8ad0e208662f1b6dc7eee Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Tue, 20 Jan 2026 09:59:11 +0100 Subject: [PATCH 02/18] [ADD] estate: create the property model Storing informtion related to properties in the module which will be mapped by ORM to a database table --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py index e69de29bb2d..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..57a1b39f6ed --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,21 @@ +from odoo import fields, models + +class Property(models.Model): + _name = "estate.property" + _description = "An estate property model" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] + ) From 29c25317d802cc424faac4a171a1e2a0b52fd7cd Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Tue, 20 Jan 2026 11:27:43 +0100 Subject: [PATCH 03/18] [LINT] estate: format python files --- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index d97cc4d8574..868d4f0764c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,5 +2,5 @@ 'name': "Real Estate", 'version': '1.0', 'depends': ['base'], - 'application': True + 'application': True, } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 57a1b39f6ed..df22cd7c30f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ from odoo import fields, models + class Property(models.Model): _name = "estate.property" _description = "An estate property model" @@ -17,5 +18,5 @@ class Property(models.Model): garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection( - selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], ) From 20f824c4c9527d8a970c4bd8333e2ad8c5de9866 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Tue, 20 Jan 2026 13:46:31 +0100 Subject: [PATCH 04/18] [ADD] estate: add access rights Defining the access rights to the 'property' model --- estate/__manifest__.py | 3 +++ estate/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 868d4f0764c..64b3611e3b3 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,5 +2,8 @@ 'name': "Real Estate", 'version': '1.0', 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + ], 'application': True, } diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..0e525a41a26 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,estate.property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From e4fad21f876ef92aaee9d2f45cac939f530d7989 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Tue, 20 Jan 2026 16:15:06 +0100 Subject: [PATCH 05/18] [IMP] estate: add missing configuration Define the author and license in manifest file --- estate/__manifest__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 64b3611e3b3..31973eb2598 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,4 +6,6 @@ 'security/ir.model.access.csv', ], 'application': True, + 'author': 'Odoo S.A.', + 'license': 'LGPL-3', } From 950741f76847a8b6ccb5836d51a41a39fe51a77c Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Wed, 21 Jan 2026 10:19:52 +0100 Subject: [PATCH 06/18] [IMP] estate: add new menus with basic action Created three menus which are linked to a a single action which creates a new property --- estate/__manifest__.py | 3 ++ estate/models/estate_property.py | 50 ++++++++++++++++++++++---- estate/views/estate_menus.xml | 8 +++++ estate/views/estate_property_views.xml | 8 +++++ 4 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 31973eb2598..4cff69d6ffd 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,6 +4,9 @@ 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', + + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'application': True, 'author': 'Odoo S.A.', diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index df22cd7c30f..665f2372c68 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,22 +1,60 @@ +from dateutil.relativedelta import relativedelta + from odoo import fields, models +GARDEN_ORIENTATIONS = [ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), +] + +PROPERTY_STATUS = [ + ('new', 'New'), + ('offer received', 'Offer Received'), + ('offer accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), +] + class Property(models.Model): _name = "estate.property" _description = "An estate property model" - name = fields.Char(required=True) + # === FIELDS ===# + + name = fields.Char( + required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() - expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + date_availability = fields.Date( + copy=False, + default=lambda self: self._default_date_availability()) + expected_price = fields.Float( + required=True) + selling_price = fields.Float( + copy=False, + readonly=True) + bedrooms = fields.Integer( + default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection( - selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + selection=GARDEN_ORIENTATIONS, + ) + active = fields.Boolean( + default=True) + state = fields.Selection( + copy=False, + default='new', + required=True, + selection=PROPERTY_STATUS, ) + + # Default method to set date_availability to three months from today + def _default_date_availability(self): + return fields.Datetime.today() + relativedelta(months=3) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..cf6435dde85 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..affda3b8abc --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate.property + list,form + + \ No newline at end of file From ba5fbc27029ef9efc678878bac0a80336e7288c1 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Wed, 21 Jan 2026 14:16:14 +0100 Subject: [PATCH 07/18] [IMP] estate: add custom views Create custom search, list and form views for the properties with filter and group by options --- estate/views/estate_property_views.xml | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index affda3b8abc..98a2636b8b9 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,5 +1,82 @@ + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +
+

+ +

+ + + + + + + + + + +
+ + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + Properties estate.property From 2b20c7eb369b4daff6a6b7f0365c91220c3f5207 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Thu, 22 Jan 2026 10:30:55 +0100 Subject: [PATCH 08/18] [IMP] estate: introduce new views and relations Created new models including property types, tags and offers. Linked the property to these models and added buyer info --- estate/__manifest__.py | 4 +- estate/models/__init__.py | 7 ++- estate/models/estate_property.py | 17 ++++++++ estate/models/estate_property_offer.py | 26 +++++++++++ estate/models/estate_property_tag.py | 11 +++++ estate/models/estate_property_type.py | 11 +++++ estate/security/ir.model.access.csv | 5 ++- estate/views/estate_menus.xml | 6 ++- estate/views/estate_property_offer_views.xml | 31 ++++++++++++++ estate/views/estate_property_tag_views.xml | 45 ++++++++++++++++++++ estate/views/estate_property_type_views.xml | 45 ++++++++++++++++++++ estate/views/estate_property_views.xml | 23 ++++++++-- 12 files changed, 223 insertions(+), 8 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 4cff69d6ffd..f2925ff4206 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,8 +4,10 @@ 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', - 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ], 'application': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..3683ff97b61 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,6 @@ -from . import estate_property +from . import ( + estate_property, + estate_property_offer, + estate_property_tag, + estate_property_type, +) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 665f2372c68..d246e361e28 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -54,6 +54,23 @@ class Property(models.Model): required=True, selection=PROPERTY_STATUS, ) + property_type_id = fields.Many2one( + "estate.property.type", + string='Property Type') + salesperson_id = fields.Many2one( + "res.users", + string="Salesperson", + default=lambda self: self.env.user) + buyer_id = fields.Many2one( + "res.partner", + string="Buyer", + copy=False) + tag_ids = fields.Many2many( + "estate.property.tag", + string="Tags") + offer_ids = fields.One2many( + "estate.property.offer", + "property_id") # Default method to set date_availability to three months from today def _default_date_availability(self): diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..f746effddb7 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,26 @@ +from odoo import fields, models + +OFFER_STATUS = [ + ('accepted', 'Accepted'), + ('refused', 'Refused'), +] + + +class PropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "An estate property offer model" + + # === FIELDS ===# + + price = fields.Float() + status = fields.Selection( + selection=OFFER_STATUS, + copy=False) + partner_id = fields.Many2one( + "res.partner", + string='Partner', + required=True) + property_id = fields.Many2one( + "estate.property", + string='Property', + required=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..e64430a30c5 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class PropertyTag(models.Model): + _name = "estate.property.tag" + _description = "An estate property tag model" + + # === FIELDS ===# + + name = fields.Char( + required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..e98bd5a59da --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class PropertyType(models.Model): + _name = "estate.property.type" + _description = "An estate property type model" + + # === FIELDS ===# + + name = fields.Char( + required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 0e525a41a26..404f43c06fe 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,estate.property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,estate.property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,estate.property.type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index cf6435dde85..81d59c8d97c 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,9 @@ + + + + -
\ No newline at end of file + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..c84028089e4 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,31 @@ + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + +
+
+
+ + + estate.property.offer.list + estate.property.offer + + + + + + + + +
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..f6830b583d1 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,45 @@ + + + + + estate.property.tag.search + estate.property.tag + + + + + + + + + estate.property.tag.form + estate.property.tag + +
+ +
+

+ +

+
+
+
+
+
+ + + estate.property.tag.list + estate.property.tag + + + + + + + + + Property Tags + estate.property.tag + list,form + +
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..95ae66e70c1 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,45 @@ + + + + + estate.property.type.search + estate.property.type + + + + + + + + + estate.property.type.form + estate.property.type + +
+ +
+

+ +

+
+
+
+
+
+ + + estate.property.type.list + estate.property.type + + + + + + + + + Property Types + estate.property.type + list,form + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 98a2636b8b9..659a911adca 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -7,6 +7,7 @@ + @@ -14,9 +15,8 @@ - - - + + @@ -33,6 +33,8 @@ + + @@ -55,6 +57,17 @@ + + + + + + + + + + + @@ -67,6 +80,8 @@ + + @@ -82,4 +97,4 @@ estate.property list,form - \ No newline at end of file + From 3211963184cd2ede6d4f58f64909b9e0dd73a071 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Thu, 22 Jan 2026 11:27:44 +0100 Subject: [PATCH 09/18] [IMP] estate: add computed fields and onchanges Compute total living area and offer deadline. Also autofill fields when garden is selected --- estate/models/estate_property.py | 32 +++++++++++++++++++- estate/models/estate_property_offer.py | 27 ++++++++++++++++- estate/views/estate_property_offer_views.xml | 6 +++- estate/views/estate_property_views.xml | 3 ++ 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d246e361e28..374bb7a55b6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models GARDEN_ORIENTATIONS = [ ('north', 'North'), @@ -71,7 +71,37 @@ class Property(models.Model): offer_ids = fields.One2many( "estate.property.offer", "property_id") + total_area = fields.Float( + compute='_compute_total_area', + string='Total Area') + best_price = fields.Float( + compute='_compute_best_price', + string='Best Offer') + + # === COMPUTE METHODS ===# # Default method to set date_availability to three months from today def _default_date_availability(self): return fields.Datetime.today() + relativedelta(months=3) + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends('offer_ids.price') + def _compute_best_price(self): + for record in self: + if record.offer_ids: + record.best_price = max(record.mapped('offer_ids.price')) + else: + record.best_price = 0.0 + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = False diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index f746effddb7..0ba248cd948 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models OFFER_STATUS = [ ('accepted', 'Accepted'), @@ -24,3 +26,26 @@ class PropertyOffer(models.Model): "estate.property", string='Property', required=True) + validity = fields.Integer( + default=7, + string='Validity (days)') + date_deadline = fields.Date( + compute='_compute_date_deadline', + inverse='_inverse_date_deadline', + store=True, + string='Deadline') + + # === COMPUTE METHODS ===# + + @api.depends('validity', 'create_date') + def _compute_date_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = record.create_date.date() + \ + relativedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + if record.create_date and record.date_deadline: + delta = record.date_deadline - record.create_date.date() + record.validity = delta.days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index c84028089e4..ee7894a509e 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -10,6 +10,8 @@ + + @@ -23,7 +25,9 @@ - + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 659a911adca..8d08af98c6b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -40,6 +40,7 @@ + @@ -52,9 +53,11 @@ + + From 6a1ba31b4d27031c5286ec12c29610357719ea57 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Thu, 22 Jan 2026 13:45:07 +0100 Subject: [PATCH 10/18] [IMP] estate: add action buttons to property form view Added buttons to mark a property listing as sold/cancel, as well as buttons to accept or reject an offer --- estate/models/estate_property.py | 19 +++++++++++++++++++ estate/models/estate_property_offer.py | 18 +++++++++++++++++- estate/views/estate_property_offer_views.xml | 2 ++ estate/views/estate_property_views.xml | 5 +++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 374bb7a55b6..ca6ba5d4eab 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.exceptions import UserError GARDEN_ORIENTATIONS = [ ('north', 'North'), @@ -105,3 +106,21 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = False + + # === ACTION METHODS ===# + + def action_set_sold(self): + for record in self: + if record.state == 'cancelled': + error_message = "Cancelled properties cannot be sold." + raise UserError(error_message) + record.state = 'sold' + return True + + def action_set_canceled(self): + for record in self: + if record.state == 'sold': + error_message = "Sold properties cannot be cancelled." + raise UserError(error_message) + record.state = 'cancelled' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 0ba248cd948..de33cc2324c 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -32,7 +32,6 @@ class PropertyOffer(models.Model): date_deadline = fields.Date( compute='_compute_date_deadline', inverse='_inverse_date_deadline', - store=True, string='Deadline') # === COMPUTE METHODS ===# @@ -43,9 +42,26 @@ def _compute_date_deadline(self): if record.create_date: record.date_deadline = record.create_date.date() + \ relativedelta(days=record.validity) + else: + record.date_deadline = fields.Datetime.today() + \ + relativedelta(days=record.validity) def _inverse_date_deadline(self): for record in self: if record.create_date and record.date_deadline: delta = record.date_deadline - record.create_date.date() record.validity = delta.days + + # === ACTION METHODS ===# + + def action_accept_offer(self): + for record in self: + record.status = 'accepted' + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + return True + + def action_reject_offer(self): + for record in self: + record.status = 'refused' + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index ee7894a509e..8d92681936c 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -28,6 +28,8 @@ + + +
+

+ +

+
+ + + + + + + + + + +
@@ -33,6 +55,7 @@ + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e9bda9e3888..8f89561dedc 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -11,7 +11,8 @@ - + @@ -27,8 +28,9 @@
-
@@ -37,9 +39,8 @@ - - - + + @@ -47,6 +48,7 @@ +
@@ -59,16 +61,13 @@ - - - + + - - - + @@ -86,16 +85,16 @@ estate.property.list estate.property - + - + - + @@ -104,5 +103,6 @@ Properties estate.property list,form + {'search_default_available_properties': True} From e251e70881d6670da89cc24a68a73967dc836ebd Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Fri, 23 Jan 2026 11:23:55 +0100 Subject: [PATCH 13/18] [FIX] estate: remove redundant code Remove redundant search and form views and fix import formatting --- estate/models/__init__.py | 10 ++--- estate/views/estate_property_tag_views.xml | 26 ------------- estate/views/estate_property_type_views.xml | 42 ++++++++------------- 3 files changed, 20 insertions(+), 58 deletions(-) diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 3683ff97b61..8f2187ee09e 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,6 +1,4 @@ -from . import ( - estate_property, - estate_property_offer, - estate_property_tag, - estate_property_type, -) +from . import estate_property +from . import estate_property_offer +from . import estate_property_tag +from . import estate_property_type diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index 672e688b3de..982a727b635 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -1,32 +1,6 @@ - - estate.property.tag.search - estate.property.tag - - - - - - - - - estate.property.tag.form - estate.property.tag - - - -
-

- -

-
-
- -
-
- estate.property.tag.list estate.property.tag diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index fc6e5f910f7..88e9e274c14 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -1,38 +1,28 @@ - - estate.property.type.search - estate.property.type - - - - - - - estate.property.type.form estate.property.type
-
- -
-
-

- -

-
+
+ +
+
+

+ +

+
From 5ede046feefddd4ed00a213400cf73bd99536d39 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Mon, 26 Jan 2026 11:33:40 +0100 Subject: [PATCH 14/18] [IMP] estate: display salesperson properties in user view Override CRUD methods to delete a property in certain states only and to update property state on offer creation with same or higher value offers. Link properties in res.users model by inheritance to display them in list view under the salesperson profile. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 12 ++++++++++++ estate/models/estate_property_offer.py | 25 ++++++++++++++++++++++++- estate/models/res_users.py | 10 ++++++++++ estate/views/res_users_views.xml | 16 ++++++++++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index f2925ff4206..506b05b417f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,6 +8,7 @@ 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', 'views/estate_property_offer_views.xml', + 'views/res_users_views.xml', 'views/estate_menus.xml', ], 'application': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8f2187ee09e..fea9f441d6d 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_offer from . import estate_property_tag from . import estate_property_type +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index c36f48f420d..bdc26d083a6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -151,3 +151,15 @@ def _check_selling_price(self): "of the expected price." ) raise UserError(error_message) + + # === CRUD OVERRIDES ===# + + @api.ondelete(at_uninstall=False) + def _unlink_property(self): + for record in self: + if record.state not in ['new', 'cancelled']: + error_message = ( + "Only properties in 'New' or 'Cancelled' state " + "can be deleted." + ) + raise UserError(error_message) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 568c679adb4..1d9605123b9 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,8 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.exceptions import UserError + OFFER_STATUS = [ ('accepted', 'Accepted'), @@ -26,7 +28,8 @@ class PropertyOffer(models.Model): property_id = fields.Many2one( "estate.property", string='Property', - required=True) + required=True, + ondelete='cascade') validity = fields.Integer( default=7, string='Validity (days)') @@ -75,3 +78,23 @@ def action_reject_offer(self): for record in self: record.status = 'refused' return True + + # === CONSTRAINT METHODS ===# + + @api.constrains('price') + def _check_price(self): + for record in self: + existing_offers = record.property_id.offer_ids.filtered(lambda o: o != record) + if existing_offers: + min_price = min(existing_offers.mapped('price')) + if record.price < min_price: + error_message = "The offer price can't be lower than existing offers!" + raise UserError(error_message) + + # === CRUD OVERRIDES ===# + + @api.model + def create(self, vals): + offer = super().create(vals) + offer.property_id.state = 'offer received' + return offer diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..32d6423e3d1 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesperson_id", + domain=['|', ('state', '=', 'new'), ('state', '=', 'offer received')]) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..136b8887184 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,16 @@ + + + + + res.users.form + res.users + + + + + + + + + + From 3aa83d3ca29f95eef1b2e5836bcbd84eea362e9b Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Mon, 26 Jan 2026 16:59:40 +0100 Subject: [PATCH 15/18] [IMP] estate: create invoice on property sale Added a new module to create an invoice when a property is marked as sold. --- estate/__manifest__.py | 2 +- estate/models/estate_property_offer.py | 1 + estate/models/res_users.py | 2 +- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 8 ++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 24 ++++++++++++++++++++++++ 7 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 506b05b417f..61dff35c57f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,9 +5,9 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', - 'views/estate_property_offer_views.xml', 'views/res_users_views.xml', 'views/estate_menus.xml', ], diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 1d9605123b9..c0e31b2f2df 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -72,6 +72,7 @@ def action_accept_offer(self): record.status = 'accepted' record.property_id.selling_price = record.price record.property_id.buyer_id = record.partner_id + record.property_id.state = 'offer accepted' return True def action_reject_offer(self): diff --git a/estate/models/res_users.py b/estate/models/res_users.py index 32d6423e3d1..ab94298b363 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -2,7 +2,7 @@ class ResUsers(models.Model): - _inherit = "res.users" + _inherit = ["res.users"] property_ids = fields.One2many( "estate.property", diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..6b709923480 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,8 @@ +{ + 'name': "Estate Account", + 'version': '1.0', + 'depends': ['estate', 'account'], + 'application': True, + 'author': 'Odoo S.A.', + 'license': 'LGPL-3', +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..32f510c1ca8 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,24 @@ +from odoo import Command, models + + +class Property(models.Model): + _inherit = ["estate.property"] + + # === ACTION METHODS === # + + def action_set_sold(self): + move = self.env['account.move'].create({ + 'partner_id': self.buyer_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': [ + Command.create({ + 'name': self.name, + 'quantity': 1, + 'price_unit': self.selling_price * 0.06}), + Command.create({ + 'name': 'Adminstration Fee', + 'quantity': 1, + 'price_unit': 100,}), + ], + }) + return super().action_set_sold() From 68fbd12788b7414faac9dd20e605461e3c28f1b4 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Tue, 27 Jan 2026 14:29:27 +0100 Subject: [PATCH 16/18] [FIX] estate: fix linting and warnings Modify code to remove warning messages and pass ci checks --- estate/models/estate_property.py | 2 +- estate/models/res_users.py | 2 +- estate_account/models/estate_property.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index bdc26d083a6..b8de0f01617 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -20,7 +20,7 @@ ] -class Property(models.Model): +class EstateProperty(models.Model): _name = "estate.property" _description = "An estate property model" _order = "id desc" diff --git a/estate/models/res_users.py b/estate/models/res_users.py index ab94298b363..32d6423e3d1 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -2,7 +2,7 @@ class ResUsers(models.Model): - _inherit = ["res.users"] + _inherit = "res.users" property_ids = fields.One2many( "estate.property", diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 32f510c1ca8..a5f6c4a73ec 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -1,13 +1,13 @@ from odoo import Command, models -class Property(models.Model): - _inherit = ["estate.property"] +class EstateProperty(models.Model): + _inherit = "estate.property" # === ACTION METHODS === # def action_set_sold(self): - move = self.env['account.move'].create({ + self.env['account.move'].create({ 'partner_id': self.buyer_id.id, 'move_type': 'out_invoice', 'invoice_line_ids': [ @@ -18,7 +18,7 @@ def action_set_sold(self): Command.create({ 'name': 'Adminstration Fee', 'quantity': 1, - 'price_unit': 100,}), + 'price_unit': 100}), ], }) return super().action_set_sold() From 839861523f65ed1487b36e3cf99e341015cc3650 Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Tue, 27 Jan 2026 15:18:46 +0100 Subject: [PATCH 17/18] [IMP] estate: create kanban view Added a definition for the kanban view in the estate_property_views xml file --- estate/views/estate_property_views.xml | 29 +++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 8f89561dedc..fbc6c43e8ae 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,6 +1,33 @@ + + estate.property.kanban + estate.property + + + + + +
+ +
Expected Price:
+
+ Best Price: +
+
+ Selling Price: +
+
+ +
+
+
+
+
+
+
+ estate.property.search estate.property @@ -102,7 +129,7 @@ Properties estate.property - list,form + list,form,kanban {'search_default_available_properties': True}
From 666c4ffe4ab17cd6c0e796a3d03bd8f75af15b2e Mon Sep 17 00:00:00 2001 From: Waleed Elgamal Date: Tue, 27 Jan 2026 15:48:41 +0100 Subject: [PATCH 18/18] [FIX] estate: follow coding guidlines Update some records to follow naming conventions --- estate/views/estate_property_offer_views.xml | 6 +++--- estate/views/estate_property_tag_views.xml | 4 ++-- estate/views/estate_property_type_views.xml | 6 +++--- estate/views/estate_property_views.xml | 10 +++++----- estate/views/res_users_views.xml | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 46da7f23c0a..a0a65f218ae 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -2,7 +2,7 @@ - estate.property.offer.form + estate.property.offer.view.form estate.property.offer @@ -19,8 +19,8 @@ - - estate.property.offer.list + + estate.property.offer.view.list estate.property.offer diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index 982a727b635..8fd1a1c3058 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -1,8 +1,8 @@ - - estate.property.tag.list + + estate.property.tag.view.list estate.property.tag diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 88e9e274c14..3764e361a01 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -2,7 +2,7 @@ - estate.property.type.form + estate.property.type.view.form estate.property.type @@ -39,8 +39,8 @@ - - estate.property.type.list + + estate.property.type.view.list estate.property.type diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index fbc6c43e8ae..7e206889b3d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -2,7 +2,7 @@ - estate.property.kanban + estate.property.view.kanban estate.property @@ -29,7 +29,7 @@ - estate.property.search + estate.property.view.search estate.property @@ -50,7 +50,7 @@ - estate.property.form + estate.property.view.form estate.property @@ -108,8 +108,8 @@ - - estate.property.list + + estate.property.view.list estate.property diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml index 136b8887184..4b58b67c734 100644 --- a/estate/views/res_users_views.xml +++ b/estate/views/res_users_views.xml @@ -2,7 +2,7 @@ - res.users.form + res.users.view.form res.users