From c95998a52657e4a2fe6d269d09f16e9b49162f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Mon, 15 Dec 2025 13:52:34 +0100 Subject: [PATCH 01/25] [ADD] Chapter 2 - Add app --- estate/__init__.py | 0 estate/__manifest__.py | 6 ++++++ 2 files changed, 6 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..c7062e2a578 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,6 @@ +{ + "name": "Real Estate", + "depends": ["base"], + "installable": True, + "application": True, +} \ No newline at end of file From 1fd3b26bd5435f3c5ac133b2a1fcb13331acc954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Mon, 15 Dec 2025 15:00:52 +0100 Subject: [PATCH 02/25] [REV] Fix build --- estate/__manifest__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c7062e2a578..154d3aed2c4 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,4 +3,5 @@ "depends": ["base"], "installable": True, "application": True, -} \ No newline at end of file +} + From 48f899ff0e199fc45faad87ba22f32f3aaf4e582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Mon, 15 Dec 2025 15:31:03 +0100 Subject: [PATCH 03/25] [FIX] Fix build --- estate/__manifest__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 154d3aed2c4..05603800ff4 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,4 +4,3 @@ "installable": True, "application": True, } - From fee6b56d2a07e56e6edbd216629be3fb017b4251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Mon, 15 Dec 2025 15:46:31 +0100 Subject: [PATCH 04/25] [ADD] Chapter 3 --- estate/__init__.py | 1 + estate/__manifest__.py | 10 ++++++---- estate/models/__init__.py | 1 + estate/models/property.py | 22 ++++++++++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/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/__manifest__.py b/estate/__manifest__.py index 05603800ff4..cdd4397853c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,8 @@ { - "name": "Real Estate", - "depends": ["base"], - "installable": True, - "application": True, + 'name': 'Real Estate', + 'depends': ['base'], + 'installable': True, + 'application': True, + 'author': "Odoo", + 'license': 'AGPL-3' } diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..8120b005bb6 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import property diff --git a/estate/models/property.py b/estate/models/property.py new file mode 100644 index 00000000000..abfb61c4027 --- /dev/null +++ b/estate/models/property.py @@ -0,0 +1,22 @@ +from odoo import fields, models + + +class Property(models.Model): + _name = "estate.property" + _description = "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 057d77fa1667cf97892f9a4a4524e4cd74bd158f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Mon, 15 Dec 2025 15:58:59 +0100 Subject: [PATCH 05/25] [ADD] Chapter 4 - Add security --- estate/__manifest__.py | 1 + estate/security/ir.model.access.csv | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index cdd4397853c..26b2a42a2dd 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,7 @@ { 'name': 'Real Estate', 'depends': ['base'], + 'data': ['security/ir.model.access.csv'], 'installable': True, 'application': True, 'author': "Odoo", diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..d9d6ba57cc5 --- /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 From 0e1ffa3c714d59a23caedb63031df0bdefea1134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Mon, 15 Dec 2025 16:44:39 +0100 Subject: [PATCH 06/25] [IMP] Chapter 5 - Add UI --- estate/__manifest__.py | 7 ++++++- estate/models/property.py | 26 ++++++++++++++++++++------ estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 4 files changed, 42 insertions(+), 7 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 26b2a42a2dd..626f049f1aa 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,7 +1,12 @@ { 'name': 'Real Estate', 'depends': ['base'], - 'data': ['security/ir.model.access.csv'], + 'data': [ + 'security/ir.model.access.csv', + + 'views/estate_property_views.xml', + 'views/estate_menus.xml', + ], 'installable': True, 'application': True, 'author': "Odoo", diff --git a/estate/models/property.py b/estate/models/property.py index abfb61c4027..fb408b5862b 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -1,22 +1,36 @@ +from dateutil.relativedelta import relativedelta + from odoo import fields, models class Property(models.Model): - _name = "estate.property" - _description = "Estate property" + _name = 'estate.property' + _description = 'Estate property' name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date(default=fields.Datetime.today() + relativedelta(months=3), copy=False) 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() garden_orientation = fields.Selection( - selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")] + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] + ) + state = fields.Selection( + default='new', + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), ('cancelled', 'Cancelled'), + ], + required=True, + copied=False, ) + active = fields.Boolean(default=True) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..2b1661b3800 --- /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..de6c08969de --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate.property + list,form + + From 80c6911338b3fb41858838062b4a4cae01839255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Tue, 16 Dec 2025 13:06:47 +0100 Subject: [PATCH 07/25] [IMP] Chapter 6 - Add custom views --- estate/models/property.py | 12 ++-- estate/views/estate_property_views.xml | 78 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/estate/models/property.py b/estate/models/property.py index fb408b5862b..b0f7297223d 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -7,18 +7,20 @@ class Property(models.Model): _name = 'estate.property' _description = 'Estate property' - name = fields.Char(required=True) + name = fields.Char(string='Title', required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date(default=fields.Datetime.today() + relativedelta(months=3), copy=False) + date_availability = fields.Date( + string='Available From', default=fields.Datetime.today() + relativedelta(months=3), copy=False + ) 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')] ) @@ -31,6 +33,6 @@ class Property(models.Model): ('sold', 'Sold'), ('cancelled', 'Cancelled'), ], required=True, - copied=False, + copy=False, ) active = fields.Boolean(default=True) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index de6c08969de..91d579c0804 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,5 +1,83 @@ + + estate.property.form + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + Properties estate.property From c551f660b711f0990e3f16fdcdeeb61d9169b3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Tue, 16 Dec 2025 13:28:41 +0100 Subject: [PATCH 08/25] [FIX] Fix parsing error in group by view --- estate/views/estate_property_views.xml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 91d579c0804..21bd6400b1f 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -67,13 +67,9 @@ - - - - - + From 793b0ccd6fd217bca1cf494bf1f1896be474a6e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Tue, 16 Dec 2025 13:28:41 +0100 Subject: [PATCH 09/25] [FIX] Fix parsing error in group by view --- estate/views/estate_property_views.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 91d579c0804..98d29a96feb 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -67,12 +67,10 @@ - - - - + + From fbf913fc95a5648bf60464ef88a9703f89c711a0 Mon Sep 17 00:00:00 2001 From: Pierre Lamotte Date: Tue, 16 Dec 2025 14:26:25 +0100 Subject: [PATCH 10/25] [IMP] estate: remember last sold price for statistics This field is added and automatically computed in order to prepare the coming changes in accounting with the new analytics dashboard and to prepare multiple sells. task-00001 --- estate/models/property.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/estate/models/property.py b/estate/models/property.py index b0f7297223d..58e50b7c3eb 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -1,6 +1,6 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models class Property(models.Model): @@ -36,3 +36,11 @@ class Property(models.Model): copy=False, ) active = fields.Boolean(default=True) + last_sold_price = fields.Monetary( + string="Last Price", compute='_compute_last_sold_price', readonly=True + ) + + @api.onchange('state') + def _compute_last_sold_price(self): + if self.state == 'sold': + self._compute_last_sold_price = self.selling_price From 8da414aa065e928f31f7bd02c4fbea9f8414a2b3 Mon Sep 17 00:00:00 2001 From: Pierre Lamotte Date: Tue, 16 Dec 2025 14:43:01 +0100 Subject: [PATCH 11/25] [FIX] estate: unfreeze field date ## Versions 19.0+ ## Issue The `date_availability` default value is a fixed value set to the date the Odoo instance has been ran for the first time. ## Cause Commit 0e1ffa3c714d59a23caedb63031df0bdefea1134 introduced a wrong default value to this field. ## Solution Use a lambda function (cf. https://www.w3schools.com/python/python_lambda.asp) to recompute de date. opw-00001 --- estate/models/property.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/estate/models/property.py b/estate/models/property.py index 58e50b7c3eb..e1c5116a9ac 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -11,7 +11,9 @@ class Property(models.Model): description = fields.Text() postcode = fields.Char() date_availability = fields.Date( - string='Available From', default=fields.Datetime.today() + relativedelta(months=3), copy=False + string="Available From", + default=lambda self: fields.Datetime.today() + relativedelta(months=3), + copy=False, ) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) From 1de0fde519d1edc76b5c9a49421f4eab13fc412c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Tue, 16 Dec 2025 15:18:25 +0100 Subject: [PATCH 12/25] [IMP] Chapter 7 - Add relational models --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 2 +- estate/models/property.py | 28 +++++++-------- estate/models/property_offer.py | 11 ++++++ estate/models/property_tag.py | 8 +++++ estate/models/property_type.py | 8 +++++ estate/security/ir.model.access.csv | 3 ++ estate/views/estate_menus.xml | 6 +++- estate/views/estate_property_offer_views.xml | 38 ++++++++++++++++++++ estate/views/estate_property_tag_views.xml | 32 +++++++++++++++++ estate/views/estate_property_type_views.xml | 34 ++++++++++++++++++ estate/views/estate_property_views.xml | 17 +++++++++ 12 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 estate/models/property_offer.py create mode 100644 estate/models/property_tag.py create mode 100644 estate/models/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 626f049f1aa..f7b3a780d14 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,6 +4,9 @@ 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml', ], diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8120b005bb6..22c7e2d2c5b 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import property +from . import property, property_offer, property_tag, property_type diff --git a/estate/models/property.py b/estate/models/property.py index e1c5116a9ac..24da3e5bd30 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -1,13 +1,13 @@ from dateutil.relativedelta import relativedelta -from odoo import api, fields, models +from odoo import fields, models class Property(models.Model): _name = 'estate.property' - _description = 'Estate property' + _description = "Estate property" - name = fields.Char(string='Title', required=True) + name = fields.Char(string="Title", required=True) description = fields.Text() postcode = fields.Char() date_availability = fields.Date( @@ -18,31 +18,29 @@ class Property(models.Model): expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) - living_area = fields.Integer(string='Living Area (sqm)') + living_area = fields.Integer(string="Living Area (sqm)") facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer(string='Garden Area (sqm)') + garden_area = fields.Integer(string="Garden Area (sqm)") garden_orientation = fields.Selection( selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] ) state = fields.Selection( - default='new', + default="new", selection=[ ('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), ('cancelled', 'Cancelled'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), ], required=True, copy=False, ) active = fields.Boolean(default=True) - last_sold_price = fields.Monetary( - string="Last Price", compute='_compute_last_sold_price', readonly=True - ) - - @api.onchange('state') - def _compute_last_sold_price(self): - if self.state == 'sold': - self._compute_last_sold_price = self.selling_price + property_type_id = fields.Many2one('estate.property.type', string="Property Type") + partner_id = fields.Many2one('res.partner', string="Buyer") + user_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', string="Offers") diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py new file mode 100644 index 00000000000..f8d34b709b3 --- /dev/null +++ b/estate/models/property_offer.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class PropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = "Property Offer" + + price = fields.Float() + status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) + partner_id = fields.Many2one('res.partner', required=True) + property_id = fields.Many2one('estate.property', required=True) diff --git a/estate/models/property_tag.py b/estate/models/property_tag.py new file mode 100644 index 00000000000..85bc4e76ef0 --- /dev/null +++ b/estate/models/property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyTag(models.Model): + _name = 'estate.property.tag' + _description = "Property Tag" + + name = fields.Char(required=True) diff --git a/estate/models/property_type.py b/estate/models/property_type.py new file mode 100644 index 00000000000..d1a88f6101f --- /dev/null +++ b/estate/models/property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyType(models.Model): + _name = 'estate.property.type' + _description = "Property Type" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index d9d6ba57cc5..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 +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 2b1661b3800..fd703d86f5a 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,8 +1,12 @@ - + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..8705e983a00 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,38 @@ + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + + + +
+
+
+ + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + Offers + estate.property.offer + list,form + +
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..cc1da268339 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,32 @@ + + + + estate.property.tag.form + estate.property.tag + +
+ + + + + +
+
+
+ + + estate.property.tag.list + estate.property.tag + + + + + + + + + Property Tags + estate.property.tag + list,form + +
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..732d8fc2837 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,34 @@ + + + + estate.property.type.form + estate.property.type + +
+ +
+

+ +

+
+
+
+
+
+ + + estate.property.type.list + estate.property.type + + + + + + + + + Property Types + estate.property.type + list,form + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 98d29a96feb..7566147ae06 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -11,8 +11,13 @@ + + + + + @@ -34,6 +39,15 @@ + + + + + + + + + @@ -46,6 +60,8 @@ + + @@ -62,6 +78,7 @@ + From 8115ce2db479fcc5141bd720cbecccb29bf6c193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Tue, 16 Dec 2025 17:23:24 +0100 Subject: [PATCH 13/25] [IMP] estate: add compute, inverse and onchange methods --- estate/models/property.py | 21 ++++++++++++++++++-- estate/models/property_offer.py | 16 ++++++++++++++- estate/views/estate_property_offer_views.xml | 4 ++++ estate/views/estate_property_views.xml | 2 ++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/estate/models/property.py b/estate/models/property.py index 24da3e5bd30..051755f9709 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -1,6 +1,6 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models class Property(models.Model): @@ -12,7 +12,7 @@ class Property(models.Model): postcode = fields.Char() date_availability = fields.Date( string="Available From", - default=lambda self: fields.Datetime.today() + relativedelta(months=3), + default=lambda self: fields.Date.today() + relativedelta(months=3), copy=False, ) expected_price = fields.Float(required=True) @@ -44,3 +44,20 @@ class Property(models.Model): user_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', string="Offers") + total_area = fields.Integer(string="Total Area (sqm)", 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') + 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 '' diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py index f8d34b709b3..6cfe72e2ab1 100644 --- a/estate/models/property_offer.py +++ b/estate/models/property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models class PropertyOffer(models.Model): @@ -9,3 +11,15 @@ class PropertyOffer(models.Model): status = fields.Selection(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, string="Validity (days)") + deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline') + + @api.depends('validity') + def _compute_deadline(self): + for record in self: + create_date = record.create_date or fields.Date.today() + record.deadline = create_date + relativedelta(days=record.validity) + + def _inverse_deadline(self): + for record in self: + record.validity = (record.deadline - fields.Date.to_date(record.create_date)).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 8705e983a00..09579441e59 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -10,6 +10,8 @@ + + @@ -25,6 +27,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 7566147ae06..5f0a9c8b30b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -23,6 +23,7 @@ + @@ -37,6 +38,7 @@ + From 648390f4e0b7a2546b132969095c575e7adf9980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Wed, 17 Dec 2025 11:24:13 +0100 Subject: [PATCH 14/25] [IMP] estate: add action buttons on property and property_offer --- estate/models/property.py | 26 ++++++++++++++++---- estate/models/property_offer.py | 17 +++++++++++++ estate/views/estate_property_offer_views.xml | 3 ++- estate/views/estate_property_views.xml | 7 +++++- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/estate/models/property.py b/estate/models/property.py index 051755f9709..19c3c52c4e2 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -1,6 +1,7 @@ from dateutil.relativedelta import relativedelta -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError class Property(models.Model): @@ -28,6 +29,7 @@ class Property(models.Model): ) state = fields.Selection( default="new", + string="Status", selection=[ ('new', 'New'), ('offer_received', 'Offer Received'), @@ -40,12 +42,12 @@ class Property(models.Model): ) active = fields.Boolean(default=True) property_type_id = fields.Many2one('estate.property.type', string="Property Type") - partner_id = fields.Many2one('res.partner', string="Buyer") + partner_id = fields.Many2one('res.partner', string="Buyer", readonly=True) user_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', string="Offers") total_area = fields.Integer(string="Total Area (sqm)", compute='_compute_total_area') - best_price = fields.Float(compute='_compute_best_price') + best_offer = fields.Float(compute="_compute_best_offer") @api.depends('living_area', 'garden_area') def _compute_total_area(self): @@ -53,11 +55,25 @@ def _compute_total_area(self): record.total_area = record.living_area + record.garden_area @api.depends('offer_ids') - def _compute_best_price(self): + def _compute_best_offer(self): for record in self: - record.best_price = max(record.offer_ids.mapped('price')) + record.best_offer = 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 '' + + def action_set_sold(self): + if self.state == 'cancelled': + raise UserError(_("Cancelled properties cannot be sold.")) + + self.state = 'sold' + return True + + def action_set_cancelled(self): + if self.state == 'sold': + raise UserError(_("Sold properties cannot be cancelled.")) + + self.state = 'cancelled' + return True diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py index 6cfe72e2ab1..0c28842c64a 100644 --- a/estate/models/property_offer.py +++ b/estate/models/property_offer.py @@ -23,3 +23,20 @@ def _compute_deadline(self): def _inverse_deadline(self): for record in self: record.validity = (record.deadline - fields.Date.to_date(record.create_date)).days + + def action_confirm(self): + self.status = 'accepted' + for offer in self.property_id.offer_ids: + if offer.id == self.id: + continue + + offer.status = 'refused' + + self.property_id.partner_id = self.partner_id + self.property_id.selling_price = self.price + + return True + + def action_refuse(self): + self.status = 'refused' + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 09579441e59..2a2a6f9018a 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -12,7 +12,6 @@ - @@ -29,6 +28,8 @@ + +

