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

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate_property.search + estate_property + + + + + + + + + + + + + + + + + + From a2ae3a1638819bf5a92c5016e8d98c7d9fb03aa1 Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Wed, 17 Dec 2025 13:41:01 +0100 Subject: [PATCH 06/14] [IMP] estate: Add relations between real estate property models Chapter 7 This adds tags to properties, lists offers made (whether refued or accepted) along with the salesperson and buyer in "Other info" tab --- estate/__manifest__.py | 3 +++ estate/data/ir.model.access.csv | 3 +++ estate/models/__init__.py | 3 ++- estate/models/estate_property.py | 9 ++++++++- estate/models/estate_property_offer.py | 13 ++++++++++++ estate/models/estate_property_tag.py | 8 ++++++++ estate/models/estate_property_type.py | 8 ++++++++ estate/views/estate_menus.xml | 8 +++++++- estate/views/estate_property_offer.xml | 15 ++++++++++++++ estate/views/estate_property_tag.xml | 8 ++++++++ estate/views/estate_property_type.xml | 8 ++++++++ estate/views/estate_property_views.xml | 28 +++++++++++++++++++------- 12 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offer.xml create mode 100644 estate/views/estate_property_tag.xml create mode 100644 estate/views/estate_property_type.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9657f67ef53..220af570e82 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,6 +12,9 @@ "data": [ "data/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_type.xml", + "views/estate_property_tag.xml", + "views/estate_property_offer.xml", "views/estate_menus.xml", ], } diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv index 98f4671fb0d..0c0b62b7fee 100644 --- a/estate/data/ir.model.access.csv +++ b/estate/data/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..aae114c2505 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,2 @@ -from . import estate_property +from . import (estate_property, estate_property_offer, estate_property_tag, + estate_property_type) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 89a7a406146..6a32bbbf71c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,7 +5,7 @@ class EstateProperty(models.Model): - _name = "estate_property" + _name = "estate.property" _description = "Estate Property data model" name = fields.Char(required=True) @@ -45,3 +45,10 @@ class EstateProperty(models.Model): default="new", help="State of the proprty listing lifecycle", ) + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + buyer_id = fields.Many2one("res.partner", string="Buyer") + salesperson_id = fields.Many2one( + "res.users", string="Salesman", default=lambda self: self.env.user + ) + tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..5e6b38c3386 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real estate property offer" + + price = fields.Float() + status = fields.Selection( + [("accepted", "Accepted"), ("refused", "Refused")], copy=False + ) + partner_id = fields.Many2one("res.partner", required=True, string="Sales") + property_id = fields.Many2one("estate.property", required=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..9f0eae86da7 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real estate property tags" + + name = fields.Char(required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..d67f8160b3b --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate property type" + + name = fields.Char(required=True) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index aa77f21e0a1..ec92a50c678 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,8 +1,14 @@ - + + + + + diff --git a/estate/views/estate_property_offer.xml b/estate/views/estate_property_offer.xml new file mode 100644 index 00000000000..1355b30ce22 --- /dev/null +++ b/estate/views/estate_property_offer.xml @@ -0,0 +1,15 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + diff --git a/estate/views/estate_property_tag.xml b/estate/views/estate_property_tag.xml new file mode 100644 index 00000000000..b10720e7969 --- /dev/null +++ b/estate/views/estate_property_tag.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_type.xml b/estate/views/estate_property_type.xml new file mode 100644 index 00000000000..45653e20ef3 --- /dev/null +++ b/estate/views/estate_property_type.xml @@ -0,0 +1,8 @@ + + + + Property Types + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 696a8a49273..e34c9ba3698 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -2,13 +2,13 @@ estate property action - estate_property + estate.property list,form - estate_property.list - estate_property + estate.property.list + estate.property @@ -23,8 +23,8 @@ - estate_property.form - estate_property + estate.property.form + estate.property
@@ -33,8 +33,11 @@ + + + @@ -58,6 +61,17 @@ + + + + + + + + + + +
@@ -65,8 +79,8 @@
- estate_property.search - estate_property + estate.property.search + estate.property From bb41efbedc62efd10f0ca523b931aa4206531f11 Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Fri, 19 Dec 2025 11:36:16 +0100 Subject: [PATCH 07/14] [IMP] estate: introduce computed fields and use onchange effects for updates Chapter 8 --- estate/models/estate_property.py | 19 ++++++++++++++++++- estate/models/estate_property_offer.py | 21 ++++++++++++++++++++- estate/views/estate_property_offer.xml | 2 ++ estate/views/estate_property_views.xml | 2 ++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 6a32bbbf71c..ed21a478865 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,7 +1,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -52,3 +52,20 @@ class EstateProperty(models.Model): ) tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") offer_ids = fields.One2many("estate.property.offer", "property_id") + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Float(compute="_compute_best_price") + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped("price")) + + @api.onchange("garden") + def _onchange_garden(self): + self.garden_area = 10 if self.garden else 0 + self.garden_orientation = "north" if self.garden else None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 5e6b38c3386..e027c9a41fb 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +import datetime + +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -11,3 +13,20 @@ class EstatePropertyOffer(models.Model): ) partner_id = fields.Many2one("res.partner", required=True, string="Sales") property_id = fields.Many2one("estate.property", required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_date_deadline", inverse="_inverse_date_deadline" + ) + + @api.depends("create_date", "validity") + def _compute_date_deadline(self): + for record in self: + record.date_deadline = ( + record.create_date.date() + if record.create_date + else datetime.date.today() + ) + datetime.timedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days diff --git a/estate/views/estate_property_offer.xml b/estate/views/estate_property_offer.xml index 1355b30ce22..c8363e2ff4f 100644 --- a/estate/views/estate_property_offer.xml +++ b/estate/views/estate_property_offer.xml @@ -7,6 +7,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e34c9ba3698..90054f7d818 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -43,6 +43,7 @@ + @@ -59,6 +60,7 @@ + From 97cb70182c7a3526a48b0a46b577de5af352e25c Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Fri, 19 Dec 2025 15:49:21 +0100 Subject: [PATCH 08/14] [IMP] estate: implement actions on properties and offers Chapter 9 This introduces marking a property as "Sold" or "Cancelled". It also allows for easier offers management from the list view and it gets reflected automatically on the property --- estate/models/estate_property.py | 26 ++++++++++++++++++++++++-- estate/models/estate_property_offer.py | 11 +++++++++++ estate/views/estate_property_offer.xml | 2 ++ estate/views/estate_property_views.xml | 5 +++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ed21a478865..13964c6e3d5 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,7 +1,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta -from odoo import api, fields, models +from odoo import api, exceptions, fields, models class EstateProperty(models.Model): @@ -63,9 +63,31 @@ def _compute_total_area(self): @api.depends("offer_ids.price") def _compute_best_price(self): for record in self: - record.best_price = max(record.offer_ids.mapped("price")) + record.best_price = ( + max(record.offer_ids.mapped("price")) if record.offer_ids else 0 + ) @api.onchange("garden") def _onchange_garden(self): self.garden_area = 10 if self.garden else 0 self.garden_orientation = "north" if self.garden else None + + def sold_estate_property(self): + for record in self: + if record.state != "cancelled": + record.state = "sold" + else: + raise exceptions.UserError("can't sell a cancelled property") + + def cancel_estate_property(self): + for record in self: + if record.state != "sold": + record.state = "cancelled" + else: + raise exceptions.UserError("can't cancel a sold property") + + def reject_other_offers(self, winning_offer): + for record in self: + for offer in record.offer_ids: + if offer != winning_offer: + offer.status = "refused" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index e027c9a41fb..3c255d91602 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -30,3 +30,14 @@ def _compute_date_deadline(self): def _inverse_date_deadline(self): for record in self: record.validity = (record.date_deadline - record.create_date.date()).days + + def action_confirm(self): + for record in self: + record.status = "accepted" + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + record.property_id.reject_other_offers(record) + + def action_cancel(self): + for record in self: + record.status = "refused" diff --git a/estate/views/estate_property_offer.xml b/estate/views/estate_property_offer.xml index c8363e2ff4f..ca6780f7d18 100644 --- a/estate/views/estate_property_offer.xml +++ b/estate/views/estate_property_offer.xml @@ -10,6 +10,8 @@ + + + + + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index eabed08aec1..78c4787d21d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,20 +4,22 @@ estate property action estate.property list,form + {'search_default_available': True} estate.property.list estate.property - + - + @@ -27,7 +29,9 @@ estate.property
-
+ +
@@ -37,12 +41,14 @@ - + - + @@ -63,14 +69,15 @@ - - + + - + @@ -94,7 +101,7 @@ - + From 8970abac84169ec298d4431788cbe9af3f822df5 Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Wed, 24 Dec 2025 10:45:41 +0100 Subject: [PATCH 11/14] [IMP] estate: inherit models and views from res.users Chapter 12 This overrides deletion methods to make them safer by only allowing deletion of new or cancelled properties. It integrates the real estate module with the views for 'res.users' model by adding a new page listing the properties managed by each user --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 8 ++++++++ estate/models/estate_property_offer.py | 12 +++++++++++- estate/models/res_users.py | 6 ++++++ estate/views/res_users.xml | 15 +++++++++++++++ 6 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index e5a3e03b0c1..1fa67232141 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -16,5 +16,6 @@ "views/estate_property_type.xml", "views/estate_property_tag.xml", "views/estate_menus.xml", + "views/res_users.xml", ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8f2187ee09e..fea9f441d6d 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_offer from . import estate_property_tag from . import estate_property_type +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 45ca6c5aa01..426a4aff8e9 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -115,3 +115,11 @@ def _check_selling_price(self): raise exceptions.ValidationError( "The selling price cannot be lower than 90% of the expected price" ) + + @api.ondelete(at_uninstall=False) + def _unlink_if_new_cancelled(self): + if any(record.state not in ('new', 'cancelled') for record in self): + raise exceptions.UserError("Can't delete a property unless it's new or cancelled") + + def lowball_offer(self, offer_price): + return any(offer_price < offer.price for offer in self.offer_ids) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index d2c02028223..4ec56818261 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,6 @@ import datetime -from odoo import api, fields, models +from odoo import api, exceptions, fields, models class EstatePropertyOffer(models.Model): @@ -51,3 +51,13 @@ def action_confirm(self): def action_cancel(self): for record in self: record.status = "refused" + + @api.model + def create(self, vals_list): + print(vals_list) + for vals in vals_list: + prop = self.env['estate.property'].browse(vals['property_id']) + if prop.lowball_offer(vals['price']): + raise exceptions.UserError("Offer price can't be lower than lowest offer") + prop.state = "offer_received" + return super().create(vals_list) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..b16bee00a0b --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,6 @@ +from odoo import fields, models + +class ResUsers(models.Model): + _inherit = ["res.users"] + + property_ids = fields.One2many("estate.property", "salesperson_id", domain= lambda self: [('salesperson_id', '=', self.id)]) diff --git a/estate/views/res_users.xml b/estate/views/res_users.xml new file mode 100644 index 00000000000..d0d7e07ff24 --- /dev/null +++ b/estate/views/res_users.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + From edcacf49fb161b156a4f3e3bc1ef683506b494d7 Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Wed, 24 Dec 2025 15:00:05 +0100 Subject: [PATCH 12/14] [IMP] estate: link real estate module with accounting to auto generate invoices on sales Chapter 13 This introduces a link module "estate_account" that enables real estate module to generate invoices automatically on sold houses. --- estate/models/estate_property.py | 2 +- estate/models/estate_property_offer.py | 3 +-- estate/models/res_users.py | 3 ++- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 11 +++++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 14 ++++++++++++++ 7 files changed, 31 insertions(+), 4 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 426a4aff8e9..8d8ee4a91e2 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -115,7 +115,7 @@ def _check_selling_price(self): raise exceptions.ValidationError( "The selling price cannot be lower than 90% of the expected price" ) - + @api.ondelete(at_uninstall=False) def _unlink_if_new_cancelled(self): if any(record.state not in ('new', 'cancelled') for record in self): diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 4ec56818261..5be93b4e167 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -51,10 +51,9 @@ def action_confirm(self): def action_cancel(self): for record in self: record.status = "refused" - + @api.model def create(self, vals_list): - print(vals_list) for vals in vals_list: prop = self.env['estate.property'].browse(vals['property_id']) if prop.lowball_offer(vals['price']): diff --git a/estate/models/res_users.py b/estate/models/res_users.py index b16bee00a0b..4d79b3f0eb0 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -1,6 +1,7 @@ from odoo import fields, models + class ResUsers(models.Model): _inherit = ["res.users"] - property_ids = fields.One2many("estate.property", "salesperson_id", domain= lambda self: [('salesperson_id', '=', self.id)]) + property_ids = fields.One2many("estate.property", "salesperson_id", domain=lambda self: [('salesperson_id', '=', self.id)]) 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..065adcd25c1 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,11 @@ +{ + "name": "estate_account", + "version": "1.0", + "depends": ["base", "account", "estate"], + "author": "Odoo S.A.", + "category": "Real Estate", + "description": """ + A link module between real estate and accounting modules + """, + "application": True, +} 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..e5cc18f74d0 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,14 @@ +from odoo import fields, models + +class EstateProperty(models.Model): + _inherit = ["estate.property"] + + def sold_estate_property(self): + for record in self: + invoice_vals = {'partner_id': record.buyer_id.id, 'move_type': 'out_invoice', 'journal_id': 1,\ + 'invoice_line_ids': [ + fields.Command.create({'name': record.name, 'quantity': 1, 'price_unit': (6/100)* record.selling_price}), + fields.Command.create({'name': 'Administrative fees', 'quantity': 1, 'price_unit': 100}) ]} + self.env['account.move'].create(invoice_vals) + + return super().sold_estate_property() From af7b03b6f1c59f70f5df465bbcbaca585a5bd43c Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Wed, 24 Dec 2025 15:52:07 +0100 Subject: [PATCH 13/14] [IMP] estate: implement kanban view Chapter 14 This implements kanban views using QWeb template definition --- estate/views/estate_property_views.xml | 32 +++++++++++++++++++++++- estate_account/models/estate_property.py | 7 +++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 78c4787d21d..74ebad999a1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ estate property action estate.property - list,form + kanban,list,form {'search_default_available': True} @@ -115,4 +115,34 @@ + + + estate.property.kanban + estate.property + + + + +
+ + + + +
Expected Price: +
+
Best Price: +
+
Selling + Price: +
+ +
+
+ +
+
+
+
diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index e5cc18f74d0..3a7485e9124 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -1,14 +1,15 @@ from odoo import fields, models + class EstateProperty(models.Model): _inherit = ["estate.property"] def sold_estate_property(self): for record in self: - invoice_vals = {'partner_id': record.buyer_id.id, 'move_type': 'out_invoice', 'journal_id': 1,\ + invoice_vals = {'partner_id': record.buyer_id.id, 'move_type': 'out_invoice', 'journal_id': 1, 'invoice_line_ids': [ - fields.Command.create({'name': record.name, 'quantity': 1, 'price_unit': (6/100)* record.selling_price}), - fields.Command.create({'name': 'Administrative fees', 'quantity': 1, 'price_unit': 100}) ]} + fields.Command.create({'name': record.name, 'quantity': 1, 'price_unit': (6 / 100) * record.selling_price}), + fields.Command.create({'name': 'Administrative fees', 'quantity': 1, 'price_unit': 100})]} self.env['account.move'].create(invoice_vals) return super().sold_estate_property() From 84b226de27e9b4b0821c0219de5875c2a7e8cf1c Mon Sep 17 00:00:00 2001 From: "ibrahim (ibmah)" Date: Wed, 24 Dec 2025 16:09:55 +0100 Subject: [PATCH 14/14] [CLN] estate: address warnings from missing licenses and model names Chapter 15 This addresses warnings and various runbot-reported issues. --- estate/__manifest__.py | 1 + estate/models/estate_property_offer.py | 2 +- estate/models/res_users.py | 1 + estate_account/__manifest__.py | 1 + estate_account/models/estate_property.py | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 1fa67232141..4e548afff57 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -18,4 +18,5 @@ "views/estate_menus.xml", "views/res_users.xml", ], + 'license': 'AGPL-3', } diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 5be93b4e167..cab6401a628 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -19,7 +19,7 @@ class EstatePropertyOffer(models.Model): compute="_compute_date_deadline", inverse="_inverse_date_deadline" ) property_type_id = fields.Integer( - related="property_id.property_type_id.id", store=True + related="property_id.property_type_id.id", store=True, string="Property Type id" ) _check_price = models.Constraint( diff --git a/estate/models/res_users.py b/estate/models/res_users.py index 4d79b3f0eb0..d2ee7afc596 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -2,6 +2,7 @@ class ResUsers(models.Model): + _name = 'res.users' _inherit = ["res.users"] property_ids = fields.One2many("estate.property", "salesperson_id", domain=lambda self: [('salesperson_id', '=', self.id)]) diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index 065adcd25c1..06fe182534b 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -8,4 +8,5 @@ A link module between real estate and accounting modules """, "application": True, + 'license': 'AGPL-3', } diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 3a7485e9124..0c1ce7f5443 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -2,6 +2,7 @@ class EstateProperty(models.Model): + _name = 'estate.property' _inherit = ["estate.property"] def sold_estate_property(self):