From 18ca5045b61c06ea8890f71d68e9350929df19bc Mon Sep 17 00:00:00 2001 From: "Cyril Moreau (cymo)" Date: Mon, 16 Feb 2026 14:10:36 +0100 Subject: [PATCH 1/7] [ADD] estate: Create Real Estate module manifest --- estate/__init__.py | 0 estate/__manifest__.py | 10 ++++++++++ 2 files changed, 10 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..18fb9fb74ae --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,10 @@ +{ + 'name': 'Real Estate', + 'author': 'test :)', + 'depends': [ + 'base', + ], + 'installable': True, + 'application': True, + 'auto_install': False +} \ No newline at end of file From 427542d5c8a7264a8443f64c25b93f8aff9c93ca Mon Sep 17 00:00:00 2001 From: "Cyril Moreau (cymo)" Date: Mon, 16 Feb 2026 15:39:55 +0100 Subject: [PATCH 2/7] [IMP] estate: create estate_property model add basic fields. \n Chapter 3 - Server framework 101 --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 21 +++++++++++++++++++++ 3 files changed, 23 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..9dfbe7328bb --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,21 @@ +from odoo import models, fields + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Handle real estate property" + + 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( + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + ) From a1468e327a18ebd0ca0c3f5fe5e7e5ca1bc9df38 Mon Sep 17 00:00:00 2001 From: "Cyril Moreau (cymo)" Date: Mon, 16 Feb 2026 17:29:31 +0100 Subject: [PATCH 3/7] [IMP] estate: Create access rights for estate property. Chapter 4 - Security --- estate/__manifest__.py | 3 +++ estate/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 18fb9fb74ae..92f3791623d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,6 +4,9 @@ 'depends': [ 'base', ], + 'data': [ + 'security/ir.model.access.csv', + ], 'installable': True, 'application': True, 'auto_install': False diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..976b61e8cb3 --- /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,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From 2aa5be2422395f159654727ddf3f7fbee0531429 Mon Sep 17 00:00:00 2001 From: "Cyril Moreau (cymo)" Date: Tue, 17 Feb 2026 10:47:52 +0100 Subject: [PATCH 4/7] [IMP] estate: Default menus and form view for estate properties. Chapter 5 - Finally, Some UI To Play With --- estate/__manifest__.py | 4 +++- estate/models/estate_property.py | 18 +++++++++++++++--- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 4 files changed, 34 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 92f3791623d..5daa0ba363d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,11 +1,13 @@ { 'name': 'Real Estate', - 'author': 'test :)', + 'author': 'Odoo S.A.', 'depends': [ 'base', ], 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml' ], 'installable': True, 'application': True, diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9dfbe7328bb..d0cd90df82d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,8 @@ +from dateutil.relativedelta import relativedelta + from odoo import models, fields + class EstateProperty(models.Model): _name = "estate.property" _description = "Handle real estate property" @@ -7,10 +10,10 @@ class EstateProperty(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()+ relativedelta(months=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() @@ -19,3 +22,12 @@ class EstateProperty(models.Model): garden_orientation = fields.Selection( selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], ) + active = fields.Boolean(default=True) + state = fields.Selection( + string = 'Status', + selection=[('new', 'New'), ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], + required = True, + copy=False, + default='new' + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..7ba49854a57 --- /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..1d2a3aaa4cd --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate.property + list,form + + From f7c4353d396dc68cc972c8e7fb928c617318f986 Mon Sep 17 00:00:00 2001 From: "Cyril Moreau (cymo)" Date: Tue, 17 Feb 2026 13:16:41 +0100 Subject: [PATCH 5/7] [IMP] estate: Add list,form and search views for estate properties Chapter 6 - Basic views --- estate/models/estate_property.py | 10 ++-- estate/views/estate_property_views.xml | 75 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d0cd90df82d..4f09e0c6df6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -7,18 +7,18 @@ class EstateProperty(models.Model): _name = "estate.property" _description = "Handle real estate property" - name = fields.Char(required=True) + name = fields.Char(string='Title', required=True) description = fields.Text() - postcode = fields.Char() - date_availability = fields.Date(copy=False, default=fields.Date.today()+ relativedelta(months=3)) + postcode = fields.Char(string='Postcode') + date_availability = fields.Date(string='Available From', copy=False, default=fields.Date.today()+ relativedelta(months=3)) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) - living_area = fields.Integer() + living_area = fields.Integer(string='Living Area (sqm)') facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer() + garden_area = fields.Integer(string='Garden Area (sqm)') garden_orientation = fields.Selection( selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], ) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1d2a3aaa4cd..8999c821a2c 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,79 @@ estate.property list,form + + + estate.properties.list + estate.property + + + + + + + + + + + + + + + estate.properties.view.form + estate.property + +
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.properties.view.search + estate.property + + + + + + + + + + + + + + + + + + + From c2e3f0c76b40594e2c8c967da8b34d7c4d427e49 Mon Sep 17 00:00:00 2001 From: "Cyril Moreau (cymo)" Date: Tue, 17 Feb 2026 14:25:34 +0100 Subject: [PATCH 6/7] [IMP] estate: Add property types, tags and offers. Chapter 7 - Relations Between Models --- estate/__manifest__.py | 5 +++- estate/models/__init__.py | 5 +++- estate/models/estate_property.py | 6 ++++ estate/models/estate_property_offer.py | 12 ++++++++ estate/models/estate_property_tag.py | 8 ++++++ estate/models/estate_property_type.py | 8 ++++++ estate/security/ir.model.access.csv | 5 +++- estate/views/estate_menus.xml | 4 +++ estate/views/estate_property_offer_views.xml | 30 ++++++++++++++++++++ estate/views/estate_property_tag_views.xml | 19 +++++++++++++ estate/views/estate_property_type_views.xml | 19 +++++++++++++ estate/views/estate_property_views.xml | 16 +++++++++++ 12 files changed, 134 insertions(+), 3 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 5daa0ba363d..8402c03656c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,9 +7,12 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml' ], 'installable': True, 'application': True, 'auto_install': False -} \ No newline at end of file +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..8f2187ee09e 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_offer +from . import estate_property_tag +from . import estate_property_type diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4f09e0c6df6..a1018a7ff6f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -31,3 +31,9 @@ class EstateProperty(models.Model): copy=False, default='new' ) + + property_type_id = fields.Many2one('estate.property.type') + buyer_id = fields.Many2one('res.partner', copy=False) + salesman_id = fields.Many2one('res.users', default=lambda self: self.env.user) + tag_ids = fields.Many2many('estate.property.tag') + 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..3e1566e2e5a --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Offer for an estate property' + + price = fields.Float() + status = fields.Selection(copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) + + partner_id = fields.Many2one('res.partner', required=True) + 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..fef8d256bae --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'Estate Property Tag' + + 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..d2305c70939 --- /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 = 'Define type of properties' + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 976b61e8cb3..49bca99cac8 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_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 7ba49854a57..f9f32a3a7fd 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,9 @@ + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..71134f43d8b --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,30 @@ + + + + + estate.property.offers.view.list + estate.property.offer + + + + + + + + + + + estate.property.offers.view.form + estate.property.offer + +
+ + + + + +
+
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..01a5e76d1b9 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,19 @@ + + + + + estate.property.tag.view.list + estate.property.tag + + + + + + + + + Property Tags + estate.property.tag + list + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..f1cccd38757 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,19 @@ + + + + + estate.property.type.view.list + estate.property.type + + + + + + + + + Property Types + estate.property.type + list + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 8999c821a2c..d98ab48a35e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -12,12 +12,14 @@ + + @@ -29,8 +31,12 @@

