From 6c46d87b96f52686c37bff139b4853a7fafdf96f Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Mon, 20 Oct 2025 13:38:37 +0200 Subject: [PATCH 01/19] [ADD] estate: add initial manifest for Real Estate module --- estate/__init__.py | 0 estate/__manifest__.py | 7 +++++++ 2 files changed, 7 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..ee27fe97eba --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,7 @@ +{ + 'name': 'Real Estate', + 'summary': 'Manages real estate properties.', + 'depends': ['base'], + 'application': True, + 'installable': True, +} \ No newline at end of file From 0f11cdacb12de3fcf75c13ccc9a5c562cb22e070 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Mon, 20 Oct 2025 14:47:13 +0200 Subject: [PATCH 02/19] [IMP] estate: implement estate property model --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 27 +++++++++++++++++++++++++++ 3 files changed, 29 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..9a7e03eded3 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..c1f77ca3555 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,27 @@ +from odoo import models, fields + +class Property(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + + 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() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ]) + From 15ad93560bcdc93f946a8b5b8e4ad30f06998247 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Mon, 20 Oct 2025 15:35:03 +0200 Subject: [PATCH 03/19] [IMP] estate: add access control for estate 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 ee27fe97eba..0a14f405266 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,6 +2,9 @@ 'name': 'Real Estate', 'summary': 'Manages real estate properties.', 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + ], 'application': True, 'installable': True, } \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..0e11f47e58d --- /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,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From c521487215186bc2515d11242826103b935634c0 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Mon, 20 Oct 2025 16:44:45 +0200 Subject: [PATCH 04/19] [IMP] estate: add estate property views and menus --- estate/__init__.py | 2 +- estate/__manifest__.py | 4 +++- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 17 ++++++++++++----- estate/security/ir.model.access.csv | 2 +- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 7 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 0a14f405266..2616eafd0de 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,7 +4,9 @@ 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'application': True, 'installable': True, -} \ No newline at end of file +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index c1f77ca3555..8ceb6c10a35 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,3 +1,4 @@ +from dateutil.relativedelta import relativedelta from odoo import models, fields class Property(models.Model): @@ -7,12 +8,10 @@ class Property(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date(default=fields.Date.today() + relativedelta(months=3), copy=False) expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() - living_area = fields.Integer() - bedrooms = fields.Integer() + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() @@ -24,4 +23,12 @@ class Property(models.Model): ('east', 'East'), ('west', 'West'), ]) + active = fields.Boolean(default=True) + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], default='new', required=True, copy=False) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 0e11f47e58d..32389642d4f 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..8a467be10fb --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..520bb20cb9a --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate.property + list,form + + From 761b7a2e1aa6692ce6140885200039665d13c4aa Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Mon, 20 Oct 2025 16:49:09 +0200 Subject: [PATCH 05/19] [FIX] estate: remove the unnneeded installable field --- estate/__manifest__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 2616eafd0de..578b8f9ae21 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,5 +8,4 @@ 'views/estate_menus.xml', ], 'application': True, - 'installable': True, } From 7c4b00203aab57a0c4e816cfaa018bec1b54fdd8 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Tue, 21 Oct 2025 11:04:15 +0200 Subject: [PATCH 06/19] [IMP] estate: enhance property views with detailed form and search functionality --- estate/views/estate_property_views.xml | 76 ++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 520bb20cb9a..5fb8f71d30b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,80 @@ estate.property list,form + + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + From 04e7f8445ab8edf288940ad08e5791a4126e2828 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Tue, 21 Oct 2025 11:43:07 +0200 Subject: [PATCH 07/19] [FIX] estate: remove the group on postcode filter for property search view --- estate/views/estate_property_views.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 5fb8f71d30b..010fe81ab5a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -73,10 +73,8 @@ - - - - + + From d771be588fa81bae86eecaeffd52723fe336e69d Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Tue, 21 Oct 2025 13:58:03 +0200 Subject: [PATCH 08/19] [IMP] estate: add property type, tag, and offer models with views and menus --- estate/__manifest__.py | 7 ++++ estate/models/__init__.py | 3 ++ estate/models/estate_property.py | 6 ++- estate/models/estate_property_offer.py | 14 +++++++ estate/models/estate_property_tag.py | 7 ++++ estate/models/estate_property_type.py | 7 ++++ estate/security/ir.model.access.csv | 3 ++ estate/views/estate_menus.xml | 2 +- estate/views/estate_property_offer_views.xml | 31 ++++++++++++++ estate/views/estate_property_tag_menus.xml | 8 ++++ estate/views/estate_property_tag_views.xml | 8 ++++ estate/views/estate_property_type_menus.xml | 8 ++++ estate/views/estate_property_type_views.xml | 44 ++++++++++++++++++++ estate/views/estate_property_views.xml | 15 +++++++ 14 files changed, 161 insertions(+), 2 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_menus.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_menus.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 578b8f9ae21..8f94ba7ffa7 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,6 +6,13 @@ 'security/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_menus.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_type_menus.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_tag_menus.xml', + 'views/estate_property_offer_views.xml', ], 'application': True, + 'author': "Odoo", + 'license': 'AGPL-3' } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 8ceb6c10a35..012dd9bd0c7 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -31,4 +31,8 @@ class Property(models.Model): ('sold', 'Sold'), ('cancelled', 'Cancelled'), ], default='new', required=True, copy=False) - + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.uid) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..e4d05091f0f --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,14 @@ +from odoo import models, fields + +class PropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real Estate Property Offer" + + price = fields.Float() + status = fields.Selection([ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ], copy=False) + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + \ No newline at end of file diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..9dae24f0797 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,7 @@ +from odoo import models, fields + +class PropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real Estate Property Tag" + + 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..2da43741264 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,7 @@ +from odoo import models, fields + +class PropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 32389642d4f..89f97c50842 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,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_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 8a467be10fb..a4cd7913d09 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,7 +1,7 @@ - + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..93897343c09 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,31 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + +
+
+ +
+
diff --git a/estate/views/estate_property_tag_menus.xml b/estate/views/estate_property_tag_menus.xml new file mode 100644 index 00000000000..9f32c0f032f --- /dev/null +++ b/estate/views/estate_property_tag_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..4dcd3674e34 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_type_menus.xml b/estate/views/estate_property_type_menus.xml new file mode 100644 index 00000000000..ed8b0c02d6e --- /dev/null +++ b/estate/views/estate_property_type_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..5e10facaade --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,44 @@ + + + + Property Types + estate.property.type + list,form + + + + estate.property.type.list + estate.property.type + + + + + + + + + estate.property.type.form + estate.property.type + +
+ +
+

