From 1d9543bceb7e39384013dc8350ec2c2bfce2176c Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Mon, 19 Jan 2026 15:42:02 +0100 Subject: [PATCH 01/37] [ADD] estate: create a new module for Real Estate Advertisement Creation of a new module estate that covers Real Estate Advertisement. There is no module that answer this business case by default. --- estate/__init__.py | 1 + estate/__manifest__.py | 7 +++++++ 2 files changed, 8 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..7c68785e9d0 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..24c2323dc1a --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Real estate", + 'depends': ['base'], + 'category': 'Tutorials', + 'application': True +} \ No newline at end of file From 03dde4d7eb4482476863144d85fedb4b6fd71634 Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Mon, 19 Jan 2026 16:50:13 +0100 Subject: [PATCH 02/37] [IMP] estate: add property model This adds property model to the estate module with the objective to store the informations related to the property. --- estate/__init__.py | 4 +++- estate/models/__init__.py | 1 + estate/models/property.py | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) 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 7c68785e9d0..5305644df14 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1,3 @@ -# -*- coding: utf-8 -*- \ No newline at end of file +# -*- coding: utf-8 -*- + +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..8d6e8c1fef0 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import property \ No newline at end of file diff --git a/estate/models/property.py b/estate/models/property.py new file mode 100644 index 00000000000..dd1d260e36c --- /dev/null +++ b/estate/models/property.py @@ -0,0 +1,5 @@ +from odoo import models + +class EstateProperty(models.Model): + _name = "estate_property" + _description = "estate property" \ No newline at end of file From b6e4d0697bef2c121f3cdcab2785daf4b18d81ab Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Mon, 19 Jan 2026 17:45:06 +0100 Subject: [PATCH 03/37] [IMP] estate: add informations field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds fields to store information related to the property (name, description, price, living area…) --- estate/models/property.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/estate/models/property.py b/estate/models/property.py index dd1d260e36c..3727a067260 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -1,5 +1,19 @@ -from odoo import models +from odoo import fields, models class EstateProperty(models.Model): _name = "estate_property" - _description = "estate property" \ No newline at end of file + _description = "estate property" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date(string="Date availability") + expected_price = fields.Float(string="Expected price", required=True) + selling_price = fields.Float(string="Selling price") + bedrooms = fields.Integer() + living_area = fields.Integer(string="Living area") + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer(string="Garden area") + garden_orientation = fields.Selection(string="Garden orientation", selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) \ No newline at end of file From b2ac4fafdac7a7d2fc43bc5692cd3b754b5330ac Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Tue, 20 Jan 2026 09:39:09 +0100 Subject: [PATCH 04/37] [CLN] estate: make code more conventional This add a new line at the end of every file to be easier to read. This changes strings defined by double quotes to strings with single quotes. This also uses two blank lines instead of one before declaring a class --- estate/__init__.py | 2 +- estate/__manifest__.py | 5 +++-- estate/models/__init__.py | 2 +- estate/models/property.py | 18 ++++++++++-------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/estate/__init__.py b/estate/__init__.py index 5305644df14..cde864bae21 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import models \ No newline at end of file +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 24c2323dc1a..bbe8ded3520 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,5 +3,6 @@ 'name': "Real estate", 'depends': ['base'], 'category': 'Tutorials', - 'application': True -} \ No newline at end of file + 'application': True, + 'data': ['security/ir.model.access.csv'] +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8d6e8c1fef0..8120b005bb6 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import property \ No newline at end of file +from . import property diff --git a/estate/models/property.py b/estate/models/property.py index 3727a067260..43e4057b9f7 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -1,19 +1,21 @@ from odoo import fields, models + class EstateProperty(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(string="Date availability") - expected_price = fields.Float(string="Expected price", required=True) - selling_price = fields.Float(string="Selling price") + date_availability = fields.Date(string='Date availability') + expected_price = fields.Float(string='Expected price', required=True) + selling_price = fields.Float(string='Selling price') bedrooms = fields.Integer() - living_area = fields.Integer(string="Living area") + living_area = fields.Integer(string='Living area') facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer(string="Garden area") - garden_orientation = fields.Selection(string="Garden orientation", selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) \ No newline at end of file + garden_area = fields.Integer(string='Garden area') + garden_orientation = fields.Selection(string='Garden orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) + \ No newline at end of file From bfd650f5cdf25f068840370b30ed909443787014 Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Tue, 20 Jan 2026 09:44:47 +0100 Subject: [PATCH 05/37] [IMP] give access rights to base.group_user This gives access rights to the group base.group_user. This is needed because no access rights was defined so no users can access the data. --- estate/security/ir.model.access.csv | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..fb17a986a7d --- /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_ir_group_user,ir_group_user,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From 3490325c9a8c1d8fb9ff22af1b11587073a15a4c Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Tue, 20 Jan 2026 11:13:03 +0100 Subject: [PATCH 06/37] [IMP] estate: add a 3 levels architecture and an action (Chapter 5) This creates a 3 levels architecture with an action to interact with property model. --- estate/__manifest__.py | 6 +++++- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) 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 bbe8ded3520..f1101e377ba 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,5 +4,9 @@ 'depends': ['base'], 'category': 'Tutorials', 'application': True, - 'data': ['security/ir.model.access.csv'] + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_menus.xml', + 'views/estate_property_views.xml', + ] } diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..3d6d7492b26 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..a19d9e1a6d7 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Property action + estate_property + list,form + + \ No newline at end of file From b330f0eb5bb4cc3c08a580912dcb83152b7700cf Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Tue, 20 Jan 2026 11:32:04 +0100 Subject: [PATCH 07/37] [IMP] estate: change attributes of some fields This changes attributes of selling price to read-only and prevents the copy of availability date and selling price fields. --- estate/models/property.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/estate/models/property.py b/estate/models/property.py index 43e4057b9f7..d912125435f 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -5,17 +5,16 @@ class EstateProperty(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(string='Date availability') + date_availability = fields.Date(string='Date availability', copy=False) expected_price = fields.Float(string='Expected price', required=True) - selling_price = fields.Float(string='Selling price') + selling_price = fields.Float(string='Selling price', readonly=True, copy=False) bedrooms = fields.Integer() - living_area = fields.Integer(string='Living area') + living_area = fields.Integer(string='Living area (sqm)') facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer(string='Garden area') + garden_area = fields.Integer(string='Garden area (sqm)') garden_orientation = fields.Selection(string='Garden orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) - \ No newline at end of file From c7b8c7f687fa024175f007e28d1ab31d3b6581bb Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Tue, 20 Jan 2026 12:11:55 +0100 Subject: [PATCH 08/37] [IMP] estate: add new fields and attributes This adds a default value for the bedrooms and the availability date. This also add an active and a state field. --- estate/models/property.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/estate/models/property.py b/estate/models/property.py index d912125435f..310edc3cfcb 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -8,13 +8,16 @@ class EstateProperty(models.Model): name = fields.Char(string='Title', required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date(string='Date availability', copy=False) + date_availability = fields.Date(string='Date availability', copy=False, default=fields.Date.add(fields.Date.today(), months=3)) expected_price = fields.Float(string='Expected price', required=True) selling_price = fields.Float(string='Selling price', readonly=True, copy=False) - bedrooms = fields.Integer() + bedrooms = fields.Integer(default=2) 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_orientation = fields.Selection(string='Garden orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) + active = fields.Boolean(default=True) + state = fields.Selection(selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], + required=True, copy=False, default='new') \ No newline at end of file From e97d00073535ff041236db14b2a1a425c3b7e774 Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Tue, 20 Jan 2026 13:01:28 +0100 Subject: [PATCH 09/37] [CLN] estate: clean some indentations and useless lines This cleans the code based on robodoo recommendations by removing some lines and correcting wrong indentations. --- estate/__init__.py | 2 -- estate/__manifest__.py | 1 - estate/models/property.py | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/estate/__init__.py b/estate/__init__.py index cde864bae21..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index f1101e377ba..70c9a6f2a75 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- { 'name': "Real estate", 'depends': ['base'], diff --git a/estate/models/property.py b/estate/models/property.py index 310edc3cfcb..54b83fa6295 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -19,5 +19,5 @@ class EstateProperty(models.Model): garden_area = fields.Integer(string='Garden area (sqm)') garden_orientation = fields.Selection(string='Garden orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) active = fields.Boolean(default=True) - state = fields.Selection(selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], - required=True, copy=False, default='new') \ No newline at end of file + state = fields.Selection(selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], required=True, copy=False, default='new') + \ No newline at end of file From f7fb822c20988aa031f4c8f4f1e9ff8c97fc962a Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Tue, 20 Jan 2026 14:56:33 +0100 Subject: [PATCH 10/37] [IMP] estate: custom view for list and form (Chapter 6) This adds new column names to the list view and a new form view according to the required specifications. --- estate/models/property.py | 13 +++--- estate/views/estate_menus.xml | 2 +- estate/views/estate_property_views.xml | 62 +++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/estate/models/property.py b/estate/models/property.py index 54b83fa6295..31553b89421 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -8,16 +8,15 @@ class EstateProperty(models.Model): name = fields.Char(string='Title', required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date(string='Date availability', copy=False, default=fields.Date.add(fields.Date.today(), months=3)) - expected_price = fields.Float(string='Expected price', required=True) - selling_price = fields.Float(string='Selling price', readonly=True, copy=False) + date_availability = fields.Date(string='Available From', copy=False, default=fields.Date.add(fields.Date.today(), 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(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_orientation = fields.Selection(string='Garden orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) + garden_area = fields.Integer(string='Garden Area (sqm)') + garden_orientation = fields.Selection(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) active = fields.Boolean(default=True) state = fields.Selection(selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], required=True, copy=False, default='new') - \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 3d6d7492b26..1d5815d0fce 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index a19d9e1a6d7..e0f881a5a72 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,8 +1,68 @@ + Property action estate_property list,form - \ No newline at end of file + + + estate_property_list + estate_property + + + + + + + + + + + + + + + estate_property_form + estate_property + +
+ + +
+

