From 9f7d1ac4cf7dfcc4edf115f0329578ac92223ba9 Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Mon, 15 Dec 2025 12:42:18 +0100 Subject: [PATCH 1/8] [ADD] estate: introduce structure for estate module Chapter 2 --- estate/__init__.py | 0 estate/__manifest__.py | 12 ++++++++++++ 2 files changed, 12 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..563a780ada6 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "estate", + "version": "1.0", + "depends": ["base"], + "author": "Odoo S.A.", + "category": "Real Estate", + "description": """ + Description text + """, + "application": True, + "installable": True, +} From d5962a2d98c808c8b700655a82a576aaf4c7a72a Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Mon, 15 Dec 2025 14:42:00 +0100 Subject: [PATCH 2/8] [IMP] estate: Add estate_property model Chapter 3 --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 30 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..f265f910d73 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,28 @@ +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = "estate_property" + _description = "Estate Property data 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( + [ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + help="Indicates the direction the garden is facing with respect to the house", + ) From f2a9463613e6fee0b56cce6dded8616e801bcb04 Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Mon, 15 Dec 2025 15:02:28 +0100 Subject: [PATCH 3/8] [IMP] estate: Add access right rules to estate module data Chapter 4 --- estate/__manifest__.py | 3 +++ estate/data/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 estate/data/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 563a780ada6..7ff58d1fd49 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -9,4 +9,7 @@ """, "application": True, "installable": True, + "data": [ + "data/ir.model.access.csv", + ], } diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv new file mode 100644 index 00000000000..775f77c8384 --- /dev/null +++ b/estate/data/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,0,0,0 From 709dd423e7de92dd621893f5761e7735b7f05f6d Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Tue, 16 Dec 2025 13:30:50 +0100 Subject: [PATCH 4/8] [IMP] estate: Implemet menus and new model customizations Chapter 5 --- estate/__manifest__.py | 2 ++ estate/data/ir.model.access.csv | 2 +- estate/models/estate_property.py | 25 ++++++++++++++++++++++--- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 5 files changed, 41 insertions(+), 4 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 7ff58d1fd49..9657f67ef53 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,5 +11,7 @@ "installable": True, "data": [ "data/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_menus.xml", ], } diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv index 775f77c8384..98f4671fb0d 100644 --- a/estate/data/ir.model.access.csv +++ b/estate/data/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,0,0,0 +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f265f910d73..89a7a406146 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,3 +1,6 @@ +from datetime import datetime + +from dateutil.relativedelta import relativedelta from odoo import fields, models @@ -7,11 +10,14 @@ class EstateProperty(models.Model): name = fields.Char(required=True) description = fields.Text() + active = fields.Boolean(default=True) postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date( + copy=False, default=datetime.now() + relativedelta(month=3) + ) expected_price = fields.Float(required=True) - selling_price = fields.Float() - 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() @@ -26,3 +32,16 @@ class EstateProperty(models.Model): ], help="Indicates the direction the garden is facing with respect to the house", ) + state = fields.Selection( + [ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + required=True, + copy=False, + default="new", + help="State of the proprty listing lifecycle", + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..aa77f21e0a1 --- /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..48d321320b6 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + estate property action + estate_property + list,form + + From 43dc52af42803a8002173cfb39515adc59f3f691 Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Tue, 16 Dec 2025 16:14:26 +0100 Subject: [PATCH 5/8] [IMP] estate: Add list, form, search views to real estate module Chapter 6 --- estate/views/estate_property_views.xml | 82 ++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 48d321320b6..696a8a49273 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,86 @@ estate_property list,form + + + estate_property.list + estate_property + + + + + + + + + + + + + + + estate_property.form + estate_property + +
+ + +

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate_property.search + estate_property + + + + + + + + + + + + + + + + + + From a2ae3a1638819bf5a92c5016e8d98c7d9fb03aa1 Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Wed, 17 Dec 2025 13:41:01 +0100 Subject: [PATCH 6/8] [IMP] estate: Add relations between real estate property models Chapter 7 This adds tags to properties, lists offers made (whether refued or accepted) along with the salesperson and buyer in "Other info" tab --- estate/__manifest__.py | 3 +++ estate/data/ir.model.access.csv | 3 +++ estate/models/__init__.py | 3 ++- estate/models/estate_property.py | 9 ++++++++- estate/models/estate_property_offer.py | 13 ++++++++++++ estate/models/estate_property_tag.py | 8 ++++++++ estate/models/estate_property_type.py | 8 ++++++++ estate/views/estate_menus.xml | 8 +++++++- estate/views/estate_property_offer.xml | 15 ++++++++++++++ estate/views/estate_property_tag.xml | 8 ++++++++ estate/views/estate_property_type.xml | 8 ++++++++ estate/views/estate_property_views.xml | 28 +++++++++++++++++++------- 12 files changed, 104 insertions(+), 10 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.xml create mode 100644 estate/views/estate_property_tag.xml create mode 100644 estate/views/estate_property_type.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9657f67ef53..220af570e82 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,6 +12,9 @@ "data": [ "data/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_type.xml", + "views/estate_property_tag.xml", + "views/estate_property_offer.xml", "views/estate_menus.xml", ], } diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv index 98f4671fb0d..0c0b62b7fee 100644 --- a/estate/data/ir.model.access.csv +++ b/estate/data/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..aae114c2505 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,2 @@ -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 89a7a406146..6a32bbbf71c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,7 +5,7 @@ class EstateProperty(models.Model): - _name = "estate_property" + _name = "estate.property" _description = "Estate Property data model" name = fields.Char(required=True) @@ -45,3 +45,10 @@ class EstateProperty(models.Model): default="new", help="State of the proprty listing lifecycle", ) + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + buyer_id = fields.Many2one("res.partner", string="Buyer") + salesperson_id = fields.Many2one( + "res.users", string="Salesman", default=lambda self: self.env.user + ) + tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..5e6b38c3386 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class EstatePropertyOffer(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", required=True, string="Sales") + property_id = fields.Many2one("estate.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..9f0eae86da7 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real estate property tags" + + 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..d67f8160b3b --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate property type" + + name = fields.Char(required=True) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index aa77f21e0a1..ec92a50c678 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,8 +1,14 @@ - + + + + + diff --git a/estate/views/estate_property_offer.xml b/estate/views/estate_property_offer.xml new file mode 100644 index 00000000000..1355b30ce22 --- /dev/null +++ b/estate/views/estate_property_offer.xml @@ -0,0 +1,15 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + diff --git a/estate/views/estate_property_tag.xml b/estate/views/estate_property_tag.xml new file mode 100644 index 00000000000..b10720e7969 --- /dev/null +++ b/estate/views/estate_property_tag.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_type.xml b/estate/views/estate_property_type.xml new file mode 100644 index 00000000000..45653e20ef3 --- /dev/null +++ b/estate/views/estate_property_type.xml @@ -0,0 +1,8 @@ + + + + Property Types + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 696a8a49273..e34c9ba3698 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -2,13 +2,13 @@ estate property action - estate_property + estate.property list,form - estate_property.list - estate_property + estate.property.list + estate.property @@ -23,8 +23,8 @@ - estate_property.form - estate_property + estate.property.form + estate.property
@@ -33,8 +33,11 @@ + + + @@ -58,6 +61,17 @@ + + + + + + + + + + +
@@ -65,8 +79,8 @@
- estate_property.search - estate_property + estate.property.search + estate.property From bb41efbedc62efd10f0ca523b931aa4206531f11 Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Fri, 19 Dec 2025 11:36:16 +0100 Subject: [PATCH 7/8] [IMP] estate: introduce computed fields and use onchange effects for updates Chapter 8 --- estate/models/estate_property.py | 19 ++++++++++++++++++- estate/models/estate_property_offer.py | 21 ++++++++++++++++++++- estate/views/estate_property_offer.xml | 2 ++ estate/views/estate_property_views.xml | 2 ++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 6a32bbbf71c..ed21a478865 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,7 +1,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -52,3 +52,20 @@ class EstateProperty(models.Model): ) tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") offer_ids = fields.One2many("estate.property.offer", "property_id") + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Float(compute="_compute_best_price") + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped("price")) + + @api.onchange("garden") + def _onchange_garden(self): + self.garden_area = 10 if self.garden else 0 + self.garden_orientation = "north" if self.garden else None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 5e6b38c3386..e027c9a41fb 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +import datetime + +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -11,3 +13,20 @@ class EstatePropertyOffer(models.Model): ) partner_id = fields.Many2one("res.partner", required=True, string="Sales") property_id = fields.Many2one("estate.property", required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_date_deadline", inverse="_inverse_date_deadline" + ) + + @api.depends("create_date", "validity") + def _compute_date_deadline(self): + for record in self: + record.date_deadline = ( + record.create_date.date() + if record.create_date + else datetime.date.today() + ) + datetime.timedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days diff --git a/estate/views/estate_property_offer.xml b/estate/views/estate_property_offer.xml index 1355b30ce22..c8363e2ff4f 100644 --- a/estate/views/estate_property_offer.xml +++ b/estate/views/estate_property_offer.xml @@ -7,6 +7,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e34c9ba3698..90054f7d818 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -43,6 +43,7 @@ + @@ -59,6 +60,7 @@ + From 97cb70182c7a3526a48b0a46b577de5af352e25c Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Fri, 19 Dec 2025 15:49:21 +0100 Subject: [PATCH 8/8] [IMP] estate: implement actions on properties and offers Chapter 9 This introduces marking a property as "Sold" or "Cancelled". It also allows for easier offers management from the list view and it gets reflected automatically on the property --- estate/models/estate_property.py | 26 ++++++++++++++++++++++++-- estate/models/estate_property_offer.py | 11 +++++++++++ estate/views/estate_property_offer.xml | 2 ++ estate/views/estate_property_views.xml | 5 +++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ed21a478865..13964c6e3d5 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,7 +1,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta -from odoo import api, fields, models +from odoo import api, exceptions, fields, models class EstateProperty(models.Model): @@ -63,9 +63,31 @@ def _compute_total_area(self): @api.depends("offer_ids.price") def _compute_best_price(self): for record in self: - record.best_price = max(record.offer_ids.mapped("price")) + record.best_price = ( + max(record.offer_ids.mapped("price")) if record.offer_ids else 0 + ) @api.onchange("garden") def _onchange_garden(self): self.garden_area = 10 if self.garden else 0 self.garden_orientation = "north" if self.garden else None + + def sold_estate_property(self): + for record in self: + if record.state != "cancelled": + record.state = "sold" + else: + raise exceptions.UserError("can't sell a cancelled property") + + def cancel_estate_property(self): + for record in self: + if record.state != "sold": + record.state = "cancelled" + else: + raise exceptions.UserError("can't cancel a sold property") + + def reject_other_offers(self, winning_offer): + for record in self: + for offer in record.offer_ids: + if offer != winning_offer: + offer.status = "refused" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index e027c9a41fb..3c255d91602 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -30,3 +30,14 @@ def _compute_date_deadline(self): def _inverse_date_deadline(self): for record in self: record.validity = (record.date_deadline - record.create_date.date()).days + + def action_confirm(self): + for record in self: + record.status = "accepted" + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + record.property_id.reject_other_offers(record) + + def action_cancel(self): + for record in self: + record.status = "refused" diff --git a/estate/views/estate_property_offer.xml b/estate/views/estate_property_offer.xml index c8363e2ff4f..ca6780f7d18 100644 --- a/estate/views/estate_property_offer.xml +++ b/estate/views/estate_property_offer.xml @@ -10,6 +10,8 @@ +