From 15bd461c8141b3646c6c8f985819584941efae8b Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Mon, 16 Feb 2026 17:26:41 +0100
Subject: [PATCH 01/16] [ADD] estate: property model with fields, views, and
menu access
- Add estate_property model with fields
- Add active field with default True
- Set date_availability default to 3 months from today
- Create list, form, and search views for estate.property
- Add search filters and group by postcode
- Configure access rights in ir.model.access.csv
---
estate/__init__.py | 1 +
estate/__manifest__.py | 15 +++++
estate/models/__init__.py | 2 +
estate/models/estate_property.py | 45 +++++++++++++
estate/security/ir.model.access.csv | 3 +
estate/views/estate_property_menus.xml | 9 +++
estate/views/estate_property_views.xml | 92 ++++++++++++++++++++++++++
7 files changed, 167 insertions(+)
create mode 100644 estate/__init__.py
create mode 100644 estate/__manifest__.py
create mode 100644 estate/models/__init__.py
create mode 100644 estate/models/estate_property.py
create mode 100644 estate/security/ir.model.access.csv
create mode 100644 estate/views/estate_property_menus.xml
create mode 100644 estate/views/estate_property_views.xml
diff --git a/estate/__init__.py b/estate/__init__.py
new file mode 100644
index 00000000000..9a7e03eded3
--- /dev/null
+++ b/estate/__init__.py
@@ -0,0 +1 @@
+from . import models
\ No newline at end of file
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
new file mode 100644
index 00000000000..8f013a95959
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,15 @@
+
+{
+ 'name': 'estate',
+ 'depends': [
+ 'base',
+ ],
+ "data": [
+ "security/ir.model.access.csv",
+ "views/estate_property_views.xml",
+ # "views/estate_property_type_views.xml",
+ "views/estate_property_menus.xml",
+ # "views/estate_property_type_menus.xml",
+ ],
+ 'application': True,
+}
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..8e433062639
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,2 @@
+from . import estate_property
+# from . import estate_property_type
\ 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..9a62783aa54
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,45 @@
+from odoo import fields, models
+
+def get_date_in_3_months():
+ '''
+ This function calculates the date that is three months from the current date.
+
+ returns:
+ A date object representing the date that is three months from today.
+ '''
+ today_data = fields.Date.today()
+ three_months_later = fields.Date.add(today_data, months=3)
+ return three_months_later
+
+class EstateProperty(models.Model):
+ _name = "estate.property"
+ _description = "Estate Property Model"
+
+ name = fields.Char(required=True)
+ description = fields.Text()
+ postcode = fields.Char()
+ date_availability = fields.Date(copy=False, default=get_date_in_3_months())
+ expected_price = fields.Float(required=True)
+ selling_price = fields.Float(readonly=True, copy=False)
+ 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([
+ ('north', 'North'),
+ ('south', 'South'),
+ ('east', 'East'),
+ ('west', 'West'),
+ ])
+ state = fields.Selection([
+ ('new', 'New'),
+ ('offer_received', 'Offer Received'),
+ ('offer_accepted', 'Offer Accepted'),
+ ('sold', 'Sold'),
+ ('canceled', 'Canceled'),
+ ], default="new", required=True, copy=False)
+ active = fields.Boolean(default=True)
+
+ property_type_id = fields.Many2one("estate.property.type", string="Property Type")
\ 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..db554fd92fd
--- /dev/null
+++ b/estate/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+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
\ No newline at end of file
diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml
new file mode 100644
index 00000000000..c4614c18783
--- /dev/null
+++ b/estate/views/estate_property_menus.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ 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..758e092cffb
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,92 @@
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Properties
+ estate.property
+ list,form
+
+
+ Define a new estate property to sell or rent.
+
+ You can specify the expected price and the selling/renting price, the buyer/tenant, the salesperson, and the status of the property.
+
+
+
+
\ No newline at end of file
From 8fa7278f53150741fd41e35465c73c8493fc4fe9 Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Tue, 17 Feb 2026 10:10:42 +0100
Subject: [PATCH 02/16] [IMP] estate: property types, tags, and offers
* Add estate.property.type model with form view and Settings menu
* Add estate.property.tag model with form view and Settings menu
* Add estate.property.offer model with form/list views for tracking offers
* Update estate.property model with:
- property_type_id: Many2one relation to property types
- tag_ids: Many2many relation to property tags
- offer_ids: One2many relation to offers
- buyer_id and salesperson_id fields
* Update property form view to display tags, type, offers, buyer, and salesperson
* Reorganize menus: Advertisements and Settings submenus
* Add security access rules for all new models
---
estate/__manifest__.py | 7 ++--
estate/models/__init__.py | 4 ++-
estate/models/estate_property.py | 8 +++--
estate/models/estate_property_offer.py | 10 ++++++
estate/models/estate_property_tag.py | 8 +++++
estate/models/estate_property_type.py | 8 +++++
estate/security/ir.model.access.csv | 4 ++-
estate/views/estate_property_offers_views.xml | 33 +++++++++++++++++++
estate/views/estate_property_tag_menus.xml | 4 +++
estate/views/estate_property_tag_views.xml | 31 +++++++++++++++++
estate/views/estate_property_type_menus.xml | 6 ++++
estate/views/estate_property_type_views.xml | 31 +++++++++++++++++
estate/views/estate_property_views.xml | 12 ++++++-
13 files changed, 159 insertions(+), 7 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_offers_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 8f013a95959..fdea66cd571 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -7,9 +7,12 @@
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
- # "views/estate_property_type_views.xml",
+ "views/estate_property_type_views.xml",
+ "views/estate_property_tag_views.xml",
+ "views/estate_property_offers_views.xml",
"views/estate_property_menus.xml",
- # "views/estate_property_type_menus.xml",
+ "views/estate_property_type_menus.xml",
+ "views/estate_property_tag_menus.xml",
],
'application': True,
}
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
index 8e433062639..09b2099fe84 100644
--- a/estate/models/__init__.py
+++ b/estate/models/__init__.py
@@ -1,2 +1,4 @@
from . import estate_property
-# from . import estate_property_type
\ No newline at end of file
+from . import estate_property_type
+from . import estate_property_tag
+from . import estate_property_offer
\ No newline at end of file
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 9a62783aa54..dd06aefaaa8 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,6 +1,6 @@
from odoo import fields, models
-def get_date_in_3_months():
+def get_date_in_3_months() -> fields.Date:
'''
This function calculates the date that is three months from the current date.
@@ -42,4 +42,8 @@ class EstateProperty(models.Model):
], default="new", required=True, copy=False)
active = fields.Boolean(default=True)
- property_type_id = fields.Many2one("estate.property.type", string="Property Type")
\ No newline at end of file
+ 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", string="Offers")
\ No newline at end of file
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..8caf5d371be
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,10 @@
+from odoo import models, fields
+
+class EstatePropertyOffer(models.Model):
+ _name = 'estate.property.offer'
+ _description = 'Estate Property Offer'
+
+ price = fields.Float(string='Price')
+ status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', copy=False)
+ partner_id = fields.Many2one('res.partner', string='Partner')
+ property_id = fields.Many2one('estate.property', string='Property')
\ 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..e63d6d7a781
--- /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 = "Estate Property Tag Model"
+
+ name = fields.Char(required=True)
+
\ No newline at end of file
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 00000000000..228eb2b0e30
--- /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 = "Estate Property Type Model"
+
+ name = fields.Char(required=True)
+
\ No newline at end of file
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
index db554fd92fd..c79331f2f1c 100644
--- a/estate/security/ir.model.access.csv
+++ b/estate/security/ir.model.access.csv
@@ -1,3 +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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml
new file mode 100644
index 00000000000..7d332d91e7c
--- /dev/null
+++ b/estate/views/estate_property_offers_views.xml
@@ -0,0 +1,33 @@
+
+
+
+ estate.property.offer.form
+ estate.property.offer
+
+
+
+
+
+
+
+ estate.property.offer.list
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_tag_menus.xml b/estate/views/estate_property_tag_menus.xml
new file mode 100644
index 00000000000..4869a0384ed
--- /dev/null
+++ b/estate/views/estate_property_tag_menus.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
new file mode 100644
index 00000000000..b40144216b4
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,31 @@
+
+
+
+ estate.property.tag.form
+ estate.property.tag
+
+
+
+
+
+
+
+
+ Property Tags
+ estate.property.tag
+ list,form
+
+
+ Define a new estate property tag.
+
+ You can specify the name of each property tag.
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_type_menus.xml b/estate/views/estate_property_type_menus.xml
new file mode 100644
index 00000000000..80513ee0adb
--- /dev/null
+++ b/estate/views/estate_property_type_menus.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..474ad7ef0fb
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,31 @@
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
+
+
+
+ Property Types
+ estate.property.type
+ list,form
+
+
+ Define a new estate property type.
+
+ You can specify the name and description of each property type.
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 758e092cffb..5246797285b 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -30,8 +30,10 @@
+
+
@@ -51,7 +53,15 @@
-
+
+
+
+
+
+
+
+
+
From fa5ac28485af584e0a0fa7fcbcce6b746e09d8c5 Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Tue, 17 Feb 2026 11:13:05 +0100
Subject: [PATCH 03/16] [IMP] estate: computed fields and onchange methods
* estate.property:
- Add total_area computed field (living_area + garden_area)
- Add best_offer computed field (max of offer prices)
- Add onchange method for garden field to auto-populate garden_area and orientation
* estate.property.offer:
- Add validity field (days, default 7)
- Add date_deadline computed field based on create_date + validity
- Add inverse method to update validity when deadline is changed
* Update views to display new computed fields (total_area, best_offer)
* Update offer views to include validity and date_deadline fields
---
estate/models/estate_property.py | 31 +++++++++++++++++--
estate/models/estate_property_offer.py | 19 ++++++++++--
estate/views/estate_property_offers_views.xml | 7 +++--
estate/views/estate_property_views.xml | 2 ++
4 files changed, 53 insertions(+), 6 deletions(-)
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index dd06aefaaa8..f33b93983c6 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,4 +1,4 @@
-from odoo import fields, models
+from odoo import api, fields, models
def get_date_in_3_months() -> fields.Date:
'''
@@ -41,9 +41,36 @@ class EstateProperty(models.Model):
('canceled', 'Canceled'),
], default="new", required=True, copy=False)
active = fields.Boolean(default=True)
+
+ total_area = fields.Integer(compute="_compute_total_area")
+ best_offer = fields.Float(compute="_compute_best_offer", string="Best Offer")
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", string="Offers")
\ No newline at end of file
+ offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
+
+ @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_offer(self):
+ for record in self:
+ # If the property has offers, the best offer is the maximum of the expected prices of the offers. Otherwise, it is 0.
+ if record.offer_ids:
+ record.best_offer = max(record.offer_ids.mapped("price"))
+ else:
+ record.best_offer = 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
\ No newline at end of file
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 8caf5d371be..769f32d30bf 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 models, fields, api
class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
@@ -7,4 +7,19 @@ class EstatePropertyOffer(models.Model):
price = fields.Float(string='Price')
status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', copy=False)
partner_id = fields.Many2one('res.partner', string='Partner')
- property_id = fields.Many2one('estate.property', string='Property')
\ No newline at end of file
+ property_id = fields.Many2one('estate.property', string='Property')
+ validity = fields.Integer(string='Validity (days)', default=7)
+ date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline')
+
+ @api.depends('validity', 'create_date')
+ def _compute_date_deadline(self):
+ for offer in self:
+ start = fields.Date.to_date(offer.create_date) or fields.Date.today()
+ offer.date_deadline = fields.Date.add(start, days=offer.validity)
+
+ def _inverse_date_deadline(self):
+ for offer in self:
+ start = fields.Date.to_date(offer.create_date) or fields.Date.today()
+ if offer.date_deadline and start:
+ delta = offer.date_deadline - start
+ offer.validity = delta.days
\ No newline at end of file
diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml
index 7d332d91e7c..32e643a4a32 100644
--- a/estate/views/estate_property_offers_views.xml
+++ b/estate/views/estate_property_offers_views.xml
@@ -8,8 +8,10 @@
-
+
+
+
@@ -23,8 +25,9 @@
-
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 5246797285b..e843946ba8b 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -39,6 +39,7 @@
+
@@ -53,6 +54,7 @@
+
From 06ea4370c815a73e3df85ae3295977e5bf421a91 Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Tue, 17 Feb 2026 11:44:04 +0100
Subject: [PATCH 04/16] [IMP] estate: action methods and offer management
buttons
* estate.property:
- Add action_set_sold() method to mark property as sold
- Add action_cancel() method to cancel property
- Add validation: prevent selling canceled properties
- Add validation: prevent canceling sold properties
* estate.property.offer:
- Add accept_offer() method: sets status to accepted, updates selling price,
property state, and buyer
- Add refuse_offer() method: sets status to refused, updates property state
* Update views:
- Add "Mark as Sold" and "Cancel" buttons to property form header
- Add accept/refuse buttons (icons) to offer list view
---
estate/models/estate_property.py | 19 ++++++++++++++++++-
estate/models/estate_property_offer.py | 15 ++++++++++++++-
estate/views/estate_property_offers_views.xml | 3 +++
estate/views/estate_property_views.xml | 4 ++++
4 files changed, 39 insertions(+), 2 deletions(-)
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index f33b93983c6..c90795bb704 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,4 +1,5 @@
from odoo import api, fields, models
+from odoo.exceptions import UserError
def get_date_in_3_months() -> fields.Date:
'''
@@ -51,6 +52,7 @@ class EstateProperty(models.Model):
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
+
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
@@ -73,4 +75,19 @@ def _onchange_garden(self):
self.garden_orientation = 'north'
else:
self.garden_area = 0
- self.garden_orientation = False
\ No newline at end of file
+ self.garden_orientation = False
+
+
+ def action_set_sold(self):
+ for record in self:
+ if record.state == "canceled":
+ raise UserError("Canceled properties cannot be sold.")
+ record.state = "sold"
+ return True
+
+ def action_cancel(self):
+ for record in self:
+ if record.state == "sold":
+ raise UserError("Sold properties cannot be canceled.")
+ record.state = "canceled"
+ return True
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 769f32d30bf..922414d7acd 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -22,4 +22,17 @@ def _inverse_date_deadline(self):
start = fields.Date.to_date(offer.create_date) or fields.Date.today()
if offer.date_deadline and start:
delta = offer.date_deadline - start
- offer.validity = delta.days
\ No newline at end of file
+ offer.validity = delta.days
+
+ def accept_offer(self):
+ for offer in self:
+ offer.status = 'accepted'
+ offer.property_id.selling_price = offer.price
+ offer.property_id.state = 'offer_accepted'
+ offer.property_id.buyer_id = offer.partner_id
+
+ def refuse_offer(self):
+ for offer in self:
+ offer.status = 'refused'
+ if offer.property_id.state != 'offer_accepted':
+ offer.property_id.state = 'offer_received'
\ No newline at end of file
diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml
index 32e643a4a32..488d2a71104 100644
--- a/estate/views/estate_property_offers_views.xml
+++ b/estate/views/estate_property_offers_views.xml
@@ -28,6 +28,9 @@
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index e843946ba8b..9b9150bb075 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -26,6 +26,10 @@
estate.property
-
\ No newline at end of file
+
diff --git a/estate/views/estate_property_type_menus.xml b/estate/views/estate_property_type_menus.xml
index 80513ee0adb..776d56c1be8 100644
--- a/estate/views/estate_property_type_menus.xml
+++ b/estate/views/estate_property_type_menus.xml
@@ -3,4 +3,4 @@
-
\ No newline at end of file
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
index 474ad7ef0fb..d77279a9d2d 100644
--- a/estate/views/estate_property_type_views.xml
+++ b/estate/views/estate_property_type_views.xml
@@ -28,4 +28,4 @@
-
\ No newline at end of file
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index d475943c404..252a8e6c220 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -105,4 +105,4 @@
-
\ No newline at end of file
+
From 0b3bd093c4f94a9ef6559ded96dec9ac1380893f Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Tue, 17 Feb 2026 15:07:41 +0100
Subject: [PATCH 08/16] [CLN] estate: rename constraints to avoid ambiguity
The Python constraint method `_check_selling_price` and SQL constraints
shared similar names or were ambiguous, making it difficult to distinguish
between them and understand their specific purposes.
This commit renames:
- The Python constraint to `_check_selling_price_vs_expected_price` to
better describe the 90% validation logic.
- The SQL constraints to `_check_expected_price_pos` and
`_check_selling_price_pos` to explicitly indicate they enforce positive
values.
---
estate/models/estate_property.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 5fbf5f819eb..813e18825a3 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -54,12 +54,12 @@ class EstateProperty(models.Model):
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
- _check_expected_price = models.Constraint(
+ _check_expected_price_pos = models.Constraint(
'CHECK(expected_price > 0)',
'The expected price must be strictly positive.'
)
- _check_selling_price = models.Constraint(
+ _check_selling_price_pos = models.Constraint(
'CHECK(selling_price >= 0)',
'The selling price cannot be negative.'
)
@@ -89,7 +89,7 @@ def _onchange_garden(self):
@api.constrains("selling_price", "expected_price")
- def _check_selling_price(self):
+ def _check_selling_price_gt_90(self):
for record in self:
if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0:
raise UserError("The selling price cannot be less than 90% of the expected price.")
From 17fdb7506b66a54c7c30310eb2bc5472f17b1aaf Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Tue, 17 Feb 2026 15:18:06 +0100
Subject: [PATCH 09/16] [FIX] estate: prevent accepting multiple offers
Previously, it was possible to accept multiple offers for the same
property.
This commit adds a validation check in the `accept_offer` method. Now,
if a user attempts to accept an offer for a property that already has
an accepted offer, a UserError is raised to block the action.
---
estate/models/estate_property.py | 2 +-
estate/models/estate_property_offer.py | 11 +++++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 813e18825a3..3299db37d64 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -89,7 +89,7 @@ def _onchange_garden(self):
@api.constrains("selling_price", "expected_price")
- def _check_selling_price_gt_90(self):
+ def _check_selling_price_vs_expected_price(self):
for record in self:
if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0:
raise UserError("The selling price cannot be less than 90% of the expected price.")
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index f202b5676db..cc408b94d70 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -1,4 +1,5 @@
from odoo import models, fields, api
+from odoo.exceptions import UserError
class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
@@ -31,11 +32,21 @@ def _inverse_date_deadline(self):
def accept_offer(self):
for offer in self:
+ # Check no other offer has been accepted for the same property
+ if offer.property_id.offer_ids.filtered(lambda o: o.status == 'accepted'):
+ raise UserError('Another offer has already been accepted for this property.')
+
+ # Accept the offer
offer.status = 'accepted'
offer.property_id.selling_price = offer.price
offer.property_id.state = 'offer_accepted'
offer.property_id.buyer_id = offer.partner_id
+ # Refuse all other offers for the same property
+ other_offers = offer.property_id.offer_ids.filtered(lambda o: o.id != offer.id and o.status != 'refused')
+ for other_offer in other_offers:
+ other_offer.status = 'refused'
+
def refuse_offer(self):
for offer in self:
offer.status = 'refused'
From 6f3ba595d5bada2de5d00d044a2f342bb9e4e747 Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Wed, 18 Feb 2026 10:23:52 +0100
Subject: [PATCH 10/16] [IMP] estate: add UI sprinkles and improve usability
Enhance the user experience for the real estate module by adding
advanced search capabilities and quick navigation.
- Add One2many field property_ids to the estate.property.type model and display it in the estate.property.type form view
- Add a statusbar widget to display the state for each estate.property
- Add default model ordering for estate.property (ID desc), estate.property.offer (price desc), estate.property.tag (name) and estate.property.type (sequence, name)
- Add manual ordering for estate.property.type by adding a sequence field and use the handle widget to allow the user to reorder
- Add color field to estate.property.tag model and the color picker widget to choose the color
- Add invisible attribute to Sold and Cancel header buttons depending on the property state
- Add constraint to prevent adding offers when property state is offer_accepted, sold or cancelled
- Add invisible attribute to Accept and Refuse buttons when the offer status is already set
- Add invisible attribute to garden_area and garden_orientation fields when garden is False on estate.property
- Add editable attribute to estate.property.offer and estate.property.tag list views
- Add optional and invisible by default attribute to date_availability field on the estate.property list view
- Add green decoration for properties with an offer received, green and bold for offer accepted and muted for sold on the estate.property list view
- Add red decoration for refused offers and green for accepted offers and add invisible attribute to the status field on the estate.property.offer list view
- Add available filter selected by default on the estate.property search view via search_default_available context
- Add filter_domain on living_area search field to include properties with an area equal to or greater than the given value
- Add property_type_id as a stored related field on estate.property.offer pointing to property_id.property_type_id
- Add offer_ids One2many field on estate.property.type as the inverse of property_type_id on estate.property.offer
- Add offer_count computed field on estate.property.type that counts the number of offers using offer_ids
- Add stat button on estate.property.type form view displaying offer_count with a cash icon, hidden when no offers exist
- Add estate.property.offer action with a domain filtering offers by property_type_id equal to active_id and link it to the stat button
---
estate/__manifest__.py | 2 ++
estate/models/estate_property_offer.py | 7 ++++
estate/models/estate_property_tag.py | 2 ++
estate/models/estate_property_type.py | 16 +++++++--
estate/views/estate_property_offers_views.xml | 12 +++----
estate/views/estate_property_tag_views.xml | 20 ++++++++---
estate/views/estate_property_type_views.xml | 35 +++++++++++++++++--
estate/views/estate_property_views.xml | 29 +++++++++------
8 files changed, 98 insertions(+), 25 deletions(-)
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index fdea66cd571..ab0d414a156 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -4,6 +4,8 @@
'depends': [
'base',
],
+ "author": "Karim Gamaleldin",
+ "license": "LGPL-3",
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index cc408b94d70..97fa1f1fb0f 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -4,6 +4,7 @@
class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = 'Estate Property Offer'
+ _order = 'price desc'
price = fields.Float(string='Price')
status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', copy=False)
@@ -11,6 +12,12 @@ class EstatePropertyOffer(models.Model):
property_id = fields.Many2one('estate.property', string='Property')
validity = fields.Integer(string='Validity (days)', default=7)
date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline')
+
+ property_type_id = fields.Many2one(
+ 'estate.property.type',
+ related='property_id.property_type_id',
+ store=True
+ )
_check_offer_price = models.Constraint(
'CHECK(price > 0)',
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index 8c67b36bf00..8d8bc54592d 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -3,8 +3,10 @@
class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Estate Property Tag Model"
+ _order = "name"
name = fields.Char(required=True)
+ color = fields.Integer(default=1)
_check_property_tag_name_unique = models.Constraint(
'UNIQUE(name)',
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 4d6b8385ae5..de66c2ed357 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -1,12 +1,24 @@
-from odoo import fields, models
+from odoo import fields, models, api
class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Estate Property Type Model"
+ _order = "sequence, name"
name = fields.Char(required=True)
-
+ sequence = fields.Integer(default=1, help="Used to order the property types. Lower numbers are displayed first.")
+
+ property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
+ offers_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers")
+
+ offers_count = fields.Integer(string="Offers Count", compute="_compute_offers_count")
+
_check_property_type_name_unique = models.Constraint(
'UNIQUE(name)',
'The name of the property type must be unique.'
)
+
+ @api.depends("offers_ids")
+ def _compute_offers_count(self):
+ for record in self:
+ record.offers_count = len(record.offers_ids)
\ No newline at end of file
diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml
index 6c04348f064..cea3c589946 100644
--- a/estate/views/estate_property_offers_views.xml
+++ b/estate/views/estate_property_offers_views.xml
@@ -23,17 +23,17 @@
estate.property.offer.listestate.property.offer
-
+
-
-
-
+
+
-
-
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
index a712745e891..1a04997cd88 100644
--- a/estate/views/estate_property_tag_views.xml
+++ b/estate/views/estate_property_tag_views.xml
@@ -6,15 +6,27 @@