+ + + + + + + + + + +
@@ -21,6 +43,7 @@ estate.property.type + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9f3e6c9f91f..59068ff44e3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,8 +6,11 @@
-
@@ -16,13 +19,12 @@
- + - - + @@ -41,13 +43,13 @@ - - + + - + @@ -65,7 +67,8 @@ estate.property.list estate.property - + @@ -74,7 +77,7 @@ - + @@ -89,12 +92,13 @@ - + - - + @@ -104,5 +108,6 @@ Properties estate.property list,form + {'search_default_available': True} From f153bb7aee8e41abefc66d55401cf00fc13ca901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Fri, 19 Dec 2025 09:34:59 +0100 Subject: [PATCH 18/25] [FIX] estate: single quote for technical field --- estate/models/property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/models/property.py b/estate/models/property.py index 9660dfc8ea4..a711b55ffff 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -73,7 +73,7 @@ def _onchange_garden(self): self.garden_area = 10 if self.garden else 0 self.garden_orientation = 'north' if self.garden else '' - @api.onchange("offer_ids") + @api.onchange('offer_ids') def _onchange_state(self): if not self.offer_ids: return From 783a0a0f14b79c4fe32faf8c7af44dfcf89e1f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Fri, 19 Dec 2025 11:48:15 +0100 Subject: [PATCH 19/25] [FIX] estate: change order of data imports to resolve runbot error --- estate/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index f7b3a780d14..72b482a656f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,9 +4,9 @@ 'data': [ 'security/ir.model.access.csv', - 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', 'views/estate_property_offer_views.xml', + 'views/estate_property_type_views.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml', ], From 65fe3662344ed9be60be0e69386d80d82bb72774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Fri, 19 Dec 2025 13:20:28 +0100 Subject: [PATCH 20/25] [IMP] estate: change property state upon creation of offer and add properties assigned to salesman in the users form --- estate/__manifest__.py | 1 + estate/models/__init__.py | 2 +- estate/models/property.py | 17 +++++++++-------- estate/models/property_offer.py | 13 +++++++++++++ estate/models/res_users.py | 7 +++++++ estate/views/res_users_views.xml | 15 +++++++++++++++ 6 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 72b482a656f..d99c0dd1381 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -9,6 +9,7 @@ 'views/estate_property_type_views.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml', + 'views/res_users_views.xml', ], 'installable': True, 'application': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 22c7e2d2c5b..cf76e15f1ce 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import property, property_offer, property_tag, property_type +from . import property, property_offer, property_tag, property_type, res_users diff --git a/estate/models/property.py b/estate/models/property.py index a711b55ffff..9dbe30b9a94 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -8,7 +8,7 @@ class Property(models.Model): _name = 'estate.property' _description = "Estate property" - _order = "id desc" + _order = 'id desc' name = fields.Char(string="Title", required=True) description = fields.Text() @@ -73,13 +73,6 @@ def _onchange_garden(self): self.garden_area = 10 if self.garden else 0 self.garden_orientation = 'north' if self.garden else '' - @api.onchange('offer_ids') - def _onchange_state(self): - if not self.offer_ids: - return - - self.state = 'offer_received' - @api.constrains('selling_price', 'expected_price') def _check_selling_price(self): for record in self: @@ -107,3 +100,11 @@ def action_set_cancelled(self): self.state = 'cancelled' return True + + @api.ondelete(at_uninstall=False) + def _unlink_except_new_or_cancelled(self): + for record in self: + if record.state not in ['new', 'cancelled']: + raise UserError( + self.env._("Only new and cancelled properties can be deleted.") + ) diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py index 2cc28a600c5..fe1c6949ade 100644 --- a/estate/models/property_offer.py +++ b/estate/models/property_offer.py @@ -1,6 +1,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.exceptions import UserError class PropertyOffer(models.Model): @@ -46,3 +47,15 @@ def action_confirm(self): def action_refuse(self): self.status = 'refused' return True + + @api.model_create_multi + def create(self, vals): + for record in vals: + property = self.env['estate.property'].browse(record['property_id']) + min_price = min(property.offer_ids.mapped('price')) if property.offer_ids else 0.0 + if record['price'] < min_price: + raise UserError(self.env._("The offer must be higher than %d.", min_price)) + + property.state = 'offer_received' + + return super(PropertyOffer, self).create(vals) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..25bc8157430 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many('estate.property', 'user_id', string="Properties", domain="[('active', '=', 'True')]") diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..4496b1fd673 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + From dc2f8deb21ff027da08dd29264afdf8251839eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Fri, 19 Dec 2025 13:23:19 +0100 Subject: [PATCH 21/25] [IMP] estate: remove buttons from view if property is cancelled or sold --- estate/views/estate_property_views.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 59068ff44e3..f9240518ab0 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -7,9 +7,9 @@
From 06d9f41f53de13189fa21229ee1a490216aeb358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Fri, 19 Dec 2025 13:34:34 +0100 Subject: [PATCH 22/25] [FIX] estate: change super call --- estate/models/property_offer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py index fe1c6949ade..aa8861596de 100644 --- a/estate/models/property_offer.py +++ b/estate/models/property_offer.py @@ -58,4 +58,4 @@ def create(self, vals): property.state = 'offer_received' - return super(PropertyOffer, self).create(vals) + return super().create(vals) From 0e9783dadd81c29573bd43a611c6763040c6508c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Fri, 19 Dec 2025 13:36:54 +0100 Subject: [PATCH 23/25] [FIX] estate: remove double specification of properties --- estate/models/res_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/models/res_users.py b/estate/models/res_users.py index 25bc8157430..e8176a5c886 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -4,4 +4,4 @@ class ResUsers(models.Model): _inherit = 'res.users' - property_ids = fields.One2many('estate.property', 'user_id', string="Properties", domain="[('active', '=', 'True')]") + property_ids = fields.One2many('estate.property', 'user_id', domain="[('active', '=', 'True')]") From 6b3d3a2e0732093b7bf12a40cf54f40934639171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Fri, 19 Dec 2025 14:37:19 +0100 Subject: [PATCH 24/25] [FIX] estate: split import on separate lines --- estate/models/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/estate/models/__init__.py b/estate/models/__init__.py index cf76e15f1ce..cfb3cd728e5 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,5 @@ -from . import property, property_offer, property_tag, property_type, res_users +from . import property +from . import property_offer +from . import property_tag +from . import property_type +from . import res_users From 8ff4802f4702ba5ad2df88044717e41326a12fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Fri, 19 Dec 2025 14:38:23 +0100 Subject: [PATCH 25/25] [ADD] estate_account: extend `estate` to create an invoice when property is sold --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 7 +++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 16 ++++++++++++++++ 4 files changed, 25 insertions(+) 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_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..5842ae8d70c --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,7 @@ +{ + 'name': 'Real Estate Account', + 'depends': ['base', 'estate', 'account'], + 'author': "Odoo", + 'installable': True, + 'license': 'AGPL-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..33f32940423 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,16 @@ +from odoo import Command, models + + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + def action_set_sold(self): + self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_id.id, + 'line_ids': [ + Command.create({'name': self.name, 'quantity': 1, 'price_unit': 0.06 * self.selling_price}), + Command.create({'name': "Administrative fees", 'quantity': 1, 'price_unit': 100}), + ] + }) + return super().action_set_sold()