+ +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + From 00761e09bcac7aad33d6fe70479f763efc8d557a Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Tue, 20 Jan 2026 15:43:04 +0100 Subject: [PATCH 11/37] [IMP] estate: add search options (chapter 6) This creates new search fields, a filter for available properties and the possibility to group the properties by postcode. --- estate/views/estate_property_views.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e0f881a5a72..0d6b6bc4aae 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -41,6 +41,7 @@ + @@ -65,4 +66,21 @@ + + estate_property_search + estate_property + + + + + + + + + + + + + + From 2b04b992276a35459d94ec5653e7c0bd12de6c3d Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Tue, 20 Jan 2026 17:18:55 +0100 Subject: [PATCH 12/37] [IMP] estate: add new module for property type (Chapter 7) This creates a new module for the type of a property. The type of a property record can be observerd in list and form views. --- estate/__manifest__.py | 3 ++- estate/models/__init__.py | 1 + estate/models/property.py | 3 +++ estate/models/property_type.py | 8 ++++++ estate/security/ir.model.access.csv | 3 ++- estate/views/estate_menus.xml | 3 +++ estate/views/estate_property_type_views.xml | 28 +++++++++++++++++++++ estate/views/estate_property_views.xml | 7 ++++++ 8 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 estate/models/property_type.py create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 70c9a6f2a75..8b1ae5da616 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,7 +5,8 @@ 'application': True, 'data': [ 'security/ir.model.access.csv', - 'views/estate_menus.xml', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_menus.xml', ] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8120b005bb6..4de9636a4c2 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,2 @@ from . import property +from . import property_type \ No newline at end of file diff --git a/estate/models/property.py b/estate/models/property.py index 31553b89421..570501ef1f5 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -20,3 +20,6 @@ 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(selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], required=True, copy=False, default='new') + + property_type_id = fields.Many2one('estate_property_type', string='Property Type') + \ No newline at end of file diff --git a/estate/models/property_type.py b/estate/models/property_type.py new file mode 100644 index 00000000000..2600e11d046 --- /dev/null +++ b/estate/models/property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate_property_type' + _description = 'estate property type' + + name = fields.Char(string='Type', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index fb17a986a7d..45cc4696a2b 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_ir_group_user,ir_group_user,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_ir_group_user,ir_group_user,model_estate_property,base.group_user,1,1,1,1 +access_ir_group_user_type,ir_group_user_type,model_estate_property_type,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 1d5815d0fce..a5ef8211df2 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,8 @@ + + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..9cde8c8f1c1 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,28 @@ + + + + + Property type action + estate_property_type + list,form + + + + estate_property_type_form + estate_property_type + +
+ + +
+

+ +

+
+
+
+
+
+
+ +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 0d6b6bc4aae..6537e5408b7 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -19,6 +19,7 @@ + @@ -38,6 +39,7 @@
+ @@ -60,6 +62,11 @@ + + + From 476383d437cb485a4eca59aaf94d25670d74e90c Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Wed, 21 Jan 2026 09:39:10 +0100 Subject: [PATCH 13/37] [IMP] estate: add salesperson and buyer as field This adds add salesperson and buyer as field of the property module. This also adds salesperson and buyer in the property form. --- .gitignore | 3 +++ estate/__manifest__.py | 3 ++- estate/models/property.py | 4 +++- estate/views/estate_property_views.xml | 7 ++++--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index b6e47617de1..fbc0c865e38 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Not functionnal +estate/security/security.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8b1ae5da616..7473c1e76ee 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,9 +1,10 @@ { 'name': "Real estate", 'depends': ['base'], - 'category': 'Tutorials', + 'category': 'Real Estate/Brokerage', 'application': True, 'data': [ + # 'security/security.xml', 'security/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_property_type_views.xml', diff --git a/estate/models/property.py b/estate/models/property.py index 570501ef1f5..c0cb79e470f 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -22,4 +22,6 @@ class EstateProperty(models.Model): state = fields.Selection(selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], required=True, copy=False, default='new') property_type_id = fields.Many2one('estate_property_type', string='Property Type') - \ No newline at end of file + + salesperson = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) + buyer = fields.Many2one('res.partner', copy=False) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 6537e5408b7..9e0c3343f85 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -63,9 +63,10 @@ - + + + + From af15929a5c755e7170f8cb5fbf0db01305e5ca0f Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Wed, 21 Jan 2026 10:19:22 +0100 Subject: [PATCH 14/37] [IMP] estate: add tags to property model (Chapter 7) This creates a new model for tags and adds a many2many field to property model. This new field is visible in list and form views. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 3 ++- estate/models/property.py | 2 ++ estate/models/property_tag.py | 8 ++++++++ estate/security/ir.model.access.csv | 3 ++- estate/views/estate_menus.xml | 1 + estate/views/estate_property_tag_views.xml | 24 ++++++++++++++++++++++ estate/views/estate_property_views.xml | 4 ++++ 8 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 estate/models/property_tag.py create mode 100644 estate/views/estate_property_tag_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 7473c1e76ee..8b36b056b4d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,6 +8,7 @@ 'security/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', 'views/estate_menus.xml', ] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 4de9636a4c2..bb1c1809b80 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,3 @@ from . import property -from . import property_type \ No newline at end of file +from . import property_type +from . import property_tag diff --git a/estate/models/property.py b/estate/models/property.py index c0cb79e470f..3b17b99d091 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -25,3 +25,5 @@ class EstateProperty(models.Model): salesperson = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) buyer = fields.Many2one('res.partner', copy=False) + + property_tag_id = fields.Many2many('estate_property_tag', string='Property Tag') diff --git a/estate/models/property_tag.py b/estate/models/property_tag.py new file mode 100644 index 00000000000..1322680e82a --- /dev/null +++ b/estate/models/property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate_property_tag' + _description = 'estate property tag' + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 45cc4696a2b..f6513ace035 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink access_ir_group_user,ir_group_user,model_estate_property,base.group_user,1,1,1,1 -access_ir_group_user_type,ir_group_user_type,model_estate_property_type,base.group_user,1,1,1,1 \ No newline at end of file +access_ir_group_user_type,ir_group_user_type,model_estate_property_type,base.group_user,1,1,1,1 +access_ir_group_user_tag,ir_group_user_tag,model_estate_property_tag,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index a5ef8211df2..28b1018038b 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -6,6 +6,7 @@ + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..db8250a7b0f --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,24 @@ + + + + + Property tag action + estate_property_tag + list,form + + + + estate_property_tag_form + estate_property_tag + +
+ + + + + +
+
+
+ +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9e0c3343f85..6518d87cf2e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -20,6 +20,7 @@ + @@ -37,6 +38,9 @@ + + + From 39246b2ba1c8866257d8228686bf3e0ce44cde86 Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Wed, 21 Jan 2026 11:05:59 +0100 Subject: [PATCH 15/37] [IMP] estate: add offer model (Chapter 7) This creates an offer model and adds it as a one2many field for the property model. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/property.py | 4 ++- estate/models/property_offer.py | 11 +++++++ estate/security/ir.model.access.csv | 3 +- estate/views/estate_property_offer_views.xml | 32 ++++++++++++++++++++ estate/views/estate_property_views.xml | 5 +++ 7 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 estate/models/property_offer.py create mode 100644 estate/views/estate_property_offer_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8b36b056b4d..e06e5d6e8d2 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -9,6 +9,7 @@ 'views/estate_property_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index bb1c1809b80..9d31a3a6bcb 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,3 +1,4 @@ from . import property from . import property_type from . import property_tag +from . import property_offer diff --git a/estate/models/property.py b/estate/models/property.py index 3b17b99d091..16257ef040b 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -20,10 +20,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(selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], required=True, copy=False, default='new') - + property_type_id = fields.Many2one('estate_property_type', string='Property Type') salesperson = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) buyer = fields.Many2one('res.partner', copy=False) property_tag_id = fields.Many2many('estate_property_tag', string='Property Tag') + + property_offer_tag = fields.One2many('estate_property_offer', 'property_id', string='Property Offer') diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py new file mode 100644 index 00000000000..3bd8473132b --- /dev/null +++ b/estate/models/property_offer.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = 'estate_property_offer' + _description = 'estate property offer' + + price = fields.Float() + status = fields.Selection(selection=(('accepted', 'Accepted'), ('refused', 'Refused')), copy=False) + partner_id = fields.Many2one('res.partner', required=True, string='Partner') + property_id = fields.Many2one('estate_property', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index f6513ace035..8fe4e67d688 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,4 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink access_ir_group_user,ir_group_user,model_estate_property,base.group_user,1,1,1,1 access_ir_group_user_type,ir_group_user_type,model_estate_property_type,base.group_user,1,1,1,1 -access_ir_group_user_tag,ir_group_user_tag,model_estate_property_tag,base.group_user,1,1,1,1 \ No newline at end of file +access_ir_group_user_tag,ir_group_user_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_ir_group_user_offer,ir_group_user_offer,model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..bf3b1868fd9 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,32 @@ + + + + + estate_property_offer_list + estate_property_offer + + + + + + + + + + + estate_property_view_form + estate_property_offer + +
+ + + + + + + +
+
+
+ +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 6518d87cf2e..5fac6feb5a6 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -66,6 +66,11 @@
+ + + + + From 4d9b290a6f5584cf4fb4baa147f0a58c0654b2a9 Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Wed, 21 Jan 2026 13:29:28 +0100 Subject: [PATCH 16/37] [IMP] estate: add total_area and best_offer fields (chapter 8) This creates computed fields (total_area and best_offer) that are visible in the form view of the estate_property model. --- estate/__manifest__.py | 4 +++- estate/models/property.py | 19 +++++++++++++++++-- estate/models/property_offer.py | 2 +- estate/views/estate_property_views.xml | 4 +++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index e06e5d6e8d2..12a16cd1fb7 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,5 +1,7 @@ { - 'name': "Real estate", + 'name': 'Real estate', + 'author': 'anden', + 'license': 'LGPL-3', 'depends': ['base'], 'category': 'Real Estate/Brokerage', 'application': True, diff --git a/estate/models/property.py b/estate/models/property.py index 16257ef040b..54152e2da5f 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -28,4 +28,19 @@ class EstateProperty(models.Model): property_tag_id = fields.Many2many('estate_property_tag', string='Property Tag') - property_offer_tag = fields.One2many('estate_property_offer', 'property_id', string='Property Offer') + property_offer_id = fields.One2many('estate_property_offer', 'property_id', string='Property Offer') + + total_area = fields.Float(string="Total Area (sqm)", compute='_compute_area') + + @api.depends('living_area', 'garden_area') + def _compute_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + best_price = fields.Float(string="Best offer", compute='_compute_best_price') + + @api.depends('property_offer_id.price') + def _compute_best_price(self): + best_price_compute = max(self.mapped('property_offer_id.price'), default=0) + for record in self: + record.best_price = best_price_compute diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py index 3bd8473132b..082d9812aee 100644 --- a/estate/models/property_offer.py +++ b/estate/models/property_offer.py @@ -6,6 +6,6 @@ class EstatePropertyOffer(models.Model): _description = 'estate property offer' price = fields.Float() - status = fields.Selection(selection=(('accepted', 'Accepted'), ('refused', 'Refused')), copy=False) + status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) partner_id = fields.Many2one('res.partner', required=True, string='Partner') property_id = fields.Many2one('estate_property', required=True) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 5fac6feb5a6..d666f3d186c 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -50,6 +50,7 @@ + @@ -64,11 +65,12 @@ +
- + From 7e72d0add341d5a26a1830a240991fe17e9bf52c Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Wed, 21 Jan 2026 14:43:12 +0100 Subject: [PATCH 17/37] [IMP] estate: add fields for validity and deadline (Chapter 8) This creates two new fields to compute deadline from validity and the inverse way. --- estate/models/property_offer.py | 20 +++++++++++++++++++- estate/views/estate_property_offer_views.xml | 5 ++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py index 082d9812aee..ceade71527e 100644 --- a/estate/models/property_offer.py +++ b/estate/models/property_offer.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -9,3 +9,21 @@ class EstatePropertyOffer(models.Model): status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) partner_id = fields.Many2one('res.partner', required=True, string='Partner') property_id = fields.Many2one('estate_property', required=True) + + validity = fields.Integer(string='Validity (days)', default=7) + date_deadline = fields.Date(string='Deadline', compute="_compute_deadline", inverse="_inverse_deadline") + + @api.depends('create_date', 'validity') + def _compute_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = fields.Date.add(record.create_date.date(), days=record.validity) + else: + record.date_deadline = fields.Date.add(fields.Date.today(), days=record.validity) # If no create_date we take the date of today + + def _inverse_deadline(self): + for record in self: + if record.create_date: + record.validity = (record.date_deadline - record.create_date.date()).days + else: + record.validity = (record.date_deadline - fields.Date.today()).days # If no create_date we take the date of today diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index bf3b1868fd9..ec0a5a44fd1 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -8,7 +8,8 @@ - + +
@@ -22,6 +23,8 @@ + + From 94a7e2ec85f88ee1bfade991cc85136b494fa9cb Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Wed, 21 Jan 2026 14:51:18 +0100 Subject: [PATCH 18/37] [CLN] estate: clean fields declaration This cleans the fields declaration based on the review. --- estate/models/property.py | 19 +++++++++++++------ estate/models/property_tag.py | 2 +- estate/security/ir.model.access.csv | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/estate/models/property.py b/estate/models/property.py index 54152e2da5f..c8a92301bd5 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -19,15 +19,22 @@ class EstateProperty(models.Model): garden_area = fields.Integer(string='Garden Area (sqm)') garden_orientation = fields.Selection(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) active = fields.Boolean(default=True) - state = fields.Selection(selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], required=True, copy=False, default='new') - + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer received', 'Offer Received'), + ('offer accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], + required=True, + copy=False, + default='new' + ) property_type_id = fields.Many2one('estate_property_type', string='Property Type') - salesperson = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) buyer = fields.Many2one('res.partner', copy=False) - property_tag_id = fields.Many2many('estate_property_tag', string='Property Tag') - property_offer_id = fields.One2many('estate_property_offer', 'property_id', string='Property Offer') total_area = fields.Float(string="Total Area (sqm)", compute='_compute_area') @@ -38,7 +45,7 @@ def _compute_area(self): record.total_area = record.living_area + record.garden_area best_price = fields.Float(string="Best offer", compute='_compute_best_price') - + @api.depends('property_offer_id.price') def _compute_best_price(self): best_price_compute = max(self.mapped('property_offer_id.price'), default=0) diff --git a/estate/models/property_tag.py b/estate/models/property_tag.py index 1322680e82a..f7d126c6797 100644 --- a/estate/models/property_tag.py +++ b/estate/models/property_tag.py @@ -1,7 +1,7 @@ from odoo import fields, models -class EstatePropertyType(models.Model): +class EstatePropertyTag(models.Model): _name = 'estate_property_tag' _description = 'estate property tag' diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 8fe4e67d688..ca416c8057c 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -2,4 +2,4 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink access_ir_group_user,ir_group_user,model_estate_property,base.group_user,1,1,1,1 access_ir_group_user_type,ir_group_user_type,model_estate_property_type,base.group_user,1,1,1,1 access_ir_group_user_tag,ir_group_user_tag,model_estate_property_tag,base.group_user,1,1,1,1 -access_ir_group_user_offer,ir_group_user_offer,model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file +access_ir_group_user_offer,ir_group_user_offer,model_estate_property_offer,base.group_user,1,1,1,1 From 71bbdcd7e1be178cdfc082641d59911aa6a27efd Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Wed, 21 Jan 2026 17:07:00 +0100 Subject: [PATCH 19/37] [IMP] estate: add buttons to property form view (Chapter 9) This add sold and cancel button on the property form view to change the status. This also add accept/refuse button on the offer list view to change the status of an offer (we can accept only if no other offers are accepted). --- estate/models/property.py | 42 ++++++++++++++++---- estate/models/property_offer.py | 18 ++++++--- estate/views/estate_property_offer_views.xml | 3 ++ estate/views/estate_property_views.xml | 5 +++ 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/estate/models/property.py b/estate/models/property.py index c8a92301bd5..9a9c2381fe1 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models +from odoo import api, fields, models, exceptions class EstateProperty(models.Model): @@ -10,7 +10,7 @@ class EstateProperty(models.Model): postcode = fields.Char() date_availability = fields.Date(string='Available From', copy=False, default=fields.Date.add(fields.Date.today(), months=3)) expected_price = fields.Float(required=True) - selling_price = fields.Float(readonly=True, copy=False) + selling_price = fields.Float(readonly=True, copy=False, compute='_compute_selling_price') bedrooms = fields.Integer(default=2) living_area = fields.Integer(string='Living Area (sqm)') facades = fields.Integer() @@ -20,6 +20,7 @@ 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'), @@ -29,25 +30,52 @@ class EstateProperty(models.Model): ], required=True, copy=False, - default='new' + default='new', + readonly=True ) property_type_id = fields.Many2one('estate_property_type', string='Property Type') salesperson = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) buyer = fields.Many2one('res.partner', copy=False) property_tag_id = fields.Many2many('estate_property_tag', string='Property Tag') property_offer_id = fields.One2many('estate_property_offer', 'property_id', string='Property Offer') - - total_area = fields.Float(string="Total Area (sqm)", compute='_compute_area') + total_area = fields.Float(string='Total Area (sqm)', compute='_compute_area') + best_price = fields.Float(string='Best offer', compute='_compute_best_price') @api.depends('living_area', 'garden_area') def _compute_area(self): for record in self: record.total_area = record.living_area + record.garden_area - best_price = fields.Float(string="Best offer", compute='_compute_best_price') - @api.depends('property_offer_id.price') def _compute_best_price(self): best_price_compute = max(self.mapped('property_offer_id.price'), default=0) for record in self: record.best_price = best_price_compute + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = False + + def action_property_sold(self): + if self.state == 'cancelled': + raise exceptions.UserError('Cancelled properties cannot be sold') + else: + self.state = 'sold' + + + def action_property_cancel(self): + if self.state == 'sold': + raise exceptions.UserError('Sold properties cannot be cancelled') + else: + self.state = 'cancelled' + + @api.depends('property_offer_id.status') + def _compute_selling_price(self): + accepted_property_offers = self.property_offer_id.search([('status', '=', 'accepted')]) + # Selling price as the max of accepted offers (the problem of multiple accepted offers will be solved with constraints in the next chapter) + self.selling_price = max(accepted_property_offers.mapped('price'), default=0.0) diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py index ceade71527e..09b65aaf4b3 100644 --- a/estate/models/property_offer.py +++ b/estate/models/property_offer.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models +from odoo import api, fields, models, exceptions class EstatePropertyOffer(models.Model): @@ -9,9 +9,8 @@ class EstatePropertyOffer(models.Model): status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) partner_id = fields.Many2one('res.partner', required=True, string='Partner') property_id = fields.Many2one('estate_property', required=True) - validity = fields.Integer(string='Validity (days)', default=7) - date_deadline = fields.Date(string='Deadline', compute="_compute_deadline", inverse="_inverse_deadline") + date_deadline = fields.Date(string='Deadline', compute='_compute_deadline', inverse='_inverse_deadline') @api.depends('create_date', 'validity') def _compute_deadline(self): @@ -19,11 +18,20 @@ def _compute_deadline(self): if record.create_date: record.date_deadline = fields.Date.add(record.create_date.date(), days=record.validity) else: - record.date_deadline = fields.Date.add(fields.Date.today(), days=record.validity) # If no create_date we take the date of today + record.date_deadline = fields.Date.add(fields.Date.today(), days=record.validity) # If no create_date we take the date of today def _inverse_deadline(self): for record in self: if record.create_date: record.validity = (record.date_deadline - record.create_date.date()).days else: - record.validity = (record.date_deadline - fields.Date.today()).days # If no create_date we take the date of today + record.validity = (record.date_deadline - fields.Date.today()).days # If no create_date we take the date of today + + def action_status_accepted(self): + if 'accepted' not in self.mapped('property_id.property_offer_id.status'): + self.status = 'accepted' + elif self.status != 'accepted': + raise exceptions.UserError('Another offer is already accepted') + + def action_status_refused(self): + self.status = 'refused' diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index ec0a5a44fd1..2bb1388be3c 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -10,6 +10,9 @@ + +