+ + + + @@ -52,6 +58,15 @@ + + + + + + + + +
@@ -69,6 +84,7 @@ + From 407eb5d8579db7ff9d7b7ee03c2a8b2663f30cb6 Mon Sep 17 00:00:00 2001 From: "Cyril Moreau (cymo)" Date: Tue, 17 Feb 2026 15:24:07 +0100 Subject: [PATCH 7/7] [IMP] estate: Add total_area, best_price and validity date. Chapter 8 - Computed Fields and Onchanges --- estate/models/estate_property.py | 24 +++++++++++++++++++- estate/models/estate_property_offer.py | 17 +++++++++++++- estate/views/estate_property_offer_views.xml | 6 ++++- estate/views/estate_property_views.xml | 2 ++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a1018a7ff6f..c2d5ca0cac7 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ from dateutil.relativedelta import relativedelta -from odoo import models, fields +from odoo import api, models, fields class EstateProperty(models.Model): @@ -37,3 +37,25 @@ class EstateProperty(models.Model): salesman_id = fields.Many2one('res.users', default=lambda self: self.env.user) tag_ids = fields.Many2many('estate.property.tag') offer_ids = fields.One2many('estate.property.offer', 'property_id') + + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Float(string='Best Offer', compute="_compute_best_price") + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for line in self: + line.total_area = line.living_area + line.garden_area + + @api.depends('offer_ids.price') + def _compute_best_price(self): + for record in self: + record.best_price = max(self.offer_ids.mapped('price'), default=0) + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = False + self.garden_orientation = False diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 3e1566e2e5a..31705e99203 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -7,6 +9,19 @@ class EstatePropertyOffer(models.Model): price = fields.Float() status = fields.Selection(copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) + validity = fields.Integer(string='Validity (days)', default=7) + date_deadline = fields.Date(string='Deadline', compute='_compute_deadline', inverse='_inverse_deadline') partner_id = fields.Many2one('res.partner', required=True) property_id = fields.Many2one('estate.property', required=True) + + @api.depends('validity') + def _compute_deadline(self): + for record in self: + if not record.create_date: + record.create_date = fields.Date.today() + record.date_deadline = record.create_date + relativedelta(days=record.validity) + + def _inverse_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 71134f43d8b..9b56f6cb7fc 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -5,9 +5,11 @@ estate.property.offers.view.list estate.property.offer - + + + @@ -21,6 +23,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d98ab48a35e..b7918aaabfe 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -42,6 +42,7 @@ + @@ -56,6 +57,7 @@ +