From 6b97ae5aa33dc1d319032aafb2082a7cd64388a4 Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Mon, 11 Aug 2025 17:57:49 +0530
Subject: [PATCH 01/14] [ADD] estate: created estate advertisement module
. Introduce a new module for real estate advertisements
. Ensure it appears in the Apps list for installation
. Provide basic description and dependencies for recognition
. Allow it to be installed as an application (empty shell for now)
---
estate/__init__.py | 0
estate/__manifest__.py | 5 +++++
2 files changed, 5 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..97c1160f50c
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,5 @@
+{
+ 'name': 'Real Estate',
+ 'application': True,
+ 'installable': True
+}
\ No newline at end of file
From 42f72a9f4e087af47ba3578d28427b096715ee4b Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Tue, 12 Aug 2025 10:02:25 +0530
Subject: [PATCH 02/14] [ADD] estate: defined estate property model
. Create a model for properties with basic info, availability, and pricing.
. Include structural details and amenities like garden size and orientation.
. Require key fields such as name and expected price.
. Ensure data is saved properly via the ORM.
---
estate/__init__.py | 1 +
estate/models/__init__.py | 1 +
estate/models/estate_property.py | 22 ++++++++++++++++++++++
3 files changed, 24 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..c34b2814e39
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,22 @@
+from odoo import models, fields
+
+class EstateModel(models.Model):
+ _name = "estate.property"
+ _description = "Real Estate Advertisement 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(
+ string='Type',
+ selection=[('north', 'North'),('south', 'South'),('east', 'East'),('west', 'West')],
+ )
\ No newline at end of file
From ae1096610cf38c5d54651a188191277f0d271dec Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Tue, 12 Aug 2025 16:30:03 +0530
Subject: [PATCH 03/14] [ADD] estate: added security access configuration
. Allow users to read, create, update, and delete property records.
. Eliminate warnings related to missing access rules for the property model.
. Implement default security settings to grant standard users proper data access.
---
estate/__manifest__.py | 6 +++++-
estate/security/ir.model.access.csv | 2 ++
2 files changed, 7 insertions(+), 1 deletion(-)
create mode 100644 estate/security/ir.model.access.csv
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index 97c1160f50c..d2644952990 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -1,5 +1,9 @@
{
'name': 'Real Estate',
'application': True,
- 'installable': True
+ 'installable': True,
+ 'category': 'Real Estate/Brokerage',
+ 'data': [
+ 'security/ir.model.access.csv'
+ ]
}
\ 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..fd62fe01321
--- /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_user,model_estate_property,model_estate_property,base.group_user,1,1,1,1
\ No newline at end of file
From 25213feb8a768b2d4eabcbb1236c05002da62329 Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Mon, 18 Aug 2025 19:10:54 +0530
Subject: [PATCH 04/14] [ADD] estate: added menus options and new fields
. Add root, first-level, and action menus for estate properties.
. Create list and form views for property records.
. Make selling price read-only and exclude it and availability date from duplicates.
. Set default bedrooms to 2 and availability date to 3 months ahead.
. Add active field (default True) and state field with key property statuses.
---
estate/__manifest__.py | 4 +++-
estate/models/estate_property.py | 25 +++++++++++++++----
estate/views/estate_menus.xml | 9 +++++++
estate/views/estate_property_views.xml | 33 ++++++++++++++++++++++++++
4 files changed, 65 insertions(+), 6 deletions(-)
create mode 100644 estate/views/estate_menus.xml
create mode 100644 estate/views/estate_property_views.xml
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index d2644952990..e9f14da3333 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -4,6 +4,8 @@
'installable': True,
'category': 'Real Estate/Brokerage',
'data': [
- 'security/ir.model.access.csv'
+ 'security/ir.model.access.csv',
+ 'views/estate_property_views.xml',
+ 'views/estate_menus.xml'
]
}
\ No newline at end of file
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index c34b2814e39..89dd45c45c0 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,4 +1,5 @@
from odoo import models, fields
+from datetime import timedelta
class EstateModel(models.Model):
_name = "estate.property"
@@ -7,16 +8,30 @@ class EstateModel(models.Model):
name = fields.Char(required=True)
description = fields.Text()
postcode = fields.Char()
- date_availability = fields.Date()
+ date_availability = fields.Date(
+ copy=False,
+ default= fields.Date.today() + timedelta(days=90)
+ )
+ active= fields.Boolean(default=True)
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()
garden = fields.Boolean()
garden_area = fields.Integer()
+ state= fields.Selection(
+ string='state',
+ default="New",
+ required=True,
+ copy=False,
+ selection=[('New', 'New'),('Offer Received', 'Offer Received'),('Offer Accepted', 'Offer Accepted'),('Sold', 'Sold'), ('Cancelled', 'Cancelled'),
+ ]
+ )
garden_orientation = fields.Selection(
- string='Type',
- selection=[('north', 'North'),('south', 'South'),('east', 'East'),('west', 'West')],
+ string='Garden Orientation',
+ selection=[('north', 'North'),('south', 'South'),('east', 'East'),('west', 'West'),
+ ],
+ help="Select the direction the garden faces"
)
\ No newline at end of file
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
new file mode 100644
index 00000000000..952789b6d35
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
new file mode 100644
index 00000000000..22c5141320c
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ Properties
+ estate.property
+ list,form
+
+
+ Create the first property
+
+
+
+
+
+
\ No newline at end of file
From 8afd3882d705b43261aeff75164100e32ee71dea Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Tue, 19 Aug 2025 14:35:38 +0530
Subject: [PATCH 05/14] [ADD] estate: customized form and list view and added
filtering options
. Add a list view displaying key property fields like name, price, and bedrooms.
. Organize the form view using groups and notebook tabs for clarity.
. Implement a search view with filters on name and availability.
. Add a filter to display only available properties (New, Offer Received).
. Provide a shortcut to group the property list by postcode.
---
estate/views/estate_property_views.xml | 72 +++++++++++++++++++++++---
1 file changed, 66 insertions(+), 6 deletions(-)
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 22c5141320c..299f8e34337 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -12,22 +12,82 @@
-
+
+ Estate Properties list
estate.property
+
+
+
+
+
+
+
-
- estate.property.form
+
+
+ Estate Property Form
estate.property
- -->
+
+
+
+
+ Estate Property Search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From dec6f49dfd3afc071ed46f06d702a5f772c98afc Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Tue, 19 Aug 2025 19:06:29 +0530
Subject: [PATCH 06/14] [ADD] estate: added relational models and property
types, tags, buyers and offers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
. Add property types with menus, actions, and views; link properties to types.
. Include buyer and salesperson info in a new tab; default salesperson to current user, don’t copy buyer.
. Implement property tags with menus, actions, and views; support multiple tags per property.
. Introduce property offers with price, status, and partner details.
. Link offers to properties and display them within the property form.
---
estate/__manifest__.py | 5 +++-
estate/models/__init__.py | 5 +++-
estate/models/estate_property.py | 7 ++++-
estate/models/estate_property_offer.py | 13 +++++++++
estate/models/estate_property_tag.py | 7 +++++
estate/models/estate_property_type.py | 7 +++++
estate/security/ir.model.access.csv | 6 ++++-
estate/views/estate_menus.xml | 14 +++++++---
estate/views/estate_property_offer_views.xml | 28 ++++++++++++++++++++
estate/views/estate_property_tag_views.xml | 15 +++++++++++
estate/views/estate_property_type_views.xml | 15 +++++++++++
estate/views/estate_property_views.xml | 20 ++++++++++++--
12 files changed, 133 insertions(+), 9 deletions(-)
create mode 100644 estate/models/estate_property_offer.py
create mode 100644 estate/models/estate_property_tag.py
create mode 100644 estate/models/estate_property_type.py
create mode 100644 estate/views/estate_property_offer_views.xml
create mode 100644 estate/views/estate_property_tag_views.xml
create mode 100644 estate/views/estate_property_type_views.xml
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index e9f14da3333..6e6f82223e8 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -6,6 +6,9 @@
'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
- 'views/estate_menus.xml'
+ 'views/estate_property_type_views.xml',
+ 'views/estate_property_tag_views.xml',
+ 'views/estate_property_offer_views.xml',
+ 'views/estate_menus.xml',
]
}
\ No newline at end of file
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
index f4c8fd6db6d..09b2099fe84 100644
--- a/estate/models/__init__.py
+++ b/estate/models/__init__.py
@@ -1 +1,4 @@
-from . import estate_property
\ No newline at end of file
+from . import estate_property
+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 89dd45c45c0..49160338415 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -34,4 +34,9 @@ class EstateModel(models.Model):
selection=[('north', 'North'),('south', 'South'),('east', 'East'),('west', 'West'),
],
help="Select the direction the garden faces"
- )
\ No newline at end of file
+ )
+ property_type_id= fields.Many2one("estate.property.type")
+ buyer= fields.Many2one("res.partner")
+ salesman= fields.Many2one("res.users")
+ tag_ids= fields.Many2many("estate.property.tag")
+ offer_ids= fields.One2many("estate.property.offer", inverse_name="property_id")
\ 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..870c435a3cd
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,13 @@
+from odoo import models, fields
+
+class EstateOfferModel(models.Model):
+ _name = "estate.property.offer"
+ _description = "Real Estate Property Offer Model"
+
+ price = fields.Float()
+ status = fields.Selection([
+ ('Accepted', 'Accepted'),
+ ('Refused', 'Refused')
+ ], copy=False)
+ partner_id= fields.Many2one('res.partner', required=True)
+ property_id= fields.Many2one('estate.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..6b6b7eaf5c6
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,7 @@
+from odoo import models, fields
+
+class EstateTagModel(models.Model):
+ _name = "estate.property.tag"
+ _description = "Real 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..50af47a3615
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,7 @@
+from odoo import models, fields
+
+class EstateTypeModel(models.Model):
+ _name = "estate.property.type"
+ _description = "Real 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 fd62fe01321..47dae2d3b06 100644
--- a/estate/security/ir.model.access.csv
+++ b/estate/security/ir.model.access.csv
@@ -1,2 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_estate_property_user,model_estate_property,model_estate_property,base.group_user,1,1,1,1
\ No newline at end of file
+access_estate_property_user,model_estate_property,model_estate_property,base.group_user,1,1,1,1
+access_estate_property_type_user,model_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
+access_estate_property_tag_user,model_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
+access_estate_property_offer_user,model_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 952789b6d35..5a7f66b8478 100644
--- a/estate/views/estate_menus.xml
+++ b/estate/views/estate_menus.xml
@@ -1,9 +1,17 @@
-
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
new file mode 100644
index 00000000000..c3399946af1
--- /dev/null
+++ b/estate/views/estate_property_offer_views.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ Property Offers
+ estate.property.offer
+ list,form
+
+
+ Create the first property Type
+
+
+
+
+
+
+ Estate Properties Offer list
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
\ 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..f3e32dd5030
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ Property Tag
+ estate.property.tag
+ list,form
+
+
+ Create the first property Tag
+
+
+
+
+
\ 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..c4f069e082f
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ Property Types
+ estate.property.type
+ list,form
+
+
+ Create the first 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 299f8e34337..6eccb81ad0a 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -36,12 +36,18 @@
@@ -82,7 +98,7 @@
-
+
From 830bfc6efc4e860e1084df1cea9a783a2c6a1ce9 Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Wed, 20 Aug 2025 18:22:50 +0530
Subject: [PATCH 07/14] [ADD] estate: added auto computed fields to property
and offer views
. Property page displays total area by summing living space and garden.
. Best offer price shown on property page for easy comparison.
. Offer page includes a deadline calculated from validity days.
. Editing deadline automatically updates validity days.
. Garden details auto-fill when enabled and clear when removed; UI reflects these behaviors.
---
estate/__init__.py | 2 +-
estate/__manifest__.py | 26 ++++----
estate/models/__init__.py | 2 +-
estate/models/estate_property.py | 65 +++++++++++++++-----
estate/models/estate_property_offer.py | 32 +++++++---
estate/models/estate_property_tag.py | 3 +-
estate/models/estate_property_type.py | 3 +-
estate/views/estate_menus.xml | 14 +++--
estate/views/estate_property_offer_views.xml | 2 +
estate/views/estate_property_views.xml | 7 ++-
10 files changed, 108 insertions(+), 48 deletions(-)
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 6e6f82223e8..717f226463a 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -1,14 +1,14 @@
{
- 'name': 'Real Estate',
- 'application': True,
- 'installable': True,
- 'category': 'Real Estate/Brokerage',
- 'data': [
- 'security/ir.model.access.csv',
- 'views/estate_property_views.xml',
- 'views/estate_property_type_views.xml',
- 'views/estate_property_tag_views.xml',
- 'views/estate_property_offer_views.xml',
- 'views/estate_menus.xml',
- ]
-}
\ No newline at end of file
+ "name": "Real Estate",
+ "application": True,
+ "installable": True,
+ "category": "Real Estate/Brokerage",
+ "data": [
+ "security/ir.model.access.csv",
+ "views/estate_property_views.xml",
+ "views/estate_property_type_views.xml",
+ "views/estate_property_tag_views.xml",
+ "views/estate_property_offer_views.xml",
+ "views/estate_menus.xml",
+ ],
+}
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
index 09b2099fe84..2f1821a39c1 100644
--- a/estate/models/__init__.py
+++ b/estate/models/__init__.py
@@ -1,4 +1,4 @@
from . import estate_property
from . import estate_property_type
from . import estate_property_tag
-from . import estate_property_offer
\ No newline at end of file
+from . import estate_property_offer
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 49160338415..70fe2d1cf9e 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,6 +1,7 @@
-from odoo import models, fields
+from odoo import models, fields, api
from datetime import timedelta
+
class EstateModel(models.Model):
_name = "estate.property"
_description = "Real Estate Advertisement Model"
@@ -9,10 +10,9 @@ class EstateModel(models.Model):
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
- copy=False,
- default= fields.Date.today() + timedelta(days=90)
- )
- active= fields.Boolean(default=True)
+ copy=False, default=fields.Date.today() + timedelta(days=90)
+ )
+ active = fields.Boolean(default=True)
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
@@ -21,22 +21,53 @@ class EstateModel(models.Model):
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
- state= fields.Selection(
- string='state',
+ state = fields.Selection(
+ string="state",
default="New",
required=True,
copy=False,
- selection=[('New', 'New'),('Offer Received', 'Offer Received'),('Offer Accepted', 'Offer Accepted'),('Sold', 'Sold'), ('Cancelled', 'Cancelled'),
- ]
+ selection=[
+ ("New", "New"),
+ ("Offer Received", "Offer Received"),
+ ("Offer Accepted", "Offer Accepted"),
+ ("Sold", "Sold"),
+ ("Cancelled", "Cancelled"),
+ ],
)
garden_orientation = fields.Selection(
- string='Garden Orientation',
- selection=[('north', 'North'),('south', 'South'),('east', 'East'),('west', 'West'),
+ string="Garden Orientation",
+ selection=[
+ ("north", "North"),
+ ("south", "South"),
+ ("east", "East"),
+ ("west", "West"),
],
- help="Select the direction the garden faces"
+ help="Select the direction the garden faces",
)
- property_type_id= fields.Many2one("estate.property.type")
- buyer= fields.Many2one("res.partner")
- salesman= fields.Many2one("res.users")
- tag_ids= fields.Many2many("estate.property.tag")
- offer_ids= fields.One2many("estate.property.offer", inverse_name="property_id")
\ No newline at end of file
+ property_type_id = fields.Many2one("estate.property.type")
+ buyer = fields.Many2one("res.partner")
+ salesman = fields.Many2one("res.users")
+ tag_ids = fields.Many2many("estate.property.tag")
+ offer_ids = fields.One2many("estate.property.offer", inverse_name="property_id")
+ total_area = fields.Float(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.living_area + record.garden_area
+
+ @api.depends("offer_ids.price")
+ def _compute_best_price(self):
+ for record in self:
+ record.best_price = max(record.offer_ids.mapped("price"), default=0.0)
+
+ # the onchange method will access self which will hold the current form value object not all the value objects
+ @api.onchange("garden")
+ def _set_garden_default_values(self):
+ if self.garden:
+ self.garden_area = 10
+ self.garden_orientation = "north"
+ else:
+ self.garden_area = 0
+ self.garden_orientation = ""
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 870c435a3cd..9c2dd881c91 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -1,13 +1,31 @@
-from odoo import models, fields
+from odoo import models, fields, api
+from datetime import timedelta
+
class EstateOfferModel(models.Model):
_name = "estate.property.offer"
_description = "Real Estate Property Offer Model"
price = fields.Float()
- status = fields.Selection([
- ('Accepted', 'Accepted'),
- ('Refused', 'Refused')
- ], copy=False)
- partner_id= fields.Many2one('res.partner', required=True)
- property_id= fields.Many2one('estate.property', required=True)
\ No newline at end of file
+ status = fields.Selection(
+ [("Accepted", "Accepted"), ("Refused", "Refused")], copy=False
+ )
+ partner_id = fields.Many2one("res.partner", required=True)
+ property_id = fields.Many2one("estate.property", required=True)
+ validity = fields.Integer(default=7)
+ date_deadline = fields.Date(default=fields.Date.today() + timedelta(days=7))
+
+ @api.depends("validity", "create_date")
+ def _compute_date_deadline(self):
+ for record in self:
+ record.date_deadline = fields.Date.add(
+ (record.create_date or fields.date.today()), days=record.validity
+ )
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ record.validity = (
+ (record.date_deadline - record.create_date.date()).days
+ if record.date_deadline
+ else 0
+ )
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index 6b6b7eaf5c6..293a0f7841b 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -1,7 +1,8 @@
from odoo import models, fields
+
class EstateTagModel(models.Model):
_name = "estate.property.tag"
_description = "Real Estate Property Tag Model"
- name = fields.Char(required=True)
\ No newline at end of file
+ name = fields.Char(required=True)
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 50af47a3615..d04b5ec5f0e 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -1,7 +1,8 @@
from odoo import models, fields
+
class EstateTypeModel(models.Model):
_name = "estate.property.type"
_description = "Real Estate Property Type Model"
- name = fields.Char(required=True)
\ No newline at end of file
+ name = fields.Char(required=True)
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
index 5a7f66b8478..c37ed208a70 100644
--- a/estate/views/estate_menus.xml
+++ b/estate/views/estate_menus.xml
@@ -4,14 +4,18 @@
-
+
\ No newline at end of file
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
index c3399946af1..9c9a1c59128 100644
--- a/estate/views/estate_property_offer_views.xml
+++ b/estate/views/estate_property_offer_views.xml
@@ -21,6 +21,8 @@
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 6eccb81ad0a..6d34295abd4 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -52,6 +52,7 @@
+
@@ -67,11 +68,13 @@
+
-
-
+
+
+
From d02d21f9e12abd773e7ce338be7b0ac56d363191 Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Thu, 21 Aug 2025 16:45:12 +0530
Subject: [PATCH 08/14] [ADD] estate: added Cancel and Sold buttons to
property, Accept/Refuse to offer
- Property page now has Cancel and Sold buttons for quick status updates
- Cancelled properties cannot be marked sold and vice versa
- Offer page includes Accept and Refuse buttons for better offer handling
- Accepting an offer sets the buyer and selling price automatically
- Ensures only one offer can be accepted per property
---
estate/__manifest__.py | 1 +
estate/models/estate_property.py | 16 +++++++--
estate/models/estate_property_offer.py | 23 ++++++++++++-
estate/models/estate_property_tag.py | 2 +-
estate/models/estate_property_type.py | 2 +-
estate/security/ir.model.access.csv | 1 -
estate/views/estate_menus.xml | 17 +++-------
estate/views/estate_property_offer_views.xml | 11 +++----
estate/views/estate_property_tag_views.xml | 3 +-
estate/views/estate_property_type_views.xml | 3 +-
estate/views/estate_property_views.xml | 34 +++++++++-----------
11 files changed, 65 insertions(+), 48 deletions(-)
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index 717f226463a..66912de2ca6 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -11,4 +11,5 @@
"views/estate_property_offer_views.xml",
"views/estate_menus.xml",
],
+ "license": "LGPL-3"
}
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 70fe2d1cf9e..1757d27c377 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,5 +1,6 @@
-from odoo import models, fields, api
from datetime import timedelta
+from odoo import api, fields, models
+from odoo.exceptions import UserError
class EstateModel(models.Model):
@@ -45,7 +46,7 @@ class EstateModel(models.Model):
help="Select the direction the garden faces",
)
property_type_id = fields.Many2one("estate.property.type")
- buyer = fields.Many2one("res.partner")
+ buyer = fields.Many2one("res.partner", readonly=True)
salesman = fields.Many2one("res.users")
tag_ids = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", inverse_name="property_id")
@@ -62,7 +63,6 @@ def _compute_best_price(self):
for record in self:
record.best_price = max(record.offer_ids.mapped("price"), default=0.0)
- # the onchange method will access self which will hold the current form value object not all the value objects
@api.onchange("garden")
def _set_garden_default_values(self):
if self.garden:
@@ -71,3 +71,13 @@ def _set_garden_default_values(self):
else:
self.garden_area = 0
self.garden_orientation = ""
+
+ def action_property_sold(self):
+ if self.state in ["Sold", "Cancelled"]:
+ raise UserError(f"Property is already {self.state}")
+ self.state = "Sold"
+
+ def action_property_cancelled(self):
+ if self.state in ["Sold", "Cancelled"]:
+ raise UserError(f"Property is already {self.state}")
+ self.state = "Cancelled"
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 9c2dd881c91..4178ceb51ef 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -1,5 +1,6 @@
-from odoo import models, fields, api
from datetime import timedelta
+from odoo import api, fields, models
+from odoo.exceptions import UserError
class EstateOfferModel(models.Model):
@@ -29,3 +30,23 @@ def _inverse_date_deadline(self):
if record.date_deadline
else 0
)
+
+ def action_offer_confirm(self):
+ for record in self:
+ if record.status in ["Accepted", "Refused"]:
+ raise UserError(f"Offer is already {record.status}")
+
+ for offer in record.property_id.offer_ids:
+ if offer.id == record.id:
+ record.status = "Accepted"
+ record.property_id.state = "Offer Accepted"
+ record.property_id.buyer = record.partner_id
+ record.property_id.selling_price = record.price
+ else:
+ offer.status = "Refused"
+
+ def action_offer_cancel(self):
+ for record in self:
+ if record.status in ["Accepted", "Refused"]:
+ raise UserError(f"Offer is already {record.status}")
+ record.status = "Refused"
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index 293a0f7841b..c8ce45dcb04 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -1,4 +1,4 @@
-from odoo import models, fields
+from odoo import fields, models
class EstateTagModel(models.Model):
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index d04b5ec5f0e..93d06b486e2 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -1,4 +1,4 @@
-from odoo import models, fields
+from odoo import fields, models
class EstateTypeModel(models.Model):
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
index 47dae2d3b06..30fc332afaf 100644
--- a/estate/security/ir.model.access.csv
+++ b/estate/security/ir.model.access.csv
@@ -3,4 +3,3 @@ access_estate_property_user,model_estate_property,model_estate_property,base.gro
access_estate_property_type_user,model_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag_user,model_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer_user,model_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 c37ed208a70..c510a6f31f4 100644
--- a/estate/views/estate_menus.xml
+++ b/estate/views/estate_menus.xml
@@ -2,20 +2,13 @@
-
\ No newline at end of file
+
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
index 9c9a1c59128..091b883bfad 100644
--- a/estate/views/estate_property_offer_views.xml
+++ b/estate/views/estate_property_offer_views.xml
@@ -11,8 +11,6 @@
-
-
Estate Properties Offer list
estate.property.offer
@@ -20,11 +18,12 @@
-
-
+
+
+
+
-
-
\ 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 f3e32dd5030..deb1b7aabe9 100644
--- a/estate/views/estate_property_tag_views.xml
+++ b/estate/views/estate_property_tag_views.xml
@@ -11,5 +11,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 c4f069e082f..de422dedf1a 100644
--- a/estate/views/estate_property_type_views.xml
+++ b/estate/views/estate_property_type_views.xml
@@ -11,5 +11,4 @@
-
-
\ No newline at end of file
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 6d34295abd4..3895af8a61d 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -11,8 +11,6 @@
-
-
Estate Properties list
estate.property
@@ -28,26 +26,29 @@
-
-
Estate Property Form
estate.property
-
-
Estate Property Search
estate.property
-
+
-
-
-
\ No newline at end of file
+
From 5782473e22f321ec2ee3a42a5ea960bfbdab6912 Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Fri, 22 Aug 2025 11:51:31 +0530
Subject: [PATCH 09/14] [ADD] estate : add data constraints from prices and
uniqueness
- Added SQL constraints for positive expected and selling prices, positive offer price, and unique tag/type names.
- Implemented Python constraint to ensure selling price is at least 90% of expected price.
- Used float_compare() and float_is_zero() for accurate float handling.
- Triggered constraint on changes to selling_price and expected_price fields.
---
estate/models/estate_property.py | 33 +++++++++++++++++++++++++-
estate/models/estate_property_offer.py | 8 +++++++
estate/models/estate_property_tag.py | 4 ++++
estate/models/estate_property_type.py | 8 +++++++
4 files changed, 52 insertions(+), 1 deletion(-)
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 1757d27c377..8f79785b0e2 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -1,6 +1,7 @@
from datetime import timedelta
from odoo import api, fields, models
-from odoo.exceptions import UserError
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools.float_utils import float_compare, float_is_zero
class EstateModel(models.Model):
@@ -53,6 +54,36 @@ class EstateModel(models.Model):
total_area = fields.Float(compute="_compute_total_area")
best_price = fields.Float(compute="_compute_best_price")
+ _sql_constraints = [
+ (
+ "check_property_expected_price",
+ "CHECK(expected_price > 0)",
+ "Property Expected Price must be a valid value",
+ ),
+ (
+ "check_property_selling_price",
+ "CHECK(selling_price >= 0)",
+ "Property Selling Price must be positive",
+ ),
+ ]
+
+ @api.constrains("selling_price", "expected_price")
+ def _check_selling_price_percentage(self):
+ for record in self:
+ if float_is_zero(record.selling_price, precision_rounding=0.01):
+ continue
+ if (
+ float_compare(
+ record.selling_price,
+ (record.expected_price * 0.9),
+ precision_rounding=0.01,
+ )
+ < 0
+ ):
+ raise ValidationError(
+ "Selling Price of the property can not be less than 90 percent of the expected Price"
+ )
+
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 4178ceb51ef..7873cd82f59 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -16,6 +16,14 @@ class EstateOfferModel(models.Model):
validity = fields.Integer(default=7)
date_deadline = fields.Date(default=fields.Date.today() + timedelta(days=7))
+ _sql_constraints = [
+ (
+ "check_property_offer_price",
+ "CHECK(price > 0)",
+ "Property Offer Price must be a valid value",
+ )
+ ]
+
@api.depends("validity", "create_date")
def _compute_date_deadline(self):
for record in self:
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index c8ce45dcb04..7144249d62d 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -6,3 +6,7 @@ class EstateTagModel(models.Model):
_description = "Real Estate Property Tag Model"
name = fields.Char(required=True)
+
+ _sql_constraints = [
+ ("check_property_tag_name", "UNIQUE(name)", "Property Tag Name must be unique")
+ ]
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 93d06b486e2..0346b1d5c96 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -6,3 +6,11 @@ class EstateTypeModel(models.Model):
_description = "Real Estate Property Type Model"
name = fields.Char(required=True)
+
+ _sql_constraints = [
+ (
+ "check_property_type_name",
+ "UNIQUE(name)",
+ "Property Type Name must be unique",
+ )
+ ]
From 1d2d7533b4e26a13415d267962cd7cdf868a8f00 Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Fri, 22 Aug 2025 15:53:52 +0530
Subject: [PATCH 10/14] [IMP] estate : add statusbar, helper text and refined
sold constrains
- status will not change to sold/cancelled unless an offer is accepted.
- helper text added to visibility field.
- state field is added at the header of form view.
---
estate/models/estate_property.py | 35 +++++++++++---------
estate/models/estate_property_offer.py | 30 +++++++++++------
estate/models/estate_property_tag.py | 1 +
estate/models/estate_property_type.py | 3 ++
estate/views/estate_property_offer_views.xml | 4 +--
estate/views/estate_property_type_views.xml | 32 ++++++++++++++++++
estate/views/estate_property_views.xml | 3 +-
7 files changed, 78 insertions(+), 30 deletions(-)
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 8f79785b0e2..45550d38046 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -7,6 +7,7 @@
class EstateModel(models.Model):
_name = "estate.property"
_description = "Real Estate Advertisement Model"
+ _order = "id desc"
name = fields.Char(required=True)
description = fields.Text()
@@ -25,15 +26,15 @@ class EstateModel(models.Model):
garden_area = fields.Integer()
state = fields.Selection(
string="state",
- default="New",
+ default="new",
required=True,
copy=False,
selection=[
- ("New", "New"),
- ("Offer Received", "Offer Received"),
- ("Offer Accepted", "Offer Accepted"),
- ("Sold", "Sold"),
- ("Cancelled", "Cancelled"),
+ ("new", "New"),
+ ("offer_received", "Offer Received"),
+ ("offer_accepted", "Offer Accepted"),
+ ("sold", "Sold"),
+ ("cancelled", "Cancelled"),
],
)
garden_orientation = fields.Selection(
@@ -51,8 +52,8 @@ class EstateModel(models.Model):
salesman = fields.Many2one("res.users")
tag_ids = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", inverse_name="property_id")
- total_area = fields.Float(compute="_compute_total_area")
- best_price = fields.Float(compute="_compute_best_price")
+ total_area = fields.Float(compute="_compute_total_area", store=True)
+ best_price = fields.Float(compute="_compute_best_price", store=True)
_sql_constraints = [
(
@@ -97,18 +98,20 @@ def _compute_best_price(self):
@api.onchange("garden")
def _set_garden_default_values(self):
if self.garden:
- self.garden_area = 10
- self.garden_orientation = "north"
+ self.write({"garden_area": 10, "garden_orientation": "north"})
else:
- self.garden_area = 0
- self.garden_orientation = ""
+ self.write({"garden_area": 0, "garden_orientation": ""})
def action_property_sold(self):
- if self.state in ["Sold", "Cancelled"]:
+ if self.state in ["sold", "cancelled"]:
raise UserError(f"Property is already {self.state}")
- self.state = "Sold"
+ if float_is_zero(self.selling_price, precision_rounding=0.01):
+ raise UserError(
+ "Atleast one offer must be accepted before selling the property"
+ )
+ self.state = "sold"
def action_property_cancelled(self):
- if self.state in ["Sold", "Cancelled"]:
+ if self.state in ["sold", "cancelled"]:
raise UserError(f"Property is already {self.state}")
- self.state = "Cancelled"
+ self.state = "cancelled"
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 7873cd82f59..7f60e621115 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -6,14 +6,18 @@
class EstateOfferModel(models.Model):
_name = "estate.property.offer"
_description = "Real Estate Property Offer Model"
+ _order = "price desc"
price = fields.Float()
status = fields.Selection(
- [("Accepted", "Accepted"), ("Refused", "Refused")], copy=False
+ [("accepted", "Accepted"), ("refused", "Refused")], copy=False
)
partner_id = fields.Many2one("res.partner", required=True)
property_id = fields.Many2one("estate.property", required=True)
- validity = fields.Integer(default=7)
+ validity = fields.Integer(
+ default=7,
+ help="offer validity period in days; the offer will be automatically refused when this expires.",
+ )
date_deadline = fields.Date(default=fields.Date.today() + timedelta(days=7))
_sql_constraints = [
@@ -41,20 +45,24 @@ def _inverse_date_deadline(self):
def action_offer_confirm(self):
for record in self:
- if record.status in ["Accepted", "Refused"]:
+ if record.status in ["accepted", "refused"]:
raise UserError(f"Offer is already {record.status}")
-
+ # breakpoint()
for offer in record.property_id.offer_ids:
if offer.id == record.id:
- record.status = "Accepted"
- record.property_id.state = "Offer Accepted"
- record.property_id.buyer = record.partner_id
- record.property_id.selling_price = record.price
+ record.status = "accepted"
+ record.property_id.write(
+ {
+ "state": "offer_accepted",
+ "buyer": record.partner_id,
+ "selling_price": record.price,
+ }
+ )
else:
- offer.status = "Refused"
+ offer.status = "refused"
def action_offer_cancel(self):
for record in self:
- if record.status in ["Accepted", "Refused"]:
+ if record.status in ["accepted", "refused"]:
raise UserError(f"Offer is already {record.status}")
- record.status = "Refused"
+ record.status = "refused"
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index 7144249d62d..d1a0c9ab011 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -4,6 +4,7 @@
class EstateTagModel(models.Model):
_name = "estate.property.tag"
_description = "Real Estate Property Tag Model"
+ _order = "name"
name = fields.Char(required=True)
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index 0346b1d5c96..c2df8fa7631 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -4,8 +4,11 @@
class EstateTypeModel(models.Model):
_name = "estate.property.type"
_description = "Real Estate Property Type Model"
+ _order = "name"
name = fields.Char(required=True)
+ property_ids = fields.One2many("estate.property", "property_type_id")
+ sequence = fields.Integer("Sequence", default=1, help="Used to change the sequence of Property Types")
_sql_constraints = [
(
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
index 091b883bfad..0f9df6bea38 100644
--- a/estate/views/estate_property_offer_views.xml
+++ b/estate/views/estate_property_offer_views.xml
@@ -20,8 +20,8 @@
-
-
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
index de422dedf1a..caa9d71a545 100644
--- a/estate/views/estate_property_type_views.xml
+++ b/estate/views/estate_property_type_views.xml
@@ -1,6 +1,17 @@
+
+
+ estate.property.type.view.list
+ estate.property.type
+
+
+
+
+
+
+
Property Types
estate.property.type
@@ -11,4 +22,25 @@
+
+ Properties by Types
+ estate.property.type
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 3895af8a61d..b59e92ee3df 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -34,6 +34,7 @@
@@ -98,7 +99,7 @@
-
+
From 9b50772e9bca5b34caa122babfe9e2ada98a3171 Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Thu, 28 Aug 2025 13:20:55 +0530
Subject: [PATCH 11/14] [IMP] estate: UI Enhancements, Ordering, Filters, and
Constraints
. Enhanced UI with inline list views, status bar widget, color picker for tags, and visual decorations.
. Improved form behavior by hiding garden fields conditionally and making availability date optional and hidden by default.
. Controlled editing by preventing direct modification of property types from property forms.
. Enabled manual ordering using sequence fields and set default/custom ordering across models.
. Restricted actions such as offer submission based on property state and adjusted button visibility accordingly.
. Refined views and filters with editable list views, default filter for available properties, improved living area search, and stat buttons with filtered results.
---
estate/__manifest__.py | 4 +-
estate/models/estate_property.py | 22 +++++---
estate/models/estate_property_offer.py | 45 ++++++++++------
estate/models/estate_property_tag.py | 1 +
estate/models/estate_property_type.py | 16 +++++-
estate/views/estate_menus.xml | 5 ++
estate/views/estate_property_offer_views.xml | 16 +++---
estate/views/estate_property_tag_views.xml | 15 +++++-
estate/views/estate_property_type_views.xml | 34 ++++++++----
estate/views/estate_property_views.xml | 54 ++++++++++++--------
10 files changed, 146 insertions(+), 66 deletions(-)
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index 66912de2ca6..e619262a5f6 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -6,10 +6,10 @@
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
+ "views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
- "views/estate_property_offer_views.xml",
"views/estate_menus.xml",
],
- "license": "LGPL-3"
+ "license": "LGPL-3",
}
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 45550d38046..903c970a6ea 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -13,7 +13,8 @@ class EstateModel(models.Model):
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
- copy=False, default=fields.Date.today() + timedelta(days=90)
+ copy=False,
+ default=fields.Date.today() + timedelta(days=90),
)
active = fields.Boolean(default=True)
expected_price = fields.Float(required=True)
@@ -25,7 +26,7 @@ class EstateModel(models.Model):
garden = fields.Boolean()
garden_area = fields.Integer()
state = fields.Selection(
- string="state",
+ string="State",
default="new",
required=True,
copy=False,
@@ -53,7 +54,7 @@ class EstateModel(models.Model):
tag_ids = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", inverse_name="property_id")
total_area = fields.Float(compute="_compute_total_area", store=True)
- best_price = fields.Float(compute="_compute_best_price", store=True)
+ best_price = fields.Float(compute="_compute_best_price", store=True, default=0.0)
_sql_constraints = [
(
@@ -93,7 +94,13 @@ 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"), default=0.0)
+ self.write(
+ {
+ "best_price": max(record.offer_ids.mapped("price"), default=0.0),
+ }
+ )
+ if record.best_price > 0.0:
+ record.state = "offer_received"
@api.onchange("garden")
def _set_garden_default_values(self):
@@ -103,8 +110,6 @@ def _set_garden_default_values(self):
self.write({"garden_area": 0, "garden_orientation": ""})
def action_property_sold(self):
- if self.state in ["sold", "cancelled"]:
- raise UserError(f"Property is already {self.state}")
if float_is_zero(self.selling_price, precision_rounding=0.01):
raise UserError(
"Atleast one offer must be accepted before selling the property"
@@ -112,6 +117,7 @@ def action_property_sold(self):
self.state = "sold"
def action_property_cancelled(self):
- if self.state in ["sold", "cancelled"]:
- raise UserError(f"Property is already {self.state}")
self.state = "cancelled"
+ for offer in self.offer_ids:
+ if not offer.status:
+ offer.status = "refused"
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 7f60e621115..abc2dcd4d13 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -14,11 +14,18 @@ class EstateOfferModel(models.Model):
)
partner_id = fields.Many2one("res.partner", required=True)
property_id = fields.Many2one("estate.property", required=True)
+ property_type_id = fields.Many2one(
+ "estate.property.type", related="property_id.property_type_id", required=True
+ )
validity = fields.Integer(
default=7,
help="offer validity period in days; the offer will be automatically refused when this expires.",
)
- date_deadline = fields.Date(default=fields.Date.today() + timedelta(days=7))
+ date_deadline = fields.Date(
+ compute="_compute_date_deadline",
+ inverse="_inverse_date_deadline",
+ default=fields.Date.today() + timedelta(days=7),
+ )
_sql_constraints = [
(
@@ -44,22 +51,26 @@ def _inverse_date_deadline(self):
)
def action_offer_confirm(self):
- for record in self:
- if record.status in ["accepted", "refused"]:
- raise UserError(f"Offer is already {record.status}")
- # breakpoint()
- for offer in record.property_id.offer_ids:
- if offer.id == record.id:
- record.status = "accepted"
- record.property_id.write(
- {
- "state": "offer_accepted",
- "buyer": record.partner_id,
- "selling_price": record.price,
- }
- )
- else:
- offer.status = "refused"
+ if self.status in ["accepted", "refused"]:
+ raise UserError(f"Offer is already {self.status}")
+
+ # update the current selected offer
+ self.status = "accepted"
+ self.property_id.write(
+ {
+ "state": "offer_accepted",
+ "buyer": self.partner_id,
+ "selling_price": self.price,
+ }
+ )
+
+ # update the rest offers status to "refused"
+ refused_offers = self.property_id.offer_ids - self
+ refused_offers.write(
+ {
+ "status": "refused",
+ }
+ )
def action_offer_cancel(self):
for record in self:
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
index d1a0c9ab011..110ac90b1eb 100644
--- a/estate/models/estate_property_tag.py
+++ b/estate/models/estate_property_tag.py
@@ -7,6 +7,7 @@ class EstateTagModel(models.Model):
_order = "name"
name = fields.Char(required=True)
+ color = fields.Integer(string="Color", default=0)
_sql_constraints = [
("check_property_tag_name", "UNIQUE(name)", "Property Tag Name must be unique")
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
index c2df8fa7631..c09765cd154 100644
--- a/estate/models/estate_property_type.py
+++ b/estate/models/estate_property_type.py
@@ -1,4 +1,4 @@
-from odoo import fields, models
+from odoo import api, fields, models
class EstateTypeModel(models.Model):
@@ -8,7 +8,15 @@ class EstateTypeModel(models.Model):
name = fields.Char(required=True)
property_ids = fields.One2many("estate.property", "property_type_id")
- sequence = fields.Integer("Sequence", default=1, help="Used to change the sequence of Property Types")
+ sequence = fields.Integer(
+ "Sequence", default=1, help="Used to change the sequence of Property Types"
+ )
+ offer_ids = fields.One2many(
+ "estate.property.offer", inverse_name="property_type_id"
+ )
+ offer_count = fields.Integer(
+ default=0, readonly=True, compute="_compute_offers_by_type"
+ )
_sql_constraints = [
(
@@ -17,3 +25,7 @@ class EstateTypeModel(models.Model):
"Property Type Name must be unique",
)
]
+
+ @api.depends("offer_ids")
+ def _compute_offers_by_type(self):
+ self.offer_count = len(self.offer_ids)
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
index c510a6f31f4..d9a3d90cc56 100644
--- a/estate/views/estate_menus.xml
+++ b/estate/views/estate_menus.xml
@@ -1,14 +1,19 @@
+
+
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
index 0f9df6bea38..ae20496f91e 100644
--- a/estate/views/estate_property_offer_views.xml
+++ b/estate/views/estate_property_offer_views.xml
@@ -1,29 +1,33 @@
-
+
+
Property Offers
estate.property.offer
- list,form
+ list
+ [('property_type_id', '=', active_id)]
Create the first property Type
+
Estate Properties Offer list
estate.property.offer
-
+
-
-
-
+
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
index deb1b7aabe9..b2f1bdcba4d 100644
--- a/estate/views/estate_property_tag_views.xml
+++ b/estate/views/estate_property_tag_views.xml
@@ -1,14 +1,27 @@
+
Property Tag
estate.property.tag
- list,form
+ list
Create the first property Tag
+
+
+ Property Tag List
+ estate.property.tag
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
index caa9d71a545..812de78d91e 100644
--- a/estate/views/estate_property_type_views.xml
+++ b/estate/views/estate_property_type_views.xml
@@ -2,16 +2,6 @@
-
- estate.property.type.view.list
- estate.property.type
-
-
-
-
-
-
-
Property Types
estate.property.type
@@ -22,12 +12,35 @@
+
+
+ Estate Property Type List
+ estate.property.type
+
+
+
+
+
+
+
+
Properties by Types
estate.property.type
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index b59e92ee3df..85f3b0f06a7 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -1,63 +1,73 @@
+
Properties
estate.property
list,form
+ {'search_default_available': True}
Create the first property
+
Estate Properties list
estate.property
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
Estate Property Form
estate.property
+
Estate Property Search
estate.property
@@ -97,13 +110,14 @@
-
+
-
+
+
From 1065ee5c9b285c573dc3b58d32d9fff8cb81f4ee Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Fri, 29 Aug 2025 10:40:08 +0530
Subject: [PATCH 12/14] [ADD] estate : extend CRUD operations and add property
view under user
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
. Restrict deletion of properties unless their state is New or Cancelled
. Automatically update property state to Offer Received when a new offer is created
. Prevent creation of offers lower than existing highest offer
. Display salesperson’s available properties directly on the user form
. Introduce a new tab in user settings to manage linked properties
---
estate/__manifest__.py | 1 +
estate/models/__init__.py | 1 +
estate/models/estate_property.py | 9 +++++++++
estate/models/estate_property_offer.py | 14 +++++++++++++-
estate/models/res_users.py | 11 +++++++++++
estate/views/res_user_view.xml | 18 ++++++++++++++++++
6 files changed, 53 insertions(+), 1 deletion(-)
create mode 100644 estate/models/res_users.py
create mode 100644 estate/views/res_user_view.xml
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index e619262a5f6..4f9d9c36fbe 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -10,6 +10,7 @@
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_menus.xml",
+ "views/res_user_view.xml",
],
"license": "LGPL-3",
}
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 903c970a6ea..6b9d109b4ac 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -121,3 +121,12 @@ def action_property_cancelled(self):
for offer in self.offer_ids:
if not offer.status:
offer.status = "refused"
+
+ @api.ondelete(at_uninstall=False)
+ def _check_before_deleting_record(self):
+ # more than one record can be present in recordset
+ for record in self:
+ if record.state in ["offer_received", "offer_accepted", "sold"]:
+ raise UserError(
+ "Properties with received offers, accepted offers, or that have been sold cannot be deleted."
+ )
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index abc2dcd4d13..3a3c250d1aa 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -58,7 +58,6 @@ def action_offer_confirm(self):
self.status = "accepted"
self.property_id.write(
{
- "state": "offer_accepted",
"buyer": self.partner_id,
"selling_price": self.price,
}
@@ -77,3 +76,16 @@ def action_offer_cancel(self):
if record.status in ["accepted", "refused"]:
raise UserError(f"Offer is already {record.status}")
record.status = "refused"
+
+ @api.model_create_multi
+ def create(self, vals):
+ for record in vals:
+ property = self.env["estate.property"].browse(record["property_id"])
+
+ if record.get("price") < property.best_price:
+ raise UserError(
+ "The offers price cannot be lower than the existing offers"
+ )
+
+ property.state = "offer_received"
+ return super().create(vals)
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 00000000000..54da05a07a7
--- /dev/null
+++ b/estate/models/res_users.py
@@ -0,0 +1,11 @@
+from odoo import fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = "res.users"
+
+ property_ids = fields.One2many(
+ "estate.property",
+ inverse_name="salesman",
+ domain="['|', ('state','=','new'), ('state', '=', 'offer_received')]",
+ )
diff --git a/estate/views/res_user_view.xml b/estate/views/res_user_view.xml
new file mode 100644
index 00000000000..197bd8d6440
--- /dev/null
+++ b/estate/views/res_user_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Inherited User View
+ res.users
+
+
+
+
+
+
+
+
+
+
+
From cd2b9e2d6ff414f7fb6836547fb4e2a320e13d0e Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Fri, 29 Aug 2025 17:55:51 +0530
Subject: [PATCH 13/14] [ADD] estate: auto invoice creation on property sale
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
. Added a new linking module estate_account depending on estate and account.
. Invoicing activates only when both modules are installed.
. When a property is sold, an invoice with two lines (6% commission + 100 fixed fee) is created for the buyer.
. Keeps estate module independent while enabling optional accounting integration following Odoo’s modular design.
---
estate/models/estate_property.py | 44 ++++++++++++------------
estate_account/__init__.py | 1 +
estate_account/__manifest__.py | 8 +++++
estate_account/models/__init__.py | 1 +
estate_account/models/estate_property.py | 37 ++++++++++++++++++++
5 files changed, 69 insertions(+), 22 deletions(-)
create mode 100644 estate_account/__init__.py
create mode 100644 estate_account/__manifest__.py
create mode 100644 estate_account/models/__init__.py
create mode 100644 estate_account/models/estate_property.py
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
index 6b9d109b4ac..a2ec9dba998 100644
--- a/estate/models/estate_property.py
+++ b/estate/models/estate_property.py
@@ -9,22 +9,37 @@ class EstateModel(models.Model):
_description = "Real Estate Advertisement Model"
_order = "id desc"
- name = fields.Char(required=True)
- description = fields.Text()
- postcode = fields.Char()
+ active = fields.Boolean(default=True)
+ bedrooms = fields.Integer(default=2)
+ best_price = fields.Float(compute="_compute_best_price", store=True, default=0.0)
+ buyer = fields.Many2one("res.partner", readonly=True)
date_availability = fields.Date(
copy=False,
default=fields.Date.today() + timedelta(days=90),
)
- active = fields.Boolean(default=True)
+ description = fields.Text()
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(
+ string="Garden Orientation",
+ selection=[
+ ("north", "North"),
+ ("south", "South"),
+ ("east", "East"),
+ ("west", "West"),
+ ],
+ help="Select the direction the garden faces",
+ )
+ living_area = fields.Integer()
+ name = fields.Char(required=True)
+ offer_ids = fields.One2many("estate.property.offer", inverse_name="property_id")
+ postcode = fields.Char()
+ property_type_id = fields.Many2one("estate.property.type")
+ salesman = fields.Many2one("res.users")
+ selling_price = fields.Float(readonly=True, copy=False)
state = fields.Selection(
string="State",
default="new",
@@ -38,23 +53,8 @@ class EstateModel(models.Model):
("cancelled", "Cancelled"),
],
)
- garden_orientation = fields.Selection(
- string="Garden Orientation",
- selection=[
- ("north", "North"),
- ("south", "South"),
- ("east", "East"),
- ("west", "West"),
- ],
- help="Select the direction the garden faces",
- )
- property_type_id = fields.Many2one("estate.property.type")
- buyer = fields.Many2one("res.partner", readonly=True)
- salesman = fields.Many2one("res.users")
tag_ids = fields.Many2many("estate.property.tag")
- offer_ids = fields.One2many("estate.property.offer", inverse_name="property_id")
total_area = fields.Float(compute="_compute_total_area", store=True)
- best_price = fields.Float(compute="_compute_best_price", store=True, default=0.0)
_sql_constraints = [
(
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..0901cde9d8a
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,8 @@
+{
+ "name": "Real Estate Accounting",
+ "depends": ["estate", "account"],
+ "category": "Real Estate",
+ "data": [],
+ "installable": True,
+ "license": "LGPL-3",
+}
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
new file mode 100644
index 00000000000..5e1963c9d2f
--- /dev/null
+++ b/estate_account/models/__init__.py
@@ -0,0 +1 @@
+from . import estate_property
diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py
new file mode 100644
index 00000000000..9f9b8f95cf7
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,37 @@
+from odoo import Command, models
+from odoo.exceptions import UserError
+from odoo.tools.float_utils import float_is_zero
+
+
+class EstatePropertyAccounting(models.Model):
+ _inherit = "estate.property"
+
+ def action_property_sold(self):
+ if float_is_zero(self.selling_price, precision_rounding=0.01):
+ raise UserError(
+ "Atleast one offer must be accepted before selling the property"
+ )
+ values = {
+ "partner_id": self.buyer.id,
+ "move_type": "out_invoice",
+ "invoice_line_ids": [
+ Command.create(
+ {
+ "name": self.name,
+ "quantity": 1,
+ "price_unit": self.selling_price * 0.06,
+ }
+ ),
+ Command.create(
+ {
+ "name": "administrative_fees",
+ "quantity": 1,
+ "price_unit": 100,
+ }
+ ),
+ ],
+ }
+
+ self.env["account.move"].create(values)
+
+ return super().action_property_sold()
From 992658e298f82da1c96cdd8edc0a837776590831 Mon Sep 17 00:00:00 2001
From: satp-odoo
Date: Mon, 1 Sep 2025 14:12:39 +0530
Subject: [PATCH 14/14] [ADD] estate: Add Kanban View for Properties
Introduces a Kanban view to visually represent properties using a custom QWeb-powered layout, offering a more intuitive and engaging alternative to the standard list view.
Key Features:
- Displays property name and key pricing details.
- Shows expected price and tags by default.
- Displays best offer only when offers exist.
- Reveals selling price only when an offer is accepted.
- Groups properties by property type by default.
- Drag and drop is disabled for data consistency.
---
estate/models/estate_property_offer.py | 1 +
estate/views/estate_property_views.xml | 45 +++++++++++++++++++++++---
2 files changed, 41 insertions(+), 5 deletions(-)
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
index 3a3c250d1aa..8771ab06b68 100644
--- a/estate/models/estate_property_offer.py
+++ b/estate/models/estate_property_offer.py
@@ -25,6 +25,7 @@ class EstateOfferModel(models.Model):
compute="_compute_date_deadline",
inverse="_inverse_date_deadline",
default=fields.Date.today() + timedelta(days=7),
+ store=True
)
_sql_constraints = [
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
index 85f3b0f06a7..001d756508f 100644
--- a/estate/views/estate_property_views.xml
+++ b/estate/views/estate_property_views.xml
@@ -5,7 +5,7 @@
Properties
estate.property
- list,form
+ kanban,list,form
{'search_default_available': True}
@@ -14,13 +14,48 @@
+
+ Estate Properties kanban
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Expected Price:
+
+
+
+ Best Price:
+
+
+
+ Selling Price:
+
+
+
+
+
+
+
+
+
+
+
+
Estate Properties list
estate.property
-
+
@@ -86,7 +121,7 @@
-
+