From 7a27ef5bef3b345d80f1dbbe8770b9b361a917af Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Mon, 26 Jan 2026 10:24:57 +0100 Subject: [PATCH 32/37] [IMP] estate: prevent deletion for some property states (Chapter 12) This prevents the deletion of a property if its state is 'New' or 'Cancelled'. This also adds a raise when we try to create an offer with a lower amount than another offer. --- estate/models/property.py | 5 +++++ estate/models/property_offer.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/estate/models/property.py b/estate/models/property.py index 1a3e06053e3..1768ee6cfdf 100644 --- a/estate/models/property.py +++ b/estate/models/property.py @@ -99,3 +99,8 @@ def _onchange_property_offer_id(self): record.state = 'offer received' elif record.state != 'cancelled': record.state = 'new' + + @api.ondelete(at_uninstall=False) + def _unlink_for_specific_state(self): + if any(record.state not in ('new', 'cancelled') for record in self): + raise exceptions.UserError('Can\'t delete a property if its state is not \'New\' or \'Cancelled\'') diff --git a/estate/models/property_offer.py b/estate/models/property_offer.py index 10dd8a09393..e1a5f720f64 100644 --- a/estate/models/property_offer.py +++ b/estate/models/property_offer.py @@ -53,3 +53,13 @@ def action_status_refused(self): _check_offer_price = models.Constraint( 'CHECK(0 < price)', 'An offer price must be strictly positive') + + @api.model + def create(self, vals_list): + + for vals in vals_list: + max_existing_price = max((offer.price for offer in self.env['estate_property'].browse(vals_list[0]['property_id']).property_offer_id), default=0) + if vals['price'] < max_existing_price: + raise exceptions.UserError('The offer must be higher than ' + str(max_existing_price)) + + return super().create(vals_list) From da2d02d1d246271d9564b3808d5762b4bc05a497 Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Mon, 26 Jan 2026 11:26:22 +0100 Subject: [PATCH 33/37] [IMP] add properties to users form (Chapter 12) This adds a 'Real Estate Properties' page to the user form. This shows the properties for which the user is the salesman. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/res_users.py | 7 +++++++ estate/views/res_users_views.xml | 19 +++++++++++++++++++ 4 files changed, 28 insertions(+) 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 bc992b58d7e..fffad6b4e40 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,6 +12,7 @@ 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', + 'views/res_users_views.xml', 'views/estate_menus.xml', ] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 9d31a3a6bcb..d7ceb496724 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import property_type from . import property_tag from . import property_offer +from . import res_users diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..d8eb99916e2 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + +class ResUsers(models.Model): + pass + _inherit = ["res.users"] + + property_ids = fields.One2many('estate_property', 'salesperson', domain=[('state', 'in', ('new', 'offer received'))]) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..8643245b69c --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,19 @@ + + + + + + res_users_view_form_inherit_estate_property + res.users + + + + + + + + + + + + \ No newline at end of file From b606da36d072040ac2250f891b0c6e0d6f16b088 Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Mon, 26 Jan 2026 14:12:04 +0100 Subject: [PATCH 34/37] [IMP] estate_account: add automatic invoice (Chapter 13) This creates an invoice for the buyer when clicking on the button 'Sold'. --- estate/models/res_users.py | 3 ++- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 9 +++++++++ estate_account/models/__init__.py | 1 + estate_account/models/property.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) 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/property.py diff --git a/estate/models/res_users.py b/estate/models/res_users.py index d8eb99916e2..44ec6bea452 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -1,7 +1,8 @@ from odoo import fields, models + class ResUsers(models.Model): - pass + _name = 'res.users' _inherit = ["res.users"] property_ids = fields.One2many('estate_property', 'salesperson', domain=[('state', 'in', ('new', 'offer received'))]) 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..0aa00cd7505 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,9 @@ +{ + "name": "Real estate accounting", + 'author': 'anden', + 'license': 'LGPL-3', + 'depends': ['account', 'estate'], + # 'category': 'Real Estate/Brokerage', + 'data': [ + ] +} \ No newline at end of file diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..8120b005bb6 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import property diff --git a/estate_account/models/property.py b/estate_account/models/property.py new file mode 100644 index 00000000000..509760eb7ad --- /dev/null +++ b/estate_account/models/property.py @@ -0,0 +1,31 @@ +from odoo import fields, models, exceptions, Command + + +class EstateProperty(models.Model): + _name = 'estate_property' + _inherit = ['estate_property'] + + def action_property_sold(self): + + if self.state != 'offer accepted': + raise exceptions.UserError('An offer should be accepted') + + + moves = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.buyer.id, + 'invoice_line_ids': [ + Command.create({ + 'name': 'Proportionnal fees', + 'quantity': 0.06, + 'price_unit': self.selling_price, + }), + Command.create({ + 'name': 'Administrative fees', + 'quantity': 1.0, + 'price_unit': 100.0, + }) + ], + }) + + return super().action_property_sold() From a74c840721ed89575eaf17f5e407b1c4790c66fb Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Mon, 26 Jan 2026 14:17:08 +0100 Subject: [PATCH 35/37] [CLN] estate: clean a white space This cleans a lost white space. --- 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 e1a5f720f64..a91dd8ba190 100644 --- a/estate/models/property_offer.py +++ b/estate/models/property_offer.py @@ -56,7 +56,7 @@ def action_status_refused(self): @api.model def create(self, vals_list): - + for vals in vals_list: max_existing_price = max((offer.price for offer in self.env['estate_property'].browse(vals_list[0]['property_id']).property_offer_id), default=0) if vals['price'] < max_existing_price: From 55edb0e0abcfc496d31b9678e346720d88f8ff20 Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Mon, 26 Jan 2026 15:12:29 +0100 Subject: [PATCH 36/37] [IMP] estate: add Kanban view to property view (Chapter 14) This adds Kanban view to the property views. The properties are grouped by type by default and cannot be drag and drop. --- estate/models/res_users.py | 2 +- estate/views/estate_property_views.xml | 27 +++++++++++++++++++++++++- estate_account/__manifest__.py | 2 +- estate_account/models/property.py | 7 +++---- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/estate/models/res_users.py b/estate/models/res_users.py index 44ec6bea452..bf36b7e9eb4 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -2,7 +2,7 @@ class ResUsers(models.Model): - _name = 'res.users' + _name = 'res.users' _inherit = ["res.users"] property_ids = fields.One2many('estate_property', 'salesperson', domain=[('state', 'in', ('new', 'offer received'))]) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 249d841367b..90b797c73ee 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,7 +4,7 @@ Property action estate_property - list,form + list,kanban,form {'search_default_available': True} @@ -26,6 +26,31 @@ + + estate_property_kaban + estate_property + + + + + + +
+ Expected Price: +
+
+ Best offer: +
+
+ Selling price: +
+ +
+
+
+
+
+ estate_property_form estate_property diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index 0aa00cd7505..b6c9b519c92 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -6,4 +6,4 @@ # 'category': 'Real Estate/Brokerage', 'data': [ ] -} \ No newline at end of file +} diff --git a/estate_account/models/property.py b/estate_account/models/property.py index 509760eb7ad..7023e4b9c14 100644 --- a/estate_account/models/property.py +++ b/estate_account/models/property.py @@ -1,17 +1,16 @@ -from odoo import fields, models, exceptions, Command +from odoo import models, exceptions, Command class EstateProperty(models.Model): - _name = 'estate_property' + _name = 'estate_property' _inherit = ['estate_property'] def action_property_sold(self): if self.state != 'offer accepted': raise exceptions.UserError('An offer should be accepted') - - moves = self.env['account.move'].create({ + self.env['account.move'].create({ 'move_type': 'out_invoice', 'partner_id': self.buyer.id, 'invoice_line_ids': [ From 5c46462f86c7db02e948410c0f6a7b376836b495 Mon Sep 17 00:00:00 2001 From: anden-odoo Date: Mon, 26 Jan 2026 17:36:04 +0100 Subject: [PATCH 37/37] [IMP] awesome_owl: add Counter in a sub component (Chapter 1) This adds a Counter class as a sub component to call multiple counters in the playground. --- awesome_owl/static/src/counter/counter.js | 14 ++++++++++++++ awesome_owl/static/src/counter/counter.xml | 12 ++++++++++++ awesome_owl/static/src/playground.js | 6 +++++- awesome_owl/static/src/playground.xml | 5 ++--- 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..3ff5f43d5fd --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,14 @@ +import { Component, useState } from "@odoo/owl"; + + +export class Counter extends Component { + static template = "awesome_owl.counter"; + + setup() { + this.state = useState({ value: 0 }); + } + + increment() { + this.state.value++; + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..6c7b5ceeb2c --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,12 @@ + + + + +
+ hello world +

Counter:

+ +
+
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..140289b4cb1 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,9 @@ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; + export class Playground extends Component { static template = "awesome_owl.playground"; + + static components = {Counter}; } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..d145b91b7c2 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,9 +2,8 @@ -
- hello world -
+ +