From 1c8c58e826bd347e4bd51c244e421df11c43d8d0 Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Mon, 16 Feb 2026 17:26:41 +0100
Subject: [PATCH 1/9] [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..6ec80af05ab
--- /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..4bf84facc1c
--- /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 6df9dac8363b816e8db83cb4ac741abd36202ffc Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Tue, 17 Feb 2026 10:10:42 +0100
Subject: [PATCH 2/9] [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_menus.xml | 2 +-
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 ++++++-
14 files changed, 160 insertions(+), 8 deletions(-)
create mode 100644 estate/models/estate_property_offer.py
create mode 100644 estate/models/estate_property_tag.py
create mode 100644 estate/models/estate_property_type.py
create mode 100644 estate/views/estate_property_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_menus.xml b/estate/views/estate_property_menus.xml
index 6ec80af05ab..2e9e9014207 100644
--- a/estate/views/estate_property_menus.xml
+++ b/estate/views/estate_property_menus.xml
@@ -1,7 +1,7 @@
-
\ 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 488d2a71104..6c04348f064 100644
--- a/estate/views/estate_property_offers_views.xml
+++ b/estate/views/estate_property_offers_views.xml
@@ -36,4 +36,4 @@
-
\ No newline at end of file
+
diff --git a/estate/views/estate_property_tag_menus.xml b/estate/views/estate_property_tag_menus.xml
index ba14c9674cd..611643450e8 100644
--- a/estate/views/estate_property_tag_menus.xml
+++ b/estate/views/estate_property_tag_menus.xml
@@ -1,4 +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
index b40144216b4..a712745e891 100644
--- a/estate/views/estate_property_tag_views.xml
+++ b/estate/views/estate_property_tag_views.xml
@@ -28,4 +28,4 @@
-
\ 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 f542ad3e23c..4adc63fef76 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 3200d8c5155..bef427c01a1 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 7b44b2e0a03c589c904ae94be6d529433db90ebd Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Tue, 17 Feb 2026 15:07:41 +0100
Subject: [PATCH 8/9] [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 3cd43d149c262f889ec74a1e0cf42cbd07c038f0 Mon Sep 17 00:00:00 2001
From: karimgamaleldin
Date: Tue, 17 Feb 2026 15:18:06 +0100
Subject: [PATCH 9/9] [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'