+ +

+
+
+
+
+
+ + + estate.property.type.search + estate.property.type + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 010fe81ab5a..31ca2344b74 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -12,12 +12,14 @@ + + @@ -32,9 +34,12 @@

+ + + @@ -56,6 +61,15 @@ + + + + + + + + + @@ -68,6 +82,7 @@ + From 582c69a8ec88301291aedf7d5fb4457335c38551 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Tue, 21 Oct 2025 14:04:54 +0200 Subject: [PATCH 09/19] [FIX] estate: adjust style --- estate/models/estate_property.py | 1 + estate/models/estate_property_offer.py | 3 ++- estate/models/estate_property_tag.py | 1 + estate/models/estate_property_type.py | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 012dd9bd0c7..238c885fb6d 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 models, fields + class Property(models.Model): _name = "estate.property" _description = "Real Estate Property" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index e4d05091f0f..ecd8071977c 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,6 @@ from odoo import models, fields + class PropertyOffer(models.Model): _name = "estate.property.offer" _description = "Real Estate Property Offer" @@ -11,4 +12,4 @@ class PropertyOffer(models.Model): ], copy=False) partner_id = fields.Many2one("res.partner", string="Partner", required=True) property_id = fields.Many2one("estate.property", string="Property", required=True) - \ No newline at end of file + \ No newline at end of file diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 9dae24f0797..d612df2ebda 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -1,5 +1,6 @@ from odoo import models, fields + class PropertyTag(models.Model): _name = "estate.property.tag" _description = "Real Estate Property Tag" diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 2da43741264..cb9713a46c2 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,5 +1,6 @@ from odoo import models, fields + class PropertyType(models.Model): _name = "estate.property.type" _description = "Real Estate Property Type" From 9d9a3a4808b66bd5fd600da20dec8aeb92f63e66 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Tue, 21 Oct 2025 14:51:53 +0200 Subject: [PATCH 10/19] [FIX] estate: adjust style --- estate/models/estate_property_offer.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index ecd8071977c..e98a1918b5e 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -6,10 +6,14 @@ class PropertyOffer(models.Model): _description = "Real Estate Property Offer" price = fields.Float() - status = fields.Selection([ - ('accepted', 'Accepted'), - ('refused', 'Refused'), - ], copy=False) - partner_id = fields.Many2one("res.partner", string="Partner", required=True) - property_id = fields.Many2one("estate.property", string="Property", required=True) - \ No newline at end of file + status = fields.Selection( + [ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + copy=False, + ) + partner_id = fields.Many2one( + "res.partner", string="Partner", required=True) + property_id = fields.Many2one( + "estate.property", string="Property", required=True) From 7148e7fc23da7caf1258b1d7b3f117859c29f721 Mon Sep 17 00:00:00 2001 From: Mathilde Pascal Date: Tue, 21 Oct 2025 14:26:52 +0200 Subject: [PATCH 11/19] [IMP] estate: Add notes. --- estate/models/estate_property.py | 1 + estate/views/estate_property_views.xml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 238c885fb6d..0d6ae3d2e1c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,6 +8,7 @@ class Property(models.Model): name = fields.Char(required=True) description = fields.Text() + notes = fields.Html() postcode = fields.Char() date_availability = fields.Date(default=fields.Date.today() + relativedelta(months=3), copy=False) expected_price = fields.Float(required=True) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 31ca2344b74..320d1711a32 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -61,6 +61,9 @@ + + + From ffc7f58002b3df2472fb14148ef8d68835156191 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Tue, 21 Oct 2025 16:08:50 +0200 Subject: [PATCH 12/19] [IMP] estate: add computed fields and onchanges --- estate/models/estate_property.py | 40 +++++++++++++++++--- estate/models/estate_property_offer.py | 24 +++++++++++- estate/views/estate_property_offer_views.xml | 6 ++- estate/views/estate_property_views.xml | 2 + 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0d6ae3d2e1c..55c15ad2860 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ from dateutil.relativedelta import relativedelta -from odoo import models, fields +from odoo import api, models, fields class Property(models.Model): @@ -10,7 +10,8 @@ class Property(models.Model): description = fields.Text() notes = fields.Html() postcode = fields.Char() - date_availability = fields.Date(default=fields.Date.today() + relativedelta(months=3), copy=False) + date_availability = fields.Date( + default=fields.Date.today() + relativedelta(months=3), copy=False) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) @@ -33,8 +34,37 @@ class Property(models.Model): ('sold', 'Sold'), ('cancelled', 'Cancelled'), ], default='new', required=True, copy=False) - property_type_id = fields.Many2one("estate.property.type", string="Property Type") + property_type_id = fields.Many2one( + "estate.property.type", string="Property Type") buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) - salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.uid) + salesperson_id = fields.Many2one( + "res.users", string="Salesperson", default=lambda self: self.env.uid) tag_ids = fields.Many2many("estate.property.tag", string="Tags") - offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + offer_ids = fields.One2many( + "estate.property.offer", "property_id", string="Offers") + total_area = fields.Integer( + compute="_compute_total_area", string="Total Area (sqm)") + best_offer = fields.Float( + compute="_compute_best_offer", string="Best Offer") + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for property in self: + property.total_area = property.living_area + property.garden_area + + @api.depends('offer_ids.price') + def _compute_best_offer(self): + for property in self: + if property.offer_ids: + property.best_offer = max(property.offer_ids.mapped('price')) + else: + property.best_offer = 0.0 + + @api.onchange('garden') + def _onchange_garden(self): + if not self.garden: + self.garden_area = 0 + self.garden_orientation = None + else: + self.garden_area = 10 + self.garden_orientation = 'north' diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index e98a1918b5e..6bac1f609d7 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import api, models, fields class PropertyOffer(models.Model): @@ -17,3 +17,25 @@ class PropertyOffer(models.Model): "res.partner", string="Partner", required=True) property_id = fields.Many2one( "estate.property", string="Property", required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_date_deadline", inverse="_inverse_date_deadline", string="Deadline") + + @api.depends('validity', 'create_date') + def _compute_date_deadline(self): + for offer in self: + if offer.create_date: + offer.date_deadline = fields.Date.add( + offer.create_date.date(), days=offer.validity) + else: + offer.date_deadline = fields.Date.add( + fields.Date.today(), days=offer.validity) + + def _inverse_date_deadline(self): + for offer in self: + if offer.create_date and offer.date_deadline: + delta = (offer.date_deadline - offer.create_date.date()).days + offer.validity = delta + elif offer.date_deadline: + delta = (offer.date_deadline - fields.Date.today()).days + offer.validity = delta diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 93897343c09..439b4f7b58a 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -6,7 +6,9 @@ - + + + @@ -21,6 +23,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 320d1711a32..9e01c8ae4ef 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -45,6 +45,7 @@ + @@ -59,6 +60,7 @@ + From c0276dcb43cc00420232e8756cf101119fe844b9 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Tue, 21 Oct 2025 17:27:16 +0200 Subject: [PATCH 13/19] [IMP] estate: add buttons for some actions --- estate/models/estate_property.py | 37 ++++++++++++++++++-- estate/models/estate_property_offer.py | 12 +++++++ estate/views/estate_property_offer_views.xml | 2 ++ estate/views/estate_property_views.xml | 4 +++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 55c15ad2860..ab9d19f7b6c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ from dateutil.relativedelta import relativedelta -from odoo import api, models, fields +from odoo import api, models, fields, exceptions class Property(models.Model): @@ -13,7 +13,8 @@ class Property(models.Model): date_availability = fields.Date( default=fields.Date.today() + relativedelta(months=3), copy=False) expected_price = fields.Float(required=True) - selling_price = fields.Float(readonly=True, copy=False) + selling_price = fields.Float( + readonly=True, copy=False, compute="_compute_selling_price_and_buyer") bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() @@ -36,7 +37,8 @@ class Property(models.Model): ], default='new', required=True, copy=False) property_type_id = fields.Many2one( "estate.property.type", string="Property Type") - buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + buyer_id = fields.Many2one("res.partner", string="Buyer", + copy=False, compute="_compute_selling_price_and_buyer") salesperson_id = fields.Many2one( "res.users", string="Salesperson", default=lambda self: self.env.uid) tag_ids = fields.Many2many("estate.property.tag", string="Tags") @@ -68,3 +70,32 @@ def _onchange_garden(self): else: self.garden_area = 10 self.garden_orientation = 'north' + + def action_set_sold(self): + for property in self: + if property.state != 'cancelled': + property.state = 'sold' + else: + raise exceptions.UserError( + "Cancelled properties cannot be sold.") + + def action_set_cancelled(self): + for property in self: + if property.state != 'sold': + property.state = 'cancelled' + else: + raise exceptions.UserError( + "Sold properties cannot be cancelled.") + + @api.depends('offer_ids') + def _compute_selling_price_and_buyer(self): + for property in self: + accepted_offers = property.offer_ids.filtered( + lambda o: o.status == 'accepted') + if accepted_offers: + best_offer = max(accepted_offers, key=lambda o: o.price) + property.selling_price = best_offer.price + property.buyer_id = best_offer.partner_id + else: + property.selling_price = 0.0 + property.buyer_id = None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 6bac1f609d7..d02329568b4 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -39,3 +39,15 @@ def _inverse_date_deadline(self): elif offer.date_deadline: delta = (offer.date_deadline - fields.Date.today()).days offer.validity = delta + + def action_accept_offer(self): + for offer in self: + offer.status = "accepted" + + return True + + def action_refuse_offer(self): + for offer in self: + offer.status = "refused" + + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 439b4f7b58a..9a820fe1373 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + +
+ + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9bd85b96018..d8c9acb06e7 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,13 +4,14 @@ Properties estate.property list,form + {'search_default_available_properties': True} estate.property.list estate.property - + @@ -18,7 +19,7 @@ - + @@ -30,8 +31,9 @@
-
@@ -39,12 +41,12 @@

- +
- + @@ -63,8 +65,8 @@ - - + + @@ -72,7 +74,7 @@ - + @@ -96,9 +98,9 @@ - + - + From e2e96ab27bb29094ae775dc7e8209ba823b9ef0f Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Thu, 23 Oct 2025 09:18:18 +0200 Subject: [PATCH 16/19] [FIX] estate: adjust style --- estate/models/estate_property_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index ff569f6f70c..f18fbff0e97 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -14,7 +14,7 @@ class PropertyType(models.Model): offer_ids = fields.One2many( "estate.property.offer", "property_type_id", string="Offers") offer_count = fields.Integer( - compute="_compute_offer_count",) + compute="_compute_offer_count") _unique_name = models.Constraint( 'UNIQUE(name)', 'The name must be unique.' From f974e96e0b4092e462f0315d48e1ab3bd958b38f Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Thu, 23 Oct 2025 11:19:16 +0200 Subject: [PATCH 17/19] [IMP] estate: use inheritance --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 7 ++++++ estate/models/estate_property_offer.py | 30 ++++++++++++++++++++------ estate/models/res_users.py | 12 +++++++++++ estate/views/res_users_views.xml | 17 +++++++++++++++ 6 files changed, 62 insertions(+), 6 deletions(-) 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 dd859a41a64..287f125b881 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,6 +11,7 @@ 'views/estate_property_type_menus.xml', 'views/estate_property_tag_views.xml', 'views/estate_property_tag_menus.xml', + 'views/res_users_views.xml', ], 'application': True, 'author': "Odoo", diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f0fa6a90dde..e7f91f94997 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -113,6 +113,13 @@ def _onchange_garden(self): self.garden_area = 10 self.garden_orientation = 'north' + @api.ondelete(at_uninstall=False) + def _unlink_if_property_new_or_cancelled(self): + for property in self: + if property.state not in ['new', 'cancelled']: + raise UserError( + f"You cannot delete a property in the '{property.state}' state.") + def action_set_sold(self): for property in self: if property.state != 'cancelled': diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 1c064f3fbbc..3887dfb95da 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ from odoo import api, models, fields +from odoo.exceptions import ValidationError class PropertyOffer(models.Model): @@ -52,17 +53,34 @@ def _inverse_date_deadline(self): delta = (offer.date_deadline - fields.Date.today()).days offer.validity = delta + @api.model + def create(self, vals): + for val in vals: + property_obj = self.env['estate.property'].browse( + val.get('property_id')) + property_offers = self.env['estate.property.offer'].search([ + ('property_id', '=', property_obj.id), + ]) + if property_offers: + lowest_offer = min(property_offers.mapped('price')) + if val.get('price', 0) <= lowest_offer: + raise ValidationError( + "The offer price must be higher than existing offers." + ) + property_obj.state = 'offer_received' + return super().create(vals) + def action_accept_offer(self): for offer in self: offer.status = "accepted" offer.property_id.state = "offer_accepted" - other_offers = self.env['estate.property.offer'].search([ - ('property_id', '=', offer.property_id.id), - ('id', '!=', offer.id), - ('status', '!=', 'refused') - ]) - other_offers.action_refuse_offer() + other_offers = self.env['estate.property.offer'].search([ + ('property_id', '=', offer.property_id.id), + ('id', '!=', offer.id), + ('status', '!=', 'refused') + ]) + other_offers.action_refuse_offer() return True diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..633f01c3512 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = "res.users" + + # Add a domain to the field so it only lists the available properties. + property_ids = fields.One2many( + comodel_name="estate.property", inverse_name="salesperson_id", + domain=[('state', 'in', ['new', 'offer_received']), + ('active', '=', True)], + ) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..4a1f4300259 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,17 @@ + + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + + + From 978fe3d0660f3f46a785ef1115dd829926d23b68 Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Thu, 23 Oct 2025 14:11:10 +0200 Subject: [PATCH 18/19] [ADD] estate_account: implement property sale invoicing --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 9 +++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 22 ++++++++++++++++++++++ 4 files changed, 33 insertions(+) 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_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..3770c60d46c --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,9 @@ +{ + 'name': 'estate_account', + 'summary': 'Accounting integration for Real Estate module.', + 'depends': ['estate', 'account'], + 'data': [], + 'application': True, + 'author': 'Odoo', + 'license': 'AGPL-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..1ed07c1388d --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,22 @@ +from odoo import models, Command + + +class Property(models.Model): + _inherit = "estate.property" + + def action_set_sold(self): + super().action_set_sold() + for property in self: + self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': property.buyer_id.id, + 'invoice_line_ids': [Command.create({ + 'name': property.name, + 'quantity': 1, + 'price_unit': 0.06 * property.selling_price, + }), Command.create({ + 'name': 'Administrative fees', + 'quantity': 1, + 'price_unit': 100.00, + })], + }) From 887b34e2704f663a83453f80eb0ffdd7c152507c Mon Sep 17 00:00:00 2001 From: Hashem Khaled Date: Thu, 23 Oct 2025 15:56:56 +0200 Subject: [PATCH 19/19] [IMP] estate: add kanban view for properties --- estate/views/estate_property_views.xml | 31 +++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d8c9acb06e7..671ef041a55 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ Properties estate.property - list,form + list,form,kanban {'search_default_available_properties': True} @@ -106,4 +106,33 @@ + + estate.property.kanban + estate.property + + + + + +
+ +
+ Expected Price: +
+
+ Best Offer: +
+
+ Selling Price: +
+
+ +
+
+
+
+
+
+
+