From a54dcce093e615c632721a7612b2739e2e9e4fa5 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 19 Jan 2026 15:40:51 +0100 Subject: [PATCH 01/86] [ADD] Define an empty "Estate" module This module will be the basis of the Estate App --- estate/__init__.py | 0 estate/__manifest__.py | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..e523bf97625 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,10 @@ +{ + 'name': 'Estate', + 'category': 'Sales/CRM', + 'description': 'Advertise your real estate', + 'author': '[THDES] Thomas des Touches', + 'depends': [ + 'base_setup', + ], + 'application': True, +} \ No newline at end of file From 0cb3bd26ad10f94b788c21a6ebad1644d5f2c625 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 19 Jan 2026 16:23:24 +0100 Subject: [PATCH 02/86] [CLN] Add missing license --- estate/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index e523bf97625..646f2ffdf59 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,4 +7,5 @@ 'base_setup', ], 'application': True, + 'license': 'LGPL-3', } \ No newline at end of file From 2869fe7e8bc50ae07371e311e5af23c805a6dec3 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 19 Jan 2026 16:45:21 +0100 Subject: [PATCH 03/86] [IMP] Define estate property model --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py index e69de29bb2d..9a7e03eded3 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..72b0ca03f5b --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,4 @@ +from odoo import models + +class EstateProperty(models.Model): + _name = "estate_property" \ No newline at end of file From a172ae5c436a0cb90a453f4411fab51d539ef607 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 19 Jan 2026 16:45:57 +0100 Subject: [PATCH 04/86] [IMP] Add fields to EstateProperty --- estate/models/estate_property.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 72b0ca03f5b..0bd13aaef98 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,19 @@ -from odoo import models +from odoo import fields, models class EstateProperty(models.Model): - _name = "estate_property" \ No newline at end of file + _name = 'estate_property' + _description = 'Estate Property' + + name = fields.Char('Name', required = True) + description = fields.Text('Description') + postcode = fields.Char('Post Code') + date_availability = fields.Date('Availability Date') + expected_price = fields.Float('Expected Price', required = True) + selling_price = fields.Float('Selling Price') + bedrooms = fields.Integer('# Bedrooms') + living_area = fields.Integer('Living Area') + facades = fields.Integer('# Facades') + garage = fields.Boolean('Garage') + garden = fields.Boolean('Garden') + garden_area = fields.Integer('Garden area') + garden_orientation = fields.Selection('Garden Orientation') \ No newline at end of file From d1024dc89fdbdceff51e3254130f0ad84f41ac1a Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 09:11:04 +0100 Subject: [PATCH 05/86] [IMP] Add Orientation selection to EstateProperty model --- estate/models/estate_property.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0bd13aaef98..5eac3609537 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -16,4 +16,9 @@ class EstateProperty(models.Model): garage = fields.Boolean('Garage') garden = fields.Boolean('Garden') garden_area = fields.Integer('Garden area') - garden_orientation = fields.Selection('Garden Orientation') \ No newline at end of file + garden_orientation = fields.Selection( + string = 'Garden Orientation', + selection = [ + ('north', 'North'), ('south', 'South'), + ('east', 'East'), ('west', 'West') + ]) From abd84f91e834b3a0772ef95240b710dcb5bd475e Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 09:55:11 +0100 Subject: [PATCH 06/86] [CLN] Solve Spacing issues --- estate/__manifest__.py | 6 +++--- estate/models/estate_property.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 646f2ffdf59..0816d5de938 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,11 +1,11 @@ { 'name': 'Estate', - 'category': 'Sales/CRM', + 'category': 'Sales', 'description': 'Advertise your real estate', 'author': '[THDES] Thomas des Touches', 'depends': [ - 'base_setup', + 'base_setup', ], - 'application': True, + 'application': True, 'license': 'LGPL-3', } \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 5eac3609537..0bb26128fbc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -4,11 +4,11 @@ class EstateProperty(models.Model): _name = 'estate_property' _description = 'Estate Property' - name = fields.Char('Name', required = True) + name = fields.Char('Name', required=True) description = fields.Text('Description') postcode = fields.Char('Post Code') date_availability = fields.Date('Availability Date') - expected_price = fields.Float('Expected Price', required = True) + expected_price = fields.Float('Expected Price', required=True) selling_price = fields.Float('Selling Price') bedrooms = fields.Integer('# Bedrooms') living_area = fields.Integer('Living Area') @@ -17,8 +17,8 @@ class EstateProperty(models.Model): garden = fields.Boolean('Garden') garden_area = fields.Integer('Garden area') garden_orientation = fields.Selection( - string = 'Garden Orientation', - selection = [ + string='Garden Orientation', + selection=[ ('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West') ]) From 8f66f31956886ab4ef24b1c6ac0f634067864375 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 09:56:07 +0100 Subject: [PATCH 07/86] [IMP] Add security rules for EstateProperty model --- estate/__manifest__.py | 5 ++++- estate/security/ir.model.access.csv | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 0816d5de938..ae7aa5dd4f3 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,4 +8,7 @@ ], 'application': True, 'license': 'LGPL-3', -} \ No newline at end of file + 'data': [ + '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..98f4671fb0d --- /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 +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 From 5de39514ba34f01e4a38278fad7e305d421f70a0 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 10:11:39 +0100 Subject: [PATCH 08/86] [CLN] Fix styling issues --- estate/__init__.py | 2 +- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0bb26128fbc..d6068ead7f7 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,9 +1,10 @@ from odoo import fields, models + class EstateProperty(models.Model): _name = 'estate_property' _description = 'Estate Property' - + name = fields.Char('Name', required=True) description = fields.Text('Description') postcode = fields.Char('Post Code') From 17c82812fb69fff9c7b9ae3d9e324ae9c919c7b1 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 11:46:50 +0100 Subject: [PATCH 09/86] [IMP] Display a 3-level menu for the Real Estate app --- estate/__manifest__.py | 4 +++- estate/views/estate_menu_views.xml | 18 ++++++++++++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 estate/views/estate_menu_views.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index ae7aa5dd4f3..5ff59faf8dc 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,5 +1,5 @@ { - 'name': 'Estate', + 'name': 'Real Estate', 'category': 'Sales', 'description': 'Advertise your real estate', 'author': '[THDES] Thomas des Touches', @@ -10,5 +10,7 @@ 'license': 'LGPL-3', 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menu_views.xml', ], } diff --git a/estate/views/estate_menu_views.xml b/estate/views/estate_menu_views.xml new file mode 100644 index 00000000000..0bb46606199 --- /dev/null +++ b/estate/views/estate_menu_views.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..905bbd45918 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate_property + list,form + + From 432f84909e7334f58512dd403ab22792141a765d Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 11:59:12 +0100 Subject: [PATCH 10/86] [IMP] Set readonly and non copied field in EstateProperty - selling_price will be set after the sale, and should not be duplicated when created a new, not sold, property. - date_availability should not be copied, because a dynamic default value will be added --- estate/models/estate_property.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d6068ead7f7..96e06eb158a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,9 +8,9 @@ class EstateProperty(models.Model): name = fields.Char('Name', required=True) description = fields.Text('Description') postcode = fields.Char('Post Code') - date_availability = fields.Date('Availability Date') + date_availability = fields.Date('Availability Date', copy=False) expected_price = fields.Float('Expected Price', required=True) - selling_price = fields.Float('Selling Price') + selling_price = fields.Float('Selling Price', readonly=True, copy=False) bedrooms = fields.Integer('# Bedrooms') living_area = fields.Integer('Living Area') facades = fields.Integer('# Facades') From e9ee754c2b0fb945ed72c7757ef7252285047d2b Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 12:12:38 +0100 Subject: [PATCH 11/86] [IMP] Add default values for EstateProperty fields --- estate/models/estate_property.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 96e06eb158a..69df607ed64 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,10 +8,11 @@ class EstateProperty(models.Model): name = fields.Char('Name', required=True) description = fields.Text('Description') postcode = fields.Char('Post Code') - date_availability = fields.Date('Availability Date', copy=False) + date_availability = fields.Date('Availability Date', copy=False, + default=fields.Date.add(fields.Date.today(), months=3)) expected_price = fields.Float('Expected Price', required=True) selling_price = fields.Float('Selling Price', readonly=True, copy=False) - bedrooms = fields.Integer('# Bedrooms') + bedrooms = fields.Integer('# Bedrooms', default=2) living_area = fields.Integer('Living Area') facades = fields.Integer('# Facades') garage = fields.Boolean('Garage') From 7925fa1682f6e4a83fde0d54d0dc9124767f1ab4 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 13:31:29 +0100 Subject: [PATCH 12/86] [IMP] Add active and state fields to EstateProperty --- estate/models/estate_property.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 69df607ed64..a0729617792 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,14 +8,17 @@ class EstateProperty(models.Model): name = fields.Char('Name', required=True) description = fields.Text('Description') postcode = fields.Char('Post Code') + date_availability = fields.Date('Availability Date', copy=False, default=fields.Date.add(fields.Date.today(), months=3)) expected_price = fields.Float('Expected Price', required=True) selling_price = fields.Float('Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer('# Bedrooms', default=2) living_area = fields.Integer('Living Area') facades = fields.Integer('# Facades') garage = fields.Boolean('Garage') + garden = fields.Boolean('Garden') garden_area = fields.Integer('Garden area') garden_orientation = fields.Selection( @@ -24,3 +27,13 @@ class EstateProperty(models.Model): ('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'), ], + default='new', + required=True + ) From 7ed9bc63a2fd9202bf912c0f09d6f3be51868629 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 13:39:53 +0100 Subject: [PATCH 13/86] [CLN] Fix style --- estate/models/estate_property.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a0729617792..96d9765b098 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -31,9 +31,9 @@ class EstateProperty(models.Model): active = fields.Boolean(default=True) state = fields.Selection( selection=[ - ('new','New'), ('offer_received','Offer Received'), - ('offer_accepted','Offer Accepted'), - ('sold','Sold'), ('cancelled','Cancelled'), ], + ('new', 'New'), ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), ('cancelled', 'Cancelled') ], default='new', required=True ) From 209216cbaf7680515b02e61ce63bb2342ba65e1e Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 14:00:36 +0100 Subject: [PATCH 14/86] [IMP] Configure list view of EstateProperty --- estate/views/estate_property_views.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 905bbd45918..1d9f80ac62a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,19 @@ estate_property list,form + + + estate_property.list + estate_property + + + + + + + + + + + From f500b45f142b6cc28a9c2cc4b04db0489cef5515 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 20 Jan 2026 15:31:54 +0100 Subject: [PATCH 15/86] [IMP] Configure form view of EstateProperty --- estate/views/estate_property_views.xml | 47 +++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1d9f80ac62a..7441818f06a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -10,7 +10,7 @@ estate_property.list estate_property - + @@ -20,4 +20,49 @@ + + + estate_property.form + estate_property + +
+ + +
+

+ +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ From e2fda31600652b28f5c429945a0f8a888ef2ffc8 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 09:20:15 +0100 Subject: [PATCH 16/86] [CLN] Adopt naming conventions --- estate/models/estate_property.py | 2 +- estate/views/estate_property_views.xml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 96d9765b098..3e9da6ba6f9 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -2,7 +2,7 @@ class EstateProperty(models.Model): - _name = 'estate_property' + _name = 'estate.property' _description = 'Estate Property' name = fields.Char('Name', required=True) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 7441818f06a..be959baa408 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -2,13 +2,13 @@ Properties - estate_property - list,form + estate.property + list,form,search - estate_property.list - estate_property + estate.property.list + estate.property @@ -22,8 +22,8 @@ - estate_property.form - estate_property + estate.property.form + estate.property
From 2b7febc0923b1825b11689e7f76c84145d397ca2 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 09:21:12 +0100 Subject: [PATCH 17/86] [IMP] Add search feature to Properties view --- 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 be959baa408..0410f04a3fb 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -65,4 +65,22 @@ + + estate.property.search + estate.property + + + + + + + + + + + + + + + From 015c7c7f1aad53fd502f6033d018e426b771dc88 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 10:13:00 +0100 Subject: [PATCH 18/86] [IMP] Create Property Type model, menus, views, access --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property_type.py | 8 ++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_menu_views.xml | 16 +++++++ estate/views/estate_property_type_views.xml | 47 +++++++++++++++++++++ 6 files changed, 74 insertions(+) create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 5ff59faf8dc..df30c7db329 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,6 +11,7 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', 'views/estate_menu_views.xml', ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..40092a2d810 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,2 @@ from . import estate_property +from . import estate_property_type diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..5b3f6c54826 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'Estate Property Type' + + name = fields.Char('Name', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 98f4671fb0d..7d4c1fcdb86 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 estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menu_views.xml b/estate/views/estate_menu_views.xml index 0bb46606199..39c39639944 100644 --- a/estate/views/estate_menu_views.xml +++ b/estate/views/estate_menu_views.xml @@ -1,9 +1,11 @@ + + + + + + + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..6b1dd1b653f --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,47 @@ + + + + Property Types + estate.property.type + list,form,search + + + + estate.property.type.list + estate.property.type + + + + + + + + + estate.property.type.form + estate.property.type + + + + +
+

+ +

+
+
+
+ +
+
+ + + estate.property.type.search + estate.property.type + + + + + + + +
From f64e80a8a3d55694d853c8d1216727252f82396f Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 10:30:46 +0100 Subject: [PATCH 19/86] [IMP] Link Property type to Property --- estate/models/estate_property.py | 1 + estate/views/estate_property_views.xml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3e9da6ba6f9..c2b426ec8f0 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,6 +8,7 @@ class EstateProperty(models.Model): name = fields.Char('Name', required=True) description = fields.Text('Description') postcode = fields.Char('Post Code') + property_type_id = fields.Many2one("estate.property.type", string="Property Type") date_availability = fields.Date('Availability Date', copy=False, default=fields.Date.add(fields.Date.today(), months=3)) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 0410f04a3fb..d1aaa6ba1d0 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -13,6 +13,7 @@ + @@ -37,6 +38,7 @@ + From 81048be06ec4a85a191bb44d4cb617d27edbec6b Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 11:07:18 +0100 Subject: [PATCH 20/86] [CLN] Fix style once again --- estate/models/estate_property.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index c2b426ec8f0..031ba4e54a6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,7 +8,7 @@ class EstateProperty(models.Model): name = fields.Char('Name', required=True) description = fields.Text('Description') postcode = fields.Char('Post Code') - property_type_id = fields.Many2one("estate.property.type", string="Property Type") + property_type_id = fields.Many2one('estate.property.type', string='Property Type') date_availability = fields.Date('Availability Date', copy=False, default=fields.Date.add(fields.Date.today(), months=3)) @@ -34,7 +34,7 @@ class EstateProperty(models.Model): selection=[ ('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), ('cancelled', 'Cancelled') ], + ('sold', 'Sold'), ('cancelled', 'Cancelled')], default='new', required=True ) From 3ebe4714ada913b9b3799181e2acd593fcf30dee Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 11:07:50 +0100 Subject: [PATCH 21/86] [IMP] Add Salesperson and Buyer in EstateProperty --- estate/models/estate_property.py | 3 +++ estate/views/estate_property_views.xml | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 031ba4e54a6..9de7c892a1a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -29,6 +29,9 @@ class EstateProperty(models.Model): ('east', 'East'), ('west', 'West') ]) + user_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.uid) + partner_id = fields.Many2one('res.partner', string='Buyer', copy=False) + active = fields.Boolean(default=True) state = fields.Selection( selection=[ diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d1aaa6ba1d0..be375cd868c 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -61,6 +61,12 @@ + + + + + +
From 6eb5ff8470ac6ea56c744e2e24eff844600d3ccf Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 11:58:45 +0100 Subject: [PATCH 22/86] [IMP] Define PropertyTag --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property_tag.py | 8 ++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_menu_views.xml | 8 +++- estate/views/estate_property_tag_views.xml | 47 ++++++++++++++++++++++ 6 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/views/estate_property_tag_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index df30c7db329..74668511fd0 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,6 +12,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_menu_views.xml', ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 40092a2d810..c620ac481a3 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,3 @@ from . import estate_property from . import estate_property_type +from . import estate_property_tag diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..7f2f5dbb24a --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'Estate Property Tag' + + name = fields.Char('Name', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 7d4c1fcdb86..5558d0fbc60 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 estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menu_views.xml b/estate/views/estate_menu_views.xml index 39c39639944..a7469baa45a 100644 --- a/estate/views/estate_menu_views.xml +++ b/estate/views/estate_menu_views.xml @@ -26,9 +26,15 @@ sequence="2"/> + +
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..a6894bcd38b --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,47 @@ + + + + Property Tags + estate.property.tag + list,form,search + + + + estate.property.tag.list + estate.property.tag + + + + + + + + + estate.property.tag.form + estate.property.tag + +
+ + +
+

+ +

+
+
+
+
+
+
+ + + estate.property.tag.search + estate.property.tag + + + + + + + +
From 544cced3fcac23748600af11087bd2ce52aac207 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 12:03:19 +0100 Subject: [PATCH 23/86] [CLN] Move parameters on their own line when too long --- estate/models/estate_property.py | 23 ++++++++++++++------- estate/views/estate_property_type_views.xml | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9de7c892a1a..90c65751f29 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -10,8 +10,11 @@ class EstateProperty(models.Model): postcode = fields.Char('Post Code') property_type_id = fields.Many2one('estate.property.type', string='Property Type') - date_availability = fields.Date('Availability Date', copy=False, - default=fields.Date.add(fields.Date.today(), months=3)) + date_availability = fields.Date( + 'Availability Date', + copy=False, + default=fields.Date.add(fields.Date.today(), months=3) + ) expected_price = fields.Float('Expected Price', required=True) selling_price = fields.Float('Selling Price', readonly=True, copy=False) @@ -25,9 +28,12 @@ class EstateProperty(models.Model): garden_orientation = fields.Selection( string='Garden Orientation', selection=[ - ('north', 'North'), ('south', 'South'), - ('east', 'East'), ('west', 'West') - ]) + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ] + ) user_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.uid) partner_id = fields.Many2one('res.partner', string='Buyer', copy=False) @@ -35,9 +41,12 @@ class EstateProperty(models.Model): active = fields.Boolean(default=True) state = fields.Selection( selection=[ - ('new', 'New'), ('offer_received', 'Offer Received'), + ('new', 'New'), + ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), ('cancelled', 'Cancelled')], + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], default='new', required=True ) diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 6b1dd1b653f..ac1fdbfcf1a 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -10,7 +10,7 @@ estate.property.type.list estate.property.type - + From 4323150dbe80f876375b49eafa1c04f00ab69f73 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 13:57:28 +0100 Subject: [PATCH 24/86] [IMP] Link tags to property offers --- estate/models/estate_property.py | 1 + estate/views/estate_property_views.xml | 13 ++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 90c65751f29..98eb610f94b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -9,6 +9,7 @@ class EstateProperty(models.Model): description = fields.Text('Description') postcode = fields.Char('Post Code') property_type_id = fields.Many2one('estate.property.type', string='Property Type') + property_tag_ids = fields.Many2many('estate.property.tag', string='Tags') date_availability = fields.Date( 'Availability Date', diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index be375cd868c..9d6124cda39 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -28,13 +28,12 @@
- -
-

- -

-
-
+
+

+ +

+ +
From 31e97bdb488164bcaa7e4b721904a7defbd68768 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 14:14:35 +0100 Subject: [PATCH 25/86] [IMP] Create offers and links them to properties --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 2 + estate/models/estate_property_offer.py | 16 ++++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_property_offer_views.xml | 39 ++++++++++++++++++++ estate/views/estate_property_views.xml | 31 +++++++++------- 7 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/views/estate_property_offer_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 74668511fd0..632e085d803 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -13,6 +13,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_menu_views.xml', ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index c620ac481a3..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,3 +1,4 @@ from . import estate_property from . import estate_property_type from . import estate_property_tag +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 98eb610f94b..db12b0b7b5b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -39,6 +39,8 @@ class EstateProperty(models.Model): user_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.uid) partner_id = fields.Many2one('res.partner', string='Buyer', copy=False) + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + active = fields.Boolean(default=True) state = fields.Selection( selection=[ diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..314031d9441 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer' + + price = fields.Float('Price') + status = fields.Selection( + string='Status', + copy=False, + selection=[('accepted', 'Accepted'), ('refused', 'Refused')] + ) + + property_id = fields.Many2one('estate.property', string='Property', required=True) + partner_id = fields.Many2one('res.partner', string='Partner', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 5558d0fbc60..0c0b62b7fee 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..e1de503da53 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,39 @@ + + + + Property Offers + estate.property.offer + list,form + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + estate.property.offer.form + estate.property.offer + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9d6124cda39..df5b4a11182 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -49,22 +49,25 @@ - - - - - - - - - - + + + + + + + + + + + + + - - - - + + + +
From bd02b3cb77ad2a474542db592f1afe3cfe32eb4d Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 14:37:14 +0100 Subject: [PATCH 26/86] [IMP] Compute total area from living + garden area --- estate/models/estate_property.py | 9 ++++++++- estate/views/estate_property_views.xml | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index db12b0b7b5b..e2be052e2cf 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -36,6 +36,8 @@ class EstateProperty(models.Model): ] ) + total_area = fields.Float('Total Area', compute='_compute_total_area') + user_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.uid) partner_id = fields.Many2one('res.partner', string='Buyer', copy=False) @@ -53,3 +55,8 @@ class EstateProperty(models.Model): default='new', required=True ) + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index df5b4a11182..8420bc66cb6 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -58,6 +58,7 @@ + From 8eb9b2bdc8775622ff9b9a09dde272e026a69b10 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 15:24:13 +0100 Subject: [PATCH 27/86] [IMP] Compute best offer --- estate/models/estate_property.py | 8 +++++++- estate/views/estate_property_views.xml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index e2be052e2cf..09eec06fb7a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -36,12 +36,13 @@ class EstateProperty(models.Model): ] ) - total_area = fields.Float('Total Area', compute='_compute_total_area') + total_area = fields.Float(string='Total Area', compute='_compute_total_area') user_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.uid) partner_id = fields.Many2one('res.partner', string='Buyer', copy=False) offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + best_offer = fields.Float(string='Best Offer', compute='_compute_best_offer') active = fields.Boolean(default=True) state = fields.Selection( @@ -60,3 +61,8 @@ class EstateProperty(models.Model): def _compute_total_area(self): for record in self: record.total_area = record.living_area + record.garden_area + + @api.depends('offer_ids.price') + def _compute_best_offer(self): + for record in self: + record.best_offer = max(record.offer_ids.mapped('price'), default=0) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 8420bc66cb6..58858d16656 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -44,6 +44,7 @@ + From 9432698104fce57285e9844cdb7c34d0507cb7c0 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 15:24:42 +0100 Subject: [PATCH 28/86] [IMP] Compute offer deadline from validity --- estate/models/estate_property_offer.py | 14 +++++++++++++- estate/views/estate_property_offer_views.xml | 4 ++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 314031d9441..241bdfb1ab7 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -12,5 +12,17 @@ class EstatePropertyOffer(models.Model): selection=[('accepted', 'Accepted'), ('refused', 'Refused')] ) + validity = fields.Integer(string='Validity (days)', default=7) + date_deadline = fields.Date( + string='Deadline', + compute='_compute_deadline' + ) + property_id = fields.Many2one('estate.property', string='Property', required=True) partner_id = fields.Many2one('res.partner', string='Partner', required=True) + + @api.depends('validity') + def _compute_deadline(self): + for record in self: + creation_date = record.create_date or fields.Date.today() + record.date_deadline = fields.Date.add(creation_date, days=record.validity) diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index e1de503da53..b45f407e65b 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -13,6 +13,8 @@ + + @@ -28,6 +30,8 @@ + + From 86c8830aba26e71daf551d7a8d633dc85ddb3f2f Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 16:12:10 +0100 Subject: [PATCH 29/86] [IMP] Compute offer validity from deadline --- estate/models/estate_property_offer.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 241bdfb1ab7..dd2430f136d 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -15,7 +15,8 @@ class EstatePropertyOffer(models.Model): validity = fields.Integer(string='Validity (days)', default=7) date_deadline = fields.Date( string='Deadline', - compute='_compute_deadline' + compute='_compute_deadline', + inverse="_compute_validity" ) property_id = fields.Many2one('estate.property', string='Property', required=True) @@ -26,3 +27,8 @@ def _compute_deadline(self): for record in self: creation_date = record.create_date or fields.Date.today() record.date_deadline = fields.Date.add(creation_date, days=record.validity) + + def _compute_validity(self): + for record in self: + creation_date = record.create_date or fields.Date.today() + record.validity = (record.date_deadline - creation_date.date()).days From edc57f04506a44bda6a882953678d8293ae51375 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 21 Jan 2026 16:56:21 +0100 Subject: [PATCH 30/86] [IMP] Update garden area and orientation garden is (un)ticked --- estate/models/estate_property.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 09eec06fb7a..0528420ee5a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -66,3 +66,12 @@ def _compute_total_area(self): def _compute_best_offer(self): for record in self: record.best_offer = max(record.offer_ids.mapped('price'), default=0) + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = None + self.garden_orientation = None From 2d1d4a60820473d7e901e84c3bc3a3e0df185013 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Thu, 22 Jan 2026 09:23:42 +0100 Subject: [PATCH 31/86] [IMP] Display offer status in form header TODO: Canceled state should be hidden when it is not the current state. --- estate/models/estate_property.py | 2 +- estate/views/estate_property_views.xml | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0528420ee5a..3746da5bf78 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -51,7 +51,7 @@ class EstateProperty(models.Model): ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), - ('cancelled', 'Cancelled') + ('canceled', 'Canceled') ], default='new', required=True diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 58858d16656..9bd8ea2b38b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -27,12 +27,17 @@ estate.property
+
+ + +
+

- +
@@ -62,9 +67,11 @@ + + From a795ef306c9fe5127c968edd63cd58c651261153 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Thu, 22 Jan 2026 09:25:02 +0100 Subject: [PATCH 32/86] [IMP] Add buttons to sell and cancel properties With basic behaviour --- estate/models/estate_property.py | 20 +++++++++++++++++++- estate/views/estate_property_views.xml | 2 ++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3746da5bf78..1a90e2ab447 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models +from odoo import api, exceptions, fields, models class EstateProperty(models.Model): @@ -75,3 +75,21 @@ def _onchange_garden(self): else: self.garden_area = None self.garden_orientation = None + + def action_sell(self): + for record in self: + if record.state == 'canceled': + raise exceptions.UserError('Canceled properties cannot be sold') + + record.state = 'sold' + + return True + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise exceptions.UserError('Sold properties cannot be canceled') + + record.state = 'canceled' + + return True diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9bd8ea2b38b..0e49f4c8573 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -28,6 +28,8 @@
+
From cabf6875bdd4127ef081f5c67f6cacc61eee0fa4 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Thu, 22 Jan 2026 09:45:05 +0100 Subject: [PATCH 33/86] [IMP] Accept and refuse offers from list view --- estate/models/estate_property_offer.py | 14 +++++++++++++- estate/views/estate_property_offer_views.xml | 6 ++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index dd2430f136d..77449ac6b6f 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -6,7 +6,7 @@ class EstatePropertyOffer(models.Model): _description = 'Estate Property Offer' price = fields.Float('Price') - status = fields.Selection( + state = fields.Selection( string='Status', copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')] @@ -32,3 +32,15 @@ def _compute_validity(self): for record in self: creation_date = record.create_date or fields.Date.today() record.validity = (record.date_deadline - creation_date.date()).days + + def action_accept(self): + for record in self: + record.state = 'accepted' + + return True + + def action_refuse(self): + for record in self: + record.state = 'refused' + + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index b45f407e65b..ffeceee9552 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -15,7 +15,9 @@ - + +

From 7014b4736faa281a1adf57e6e65a5f7beceb2073 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 09:08:07 +0100 Subject: [PATCH 55/86] [IMP] Prevent deletion of properties Only new and canceled properties can be deleted --- estate/models/estate_property.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 46ffbd10c8e..dbbbe599660 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -120,3 +120,9 @@ def action_cancel(self): record.state = 'canceled' return True + + @api.ondelete(at_uninstall=False) + def _ondelete(self): + for record in self: + if record.state not in ('new', 'canceled'): + raise UserError('Only new and canceled properties can be deleted') From 69f19b91d13f86743c3fa36f5f264645bd12bf28 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 10:00:59 +0100 Subject: [PATCH 56/86] [REF] Update property state when an offer is received Use create method of offer instead of onchange in property for performance and readability reasons --- estate/models/estate_property.py | 11 +++++------ estate/models/estate_property_offer.py | 6 ++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index dbbbe599660..2232e41ee89 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -97,12 +97,6 @@ def _onchange_garden(self): self.garden_area = None self.garden_orientation = None - @api.onchange('offer_ids') - def _onchange_offer_ids(self): - for record in self: - if record.state == 'new' and len(record.offer_ids) > 0: - record.state = 'offer_received' - def action_sell(self): for record in self: if record.state == 'canceled': @@ -126,3 +120,8 @@ def _ondelete(self): for record in self: if record.state not in ('new', 'canceled'): raise UserError('Only new and canceled properties can be deleted') + + def set_offer_received(self): + for record in self: + if record.state == 'new': + record.state = 'offer_received' diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 638b998e098..a42df13376a 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -67,3 +67,9 @@ def action_refuse(self): property.partner_id = None return True + + @api.model + def create(self, vals): + for record in vals: + self.env['estate.property'].browse(record['property_id']).set_offer_received() + return super().create(vals) From 568965f329a8a6d20964f440e39bf8ac83a3e2e9 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 10:03:13 +0100 Subject: [PATCH 57/86] [CLN] Fix typos and style --- estate/models/estate_property.py | 2 +- estate/models/estate_property_offer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2232e41ee89..5e8410a61e3 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -76,7 +76,7 @@ def _check_prices(self): for record in self: if record.state == 'offer_accepted' \ and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) == -1: - raise ValidationError('The selling price must be at least 90% of the selling price') + raise ValidationError('The selling price must be at least 90% of the expected price') @api.depends('living_area', 'garden_area') def _compute_total_area(self): diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index a42df13376a..3bb36e04bc5 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -60,7 +60,7 @@ def action_refuse(self): property = self.property_id - if not 'accepted' in property.offer_ids.mapped('state'): + if 'accepted' not in property.offer_ids.mapped('state'): property.state = 'offer_received' property.selling_price = None From 9351f9aed81883e7e89357cd8ee4c6d43cbc6b78 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 10:29:27 +0100 Subject: [PATCH 58/86] [IMP] Prevent creation of offers below the best offer --- estate/models/estate_property_offer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 3bb36e04bc5..acb913c0e8e 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): @@ -71,5 +72,11 @@ def action_refuse(self): @api.model def create(self, vals): for record in vals: - self.env['estate.property'].browse(record['property_id']).set_offer_received() + property = self.env['estate.property'].browse(record['property_id']) + + if record['price'] < property.best_offer: + raise UserError(f'The offer must be above {property.best_offer}') + + property.set_offer_received() + return super().create(vals) From b5d6e741b94da529535f16f518bc0c7b09d2f6ef Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 11:29:36 +0100 Subject: [PATCH 59/86] [IMP] Display user's properties in form view --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/users.py | 8 ++++++++ estate/views/res_user_views.xml | 23 +++++++++++++++++++++++ 4 files changed, 33 insertions(+) create mode 100644 estate/models/users.py create mode 100644 estate/views/res_user_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 632e085d803..8cd68e5692d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -15,5 +15,6 @@ 'views/estate_property_tag_views.xml', 'views/estate_property_offer_views.xml', 'views/estate_menu_views.xml', + 'views/res_user_views.xml', ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..8ada7efccc7 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import users diff --git a/estate/models/users.py b/estate/models/users.py new file mode 100644 index 00000000000..a1a270abe3e --- /dev/null +++ b/estate/models/users.py @@ -0,0 +1,8 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class Users(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many('estate.property', 'user_id', domains = "[('state', 'in', ('new', 'offer_received'))]") diff --git a/estate/views/res_user_views.xml b/estate/views/res_user_views.xml new file mode 100644 index 00000000000..73d7c81dd01 --- /dev/null +++ b/estate/views/res_user_views.xml @@ -0,0 +1,23 @@ + + + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + + + + From 0802f610368833ea60e22e0d08a3113f78e43ecd Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 11:46:40 +0100 Subject: [PATCH 60/86] [IMP] create empty link module between estate and account --- estate_account/__manifest__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 estate_account/__manifest__.py diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..1b0fdeaa11e --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Real Estate Accounting', + 'category': 'Sales/Accouting', + 'description': 'Invoice your real estate', + 'author': '[THDES] Thomas des Touches', + 'depends': [ + 'estate', + 'account' + ], + 'application': True, + 'license': 'LGPL-3', + 'data': [ + ], +} From 3ec48dbe7b0d71607a741929e3a171495b4245d2 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 13:33:26 +0100 Subject: [PATCH 61/86] [FIX] Correct res_user names --- estate/models/__init__.py | 2 +- estate/models/{users.py => res_users.py} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename estate/models/{users.py => res_users.py} (78%) diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8ada7efccc7..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,4 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer -from . import users +from . import res_users diff --git a/estate/models/users.py b/estate/models/res_users.py similarity index 78% rename from estate/models/users.py rename to estate/models/res_users.py index a1a270abe3e..a445650f852 100644 --- a/estate/models/users.py +++ b/estate/models/res_users.py @@ -2,7 +2,7 @@ from odoo.exceptions import UserError, ValidationError -class Users(models.Model): - _inherit = 'res.users' +class ResUsers(models.Model): + _inherit = ['res.users'] property_ids = fields.One2many('estate.property', 'user_id', domains = "[('state', 'in', ('new', 'offer_received'))]") From 451c7894a7b4768b05b5ad5e6f22862bf65991e6 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 13:34:42 +0100 Subject: [PATCH 62/86] [IMP] inherit estate_property in estate_account --- estate_account/__init__.py | 1 + estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 8 ++++++++ 3 files changed, 10 insertions(+) create mode 100644 estate_account/__init__.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/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..1c5b13d07ca --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,8 @@ +from odoo import models + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + def action_sell(self): + print('----- INHERITED -----') + return super().action_sell() From 53cb6c42cc05a79db38fa7c5a172c66b3b50dc04 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 15:00:34 +0100 Subject: [PATCH 63/86] [IMP] Create an invoice when a property is sold --- estate_account/models/estate_property.py | 29 +++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 1c5b13d07ca..9a7817faf49 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -1,8 +1,35 @@ from odoo import models +from odoo.exceptions import UserError +from odoo.orm.commands import Command class EstateProperty(models.Model): _inherit = 'estate.property' def action_sell(self): - print('----- INHERITED -----') + + journal = self.env['account.journal'].search([('type', 'in', 'sale')], limit=1) + invoice_vals = [] + + for record in self: + invoice_vals.append({ + 'name': record.name, + 'partner_id': record.partner_id.id, + 'move_type': 'out_invoice', + 'journal_id': journal.id, + 'invoice_line_ids': [ + Command.create({ + 'name': record.name, + 'quantity': 1, + 'price_unit': record.selling_price * 0.06, + }), + Command.create({ + 'name': 'Administrative fees', + 'quantity': 1, + 'price_unit': 100.00, + }), + ] + }) + + self.env['account.move'].create(invoice_vals) + return super().action_sell() From 05f00f901fa3b800e121e6f5a874dd1a55473598 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 16:34:31 +0100 Subject: [PATCH 64/86] [IMP] Add basic kanban view on properties --- estate/views/estate_property_views.xml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index a2ab8d97e83..e03f91ece3e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ Properties estate.property - list,form,search + list,form,search,kanban {'search_default_available': True} @@ -104,4 +104,20 @@ + + estate.property.kanban + estate.property + + + + +
+ +
+
+
+
+
+
+ From 4547d554c195493565924dd29103253837d303ad Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Mon, 26 Jan 2026 16:52:01 +0100 Subject: [PATCH 65/86] [IMP] estate: Make Kanban view of properties useable --- estate/views/estate_property_views.xml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e03f91ece3e..99a904a1342 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -108,11 +108,24 @@ estate.property.kanban estate.property - + +
+
+ Expected Price: +
+
+ Best Price: +
+
+ Selling Price: +
+
+ +
From 1eb520fe6e2aa26f4c7bd43c8414ce9aee6c890d Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Tue, 27 Jan 2026 13:17:20 +0100 Subject: [PATCH 66/86] [CLN] Follow coding guideline in xml --- estate/views/estate_menu_views.xml | 28 ++++++++++---------- estate/views/estate_property_offer_views.xml | 10 +++---- estate/views/estate_property_tag_views.xml | 14 +++++----- estate/views/estate_property_type_views.xml | 16 +++++------ estate/views/estate_property_views.xml | 18 ++++++------- estate/views/res_user_views.xml | 5 ---- 6 files changed, 43 insertions(+), 48 deletions(-) diff --git a/estate/views/estate_menu_views.xml b/estate/views/estate_menu_views.xml index a7469baa45a..684aded1faf 100644 --- a/estate/views/estate_menu_views.xml +++ b/estate/views/estate_menu_views.xml @@ -2,39 +2,39 @@ diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index c8cb7400057..42273a0a99c 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,14 +1,14 @@ - + Property Offers estate.property.offer list,form [('property_type_id', '=', active_id)] - - estate.property.offer.list + + estate.property.offer.view.list estate.property.offer @@ -23,8 +23,8 @@ - - estate.property.offer.form + + estate.property.offer.view.form estate.property.offer diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index 3cfc89ee436..15ee0478cfc 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -1,13 +1,13 @@ - + Property Tags estate.property.tag list,form,search - - estate.property.tag.list + + estate.property.tag.view.list estate.property.tag @@ -17,8 +17,8 @@ - - estate.property.tag.form + + estate.property.tag.view.form estate.property.tag @@ -36,8 +36,8 @@ - - estate.property.tag.search + + estate.property.tag.view.search estate.property.tag diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 535ae3acbdf..f85bda9b6e1 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -1,13 +1,13 @@ - + Property Types estate.property.type list,form,search - - estate.property.type.list + + estate.property.type.view.list estate.property.type @@ -17,14 +17,14 @@ - - estate.property.type.form + + estate.property.type.view.form estate.property.type
- + + + diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..cacfc5058e6 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,13 @@ -import { Component } from "@odoo/owl"; +import { Component, markup } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card }; + static props = [] + + setup() { + this.html = markup("some content") + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..c6ebc6e32b4 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,18 @@ - -
- hello world -
+

+

hello world!
+
+ + +
+

+ +

+ + +

From f0571da32ffc8b0a7126dd8ef1917f5522e7f600 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 10:06:00 +0100 Subject: [PATCH 69/86] [IMP] Add a bit of styling --- awesome_owl/static/src/counter/counter.css | 6 ++++++ awesome_owl/static/src/counter/counter.js | 5 +++-- awesome_owl/static/src/counter/counter.xml | 8 ++++++-- awesome_owl/static/src/playground.css | 12 ++++++++++++ awesome_owl/static/src/playground.js | 3 ++- awesome_owl/static/src/playground.xml | 14 ++++++++------ 6 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 awesome_owl/static/src/counter/counter.css create mode 100644 awesome_owl/static/src/playground.css diff --git a/awesome_owl/static/src/counter/counter.css b/awesome_owl/static/src/counter/counter.css new file mode 100644 index 00000000000..2a772c30d1d --- /dev/null +++ b/awesome_owl/static/src/counter/counter.css @@ -0,0 +1,6 @@ +.counter { + margin: 0 1rem; + border: 1px solid black; + padding: 0.5rem 1rem; + width: fit-content; +} diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index b76676f4a90..6a340975fd3 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -2,13 +2,14 @@ import { Component, useState } from "@odoo/owl"; export class Counter extends Component { static template = "awesome_owl.counter"; - static props = [] + static props = ["onChange?"] setup() { - this.state = useState({ value: 0}); + this.state = useState({ value: 1}); } increment() { this.state.value++; + this.props.onChange?.(); } } diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml index 647dc893932..bf8aa3d556d 100644 --- a/awesome_owl/static/src/counter/counter.xml +++ b/awesome_owl/static/src/counter/counter.xml @@ -1,8 +1,12 @@ - Counter: - +
+ Counter: + +
diff --git a/awesome_owl/static/src/playground.css b/awesome_owl/static/src/playground.css new file mode 100644 index 00000000000..02de697126e --- /dev/null +++ b/awesome_owl/static/src/playground.css @@ -0,0 +1,12 @@ +.counters { + margin: 1rem; + border: 1px solid black; + padding: 1rem 0; + width: fit-content; +} + +.sum { + margin-top: 1rem; + margin-left: 1rem; + width: fit-content; +} diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index cacfc5058e6..3bf41d2b39b 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -8,6 +8,7 @@ export class Playground extends Component { static props = [] setup() { - this.html = markup("some content") + this.html = markup("some content"); + this.sum = 2; } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index c6ebc6e32b4..20e3c51c6fe 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,13 +1,15 @@ -

-

hello world!
-
- - +
+
+ + +
+
+ Sum: +
-

From 1dcf4a5ad41dc35ef64b7a3d0140c5a699cde207 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 10:23:14 +0100 Subject: [PATCH 70/86] [IMP] Display the sum of counters --- awesome_owl/static/src/counter/counter.js | 2 +- awesome_owl/static/src/playground.js | 11 ++++++++--- awesome_owl/static/src/playground.xml | 16 ++++++++-------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index 6a340975fd3..f5e371c5a3d 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -5,7 +5,7 @@ export class Counter extends Component { static props = ["onChange?"] setup() { - this.state = useState({ value: 1}); + this.state = useState({ value: 0}); } increment() { diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 3bf41d2b39b..05bf45066b0 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,4 +1,4 @@ -import { Component, markup } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; import { Counter } from "./counter/counter"; import { Card } from "./card/card"; @@ -8,7 +8,12 @@ export class Playground extends Component { static props = [] setup() { - this.html = markup("some content"); - this.sum = 2; + this.html = markup("some content rendered from html"); + this.state = useState({ sum: 0}); + } + + incrementSum() { + console.log("incrementSum called") + this.state.sum++; } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 20e3c51c6fe..9ce067e5e82 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,15 +1,15 @@ -

-
- - -
-
- Sum: -
+
+
+ +
+
+ Sum: +
+

From 49f56ff23e22b40431996266c8a56f5f820c3572 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 13:07:50 +0100 Subject: [PATCH 71/86] [IMP] Add todo list in playground --- awesome_owl/static/src/hooks.js | 5 +++ awesome_owl/static/src/playground.js | 5 ++- awesome_owl/static/src/playground.xml | 4 +- awesome_owl/static/src/todo_list/todo_item.js | 20 ++++++++++ .../static/src/todo_list/todo_item.xml | 11 +++++ .../static/src/todo_list/todo_list.css | 0 awesome_owl/static/src/todo_list/todo_list.js | 40 +++++++++++++++++++ .../static/src/todo_list/todo_list.xml | 17 ++++++++ 8 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 awesome_owl/static/src/hooks.js create mode 100644 awesome_owl/static/src/todo_list/todo_item.js create mode 100644 awesome_owl/static/src/todo_list/todo_item.xml create mode 100644 awesome_owl/static/src/todo_list/todo_list.css create mode 100644 awesome_owl/static/src/todo_list/todo_list.js create mode 100644 awesome_owl/static/src/todo_list/todo_list.xml diff --git a/awesome_owl/static/src/hooks.js b/awesome_owl/static/src/hooks.js new file mode 100644 index 00000000000..c356ae980af --- /dev/null +++ b/awesome_owl/static/src/hooks.js @@ -0,0 +1,5 @@ +import { onMounted, useRef } from "@odoo/owl"; +export function useAutoFocus(ref_name) { + const ref = useRef(ref_name); + onMounted(() => ref.el.focus()) +} diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 05bf45066b0..89e8514d0a2 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,10 +1,11 @@ import { Component, markup, useState } from "@odoo/owl"; -import { Counter } from "./counter/counter"; import { Card } from "./card/card"; +import { Counter } from "./counter/counter"; +import { TodoList } from "./todo_list/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; - static components = { Counter, Card }; + static components = { Counter, Card, TodoList }; static props = [] setup() { diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 9ce067e5e82..155097a4cb8 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -7,7 +7,7 @@

- Sum: + Sum:
@@ -15,6 +15,8 @@

+ +
diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js new file mode 100644 index 00000000000..977cf959604 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,20 @@ +import { Component } from "@odoo/owl"; +import { Card } from "../card/card"; + +export class TodoItem extends Component { + static template = "awesome_owl.todo_item"; + static components = { Card }; + static props = { + todo: { + type: Object, + shape: { + id: Number, + description: String, + isCompleted: Boolean, + }, + }, + toggleCompleted: Function, + removeTodo: Function, + } +} + diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml new file mode 100644 index 00000000000..56e154a733e --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,11 @@ + + + +
+ + + +
+
+ +
diff --git a/awesome_owl/static/src/todo_list/todo_list.css b/awesome_owl/static/src/todo_list/todo_list.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..8c474e21e24 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,40 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutoFocus } from "../hooks"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static components = { TodoItem }; + static props = []; + + setup() { + this.nextId = 1; + this.todos = useState([]); + useAutoFocus('input'); + } + + addTodo(e) { + if (e.keyCode !== 13) return; + + const input = e.target.value + if (!input) return + + this.todos.push({ + id: this.nextId++, + description: input, + isCompleted: false, + }) + + e.target.value = ""; + } + + toggleCompleted(id) { + const todo = this.todos.find(todo => todo.id === id); + todo.isCompleted = !todo.isCompleted; + } + + removeTodo(id) { + const index = this.todos.findIndex(todo => todo.id === id); + this.todos.splice(index, 1); + } +} diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml new file mode 100644 index 00000000000..b0f4ae6f18f --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,17 @@ + + + +
+

Todo List

+ + + + +
+
+ +
From ad75ad7b683280b4e05fe4c0bf83bd7d1505cfb0 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 13:26:49 +0100 Subject: [PATCH 72/86] [IMP] Use slots and toggle button in Card --- awesome_owl/static/src/card/card.js | 18 ++++++++++++++++-- awesome_owl/static/src/card/card.xml | 9 ++++++--- awesome_owl/static/src/playground.xml | 11 +++++++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index 969ca072852..68183fc32cc 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -1,6 +1,20 @@ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; export class Card extends Component { static template = "awesome_owl.card"; - static props = ["title", "content"] + static props = { + title: String, + slots: { + type: Object, + shape: {default: Object} + } + } + + setup() { + this.state = useState({ open: true }); + } + + toggleOpen() { + this.state.open = !this.state.open; + } } diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index cb18fd44a91..e4cd318dc4f 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -3,9 +3,12 @@
-
-

- +

+ + +
+

+

diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 155097a4cb8..a689d2b119d 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -12,8 +12,15 @@

- - + + + + + content of card 2 + + + +

From d56be0ce639b4cabf77fdc612b8c139b9f51c898 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 15:02:37 +0100 Subject: [PATCH 73/86] [IMP] Add navigation buttons to dashboard --- awesome_dashboard/static/src/dashboard.js | 21 +++++++++++++++++++++ awesome_dashboard/static/src/dashboard.scss | 3 +++ awesome_dashboard/static/src/dashboard.xml | 8 +++++++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 awesome_dashboard/static/src/dashboard.scss diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index c4fb245621b..bb0628233ff 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,8 +1,29 @@ import { Component } from "@odoo/owl"; import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { Layout } from "@web/search/layout"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout }; + // static props = ["action", "actionId", "updateActionState", "className"]; + + setup() { + this.action = useService("action"); + } + + openCustomers() { + this.action.doAction("base.action_partner_form") + } + + async openLeads() { + this.action.doAction({ + type: 'ir.actions.act_window', + name: 'All Leads', + res_model: 'crm.lead', + views: [[false, 'list'], [false, 'form']], + }); + } } registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss new file mode 100644 index 00000000000..32862ec0d82 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: gray; +} diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 1a2ac9a2fed..a0857859185 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -2,7 +2,13 @@ - hello dashboard + + + + + + some content + From b0c79be05818f336662055ceecf3e8c99ea5d30c Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 15:15:00 +0100 Subject: [PATCH 74/86] [CLN] Fix format with prettier --- awesome_dashboard/static/src/dashboard.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index bb0628233ff..36b82ee6856 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -9,19 +9,22 @@ class AwesomeDashboard extends Component { // static props = ["action", "actionId", "updateActionState", "className"]; setup() { - this.action = useService("action"); + this.action = useService("action"); } openCustomers() { - this.action.doAction("base.action_partner_form") + this.action.doAction("base.action_partner_form"); } async openLeads() { this.action.doAction({ - type: 'ir.actions.act_window', - name: 'All Leads', - res_model: 'crm.lead', - views: [[false, 'list'], [false, 'form']], + type: "ir.actions.act_window", + name: "All Leads", + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"], + ], }); } } From 4e4773c2475b096d3b8f09b70c7691d406df0896 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 16:04:14 +0100 Subject: [PATCH 75/86] [IMP] Define DashboardItem --- .../static/src/dashboard_item/dashboard_item.js | 17 +++++++++++++++++ .../src/dashboard_item/dashboard_item.xml | 12 ++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 awesome_dashboard/static/src/dashboard_item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard_item/dashboard_item.xml diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..0cb70818a03 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js @@ -0,0 +1,17 @@ +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static props = { + size: { type: Number, optional: true }, + slots: { + type: Object, + shape: { default: Object }, + }, + }; + + static defaultProps = { + size: 1, + }; +} diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..19401f5a238 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml @@ -0,0 +1,12 @@ + + + + +
+
+ +
+
+
+ +
From e39892212e1685bc4f10b67de1055bf9a706d77c Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 16:04:35 +0100 Subject: [PATCH 76/86] [IMP] Fill dashboard with response of rpc --- awesome_dashboard/static/src/dashboard.js | 9 +++++++-- awesome_dashboard/static/src/dashboard.xml | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 36b82ee6856..8642e37046d 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,15 +1,20 @@ -import { Component } from "@odoo/owl"; +import { Component, onWillStart } from "@odoo/owl"; import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; import { useService } from "@web/core/utils/hooks"; import { Layout } from "@web/search/layout"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout }; + static components = { Layout, DashboardItem }; // static props = ["action", "actionId", "updateActionState", "className"]; setup() { this.action = useService("action"); + onWillStart(async () => { + this.stats = await rpc("/awesome_dashboard/statistics"); + }); } openCustomers() { diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index a0857859185..f76fc299455 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -7,7 +7,21 @@ - some content + + New orders: + + + Total orders: + + + Average quantity: + + + Canceled orders: + + + Average time: + From ca446b1b3fbb5cd57b9c806b329a5454dba50735 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 16:31:39 +0100 Subject: [PATCH 77/86] [IMP] Memoize rpc results --- awesome_dashboard/static/src/dashboard.js | 4 ++-- .../static/src/statistics_service.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 awesome_dashboard/static/src/statistics_service.js diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 8642e37046d..91258ae9e7e 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,6 +1,5 @@ import { Component, onWillStart } from "@odoo/owl"; import { registry } from "@web/core/registry"; -import { rpc } from "@web/core/network/rpc"; import { useService } from "@web/core/utils/hooks"; import { Layout } from "@web/search/layout"; import { DashboardItem } from "./dashboard_item/dashboard_item"; @@ -12,8 +11,9 @@ class AwesomeDashboard extends Component { setup() { this.action = useService("action"); + const { loadStatistics } = useService("awesome_dashboard.statistics"); onWillStart(async () => { - this.stats = await rpc("/awesome_dashboard/statistics"); + this.stats = await loadStatistics(); }); } diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js new file mode 100644 index 00000000000..62d996ddff2 --- /dev/null +++ b/awesome_dashboard/static/src/statistics_service.js @@ -0,0 +1,15 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { memoize } from "@web/core/utils/functions"; + +const loadStatistics = memoize(async () => { + return await rpc("/awesome_dashboard/statistics"); +}); + +const statisticsService = { + start(env) { + return { loadStatistics }; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); From b9b15ebe72ff8a0795a7f2c7e01b110915c03fdb Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Wed, 28 Jan 2026 17:57:12 +0100 Subject: [PATCH 78/86] [IMP] Build a reactive pie chart --- awesome_dashboard/static/src/dashboard.js | 10 +++--- awesome_dashboard/static/src/dashboard.xml | 35 +++++++++++-------- .../src/dashboard_item/dashboard_item.xml | 1 - .../static/src/pie_chart/pie_chart.js | 28 +++++++++++++++ .../static/src/pie_chart/pie_chart.xml | 7 ++++ .../static/src/statistics_service.js | 18 ++++++---- 6 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 awesome_dashboard/static/src/pie_chart/pie_chart.js create mode 100644 awesome_dashboard/static/src/pie_chart/pie_chart.xml diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 91258ae9e7e..8fe2ef578e1 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,20 +1,18 @@ -import { Component, onWillStart } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { Layout } from "@web/search/layout"; import { DashboardItem } from "./dashboard_item/dashboard_item"; +import { PieChart } from "./pie_chart/pie_chart"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout, DashboardItem }; + static components = { Layout, DashboardItem, PieChart }; // static props = ["action", "actionId", "updateActionState", "className"]; setup() { this.action = useService("action"); - const { loadStatistics } = useService("awesome_dashboard.statistics"); - onWillStart(async () => { - this.stats = await loadStatistics(); - }); + this.stats = useState(useService("awesome_dashboard.statistics")); } openCustomers() { diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index f76fc299455..edff7d69bb1 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -7,21 +7,26 @@ - - New orders: - - - Total orders: - - - Average quantity: - - - Canceled orders: - - - Average time: - + + + New orders: + + + Total orders: + + + Average quantity: + + + Canceled orders: + + + Average time: + + + + + diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml index 19401f5a238..1a0790200ab 100644 --- a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml @@ -1,4 +1,3 @@ - diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.js b/awesome_dashboard/static/src/pie_chart/pie_chart.js new file mode 100644 index 00000000000..164c6c9e41a --- /dev/null +++ b/awesome_dashboard/static/src/pie_chart/pie_chart.js @@ -0,0 +1,28 @@ +import { Component, onWillStart, onWillUnmount, useEffect, useRef } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + data: Object, + }; + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js")); + useEffect(() => this.renderChart()); + onWillUnmount(() => this.chart?.destroy()); + } + + renderChart() { + this.chart?.destroy(); + const ctx = this.canvasRef.el; + this.chart = new Chart(ctx, { + type: "doughnut", + data: { + datasets: [{ data: Object.values(this.props.data) }], + labels: Object.keys(this.props.data), + }, + }); + } +} diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..697b3287b6e --- /dev/null +++ b/awesome_dashboard/static/src/pie_chart/pie_chart.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js index 62d996ddff2..840a9b82ace 100644 --- a/awesome_dashboard/static/src/statistics_service.js +++ b/awesome_dashboard/static/src/statistics_service.js @@ -1,14 +1,18 @@ +import { reactive } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { rpc } from "@web/core/network/rpc"; -import { memoize } from "@web/core/utils/functions"; - -const loadStatistics = memoize(async () => { - return await rpc("/awesome_dashboard/statistics"); -}); const statisticsService = { - start(env) { - return { loadStatistics }; + start() { + const statistics = reactive({ isReady: false }); + async function loadStatistics() { + const updates = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, updates, { isReady: true }); + } + + setInterval(loadStatistics, 10000); + loadStatistics(); + return statistics; }, }; From f639f10212df436725b1843552bae4db7cfc2f59 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Thu, 29 Jan 2026 08:38:16 +0100 Subject: [PATCH 79/86] [MOV] awesome_dashboard: Move dashboard assets in dedicated folder --- awesome_dashboard/static/src/{ => dashboard}/dashboard.js | 0 awesome_dashboard/static/src/{ => dashboard}/dashboard.scss | 0 awesome_dashboard/static/src/{ => dashboard}/dashboard.xml | 0 .../static/src/{ => dashboard}/dashboard_item/dashboard_item.js | 0 .../static/src/{ => dashboard}/dashboard_item/dashboard_item.xml | 0 .../static/src/{ => dashboard}/pie_chart/pie_chart.js | 0 .../static/src/{ => dashboard}/pie_chart/pie_chart.xml | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.js (100%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.scss (100%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.xml (100%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard_item/dashboard_item.js (100%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard_item/dashboard_item.xml (100%) rename awesome_dashboard/static/src/{ => dashboard}/pie_chart/pie_chart.js (100%) rename awesome_dashboard/static/src/{ => dashboard}/pie_chart/pie_chart.xml (100%) diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js similarity index 100% rename from awesome_dashboard/static/src/dashboard.js rename to awesome_dashboard/static/src/dashboard/dashboard.js diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss similarity index 100% rename from awesome_dashboard/static/src/dashboard.scss rename to awesome_dashboard/static/src/dashboard/dashboard.scss diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml similarity index 100% rename from awesome_dashboard/static/src/dashboard.xml rename to awesome_dashboard/static/src/dashboard/dashboard.xml diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js similarity index 100% rename from awesome_dashboard/static/src/dashboard_item/dashboard_item.js rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml similarity index 100% rename from awesome_dashboard/static/src/dashboard_item/dashboard_item.xml rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js similarity index 100% rename from awesome_dashboard/static/src/pie_chart/pie_chart.js rename to awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml similarity index 100% rename from awesome_dashboard/static/src/pie_chart/pie_chart.xml rename to awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml From f1052e90e84777362d4ddb850747c8ed64efea48 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Thu, 29 Jan 2026 09:35:46 +0100 Subject: [PATCH 80/86] [PERF] Lazy load the AwesomeDashboard --- awesome_dashboard/__manifest__.py | 4 ++++ awesome_dashboard/static/src/dashboard/dashboard.js | 2 +- awesome_dashboard/static/src/dashboard_action.js | 10 ++++++++++ awesome_dashboard/static/src/dashboard_action.xml | 7 +++++++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 awesome_dashboard/static/src/dashboard_action.js create mode 100644 awesome_dashboard/static/src/dashboard_action.xml diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..6c152c3bad0 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -24,7 +24,11 @@ 'assets': { 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', + ('remove', 'awesome_dashboard/static/src/dashboard/*'), ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', + ] }, 'license': 'AGPL-3' } diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index 8fe2ef578e1..41fcb6ae9df 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -32,4 +32,4 @@ class AwesomeDashboard extends Component { } } -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..21c4e8cd52c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +class DashboardLoader extends Component { + static template = "awesome_dashboard.DashboardLoader"; + static components = { LazyComponent }; +} + +registry.category("actions").add("awesome_dashboard.dashboard", DashboardLoader); diff --git a/awesome_dashboard/static/src/dashboard_action.xml b/awesome_dashboard/static/src/dashboard_action.xml new file mode 100644 index 00000000000..b5502cdefc7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.xml @@ -0,0 +1,7 @@ + + + + + + + From c70c0393282797e52a842970841c004dcd802094 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Thu, 29 Jan 2026 09:50:43 +0100 Subject: [PATCH 81/86] [CLN] Remove useless empty lines --- estate/models/estate_property.py | 2 -- estate/models/estate_property_offer.py | 3 --- 2 files changed, 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 126c1e48517..f83b32ca0d6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -101,7 +101,6 @@ def action_sell(self): for record in self: if record.state == 'canceled': raise UserError('Canceled properties cannot be sold') - record.state = 'sold' return True @@ -110,7 +109,6 @@ def action_cancel(self): for record in self: if record.state == 'sold': raise UserError('Sold properties cannot be canceled') - record.state = 'canceled' return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 6e1c0e5958e..1d79d7ff48a 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -41,17 +41,14 @@ def _compute_validity(self): def create(self, vals): for record in vals: property = self.env['estate.property'].browse(record['property_id']) - if record['price'] < property.best_offer: raise UserError(f'The offer must be above {property.best_offer}') - property._set_offer_received() return super().create(vals) def action_accept(self): self.ensure_one() - self.state = 'accepted' property = self.property_id From 22cd15c9a70a9c16af056874253d4014bf588b3e Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Thu, 29 Jan 2026 14:40:21 +0100 Subject: [PATCH 82/86] [IMP] awesome_dashboard: Make dashboard generic --- .../static/src/dashboard/dashboard.js | 8 ++- .../static/src/dashboard/dashboard.xml | 26 +++----- .../dashboard_item/dashboard_item.js | 1 - .../static/src/dashboard/dashboard_items.js | 60 +++++++++++++++++++ .../src/dashboard/number_card/number_card.js | 9 +++ .../src/dashboard/number_card/number_card.xml | 10 ++++ .../pie_chart_card/pie_chart_card.js | 11 ++++ .../pie_chart_card/pie_chart_card.xml | 8 +++ 8 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index 41fcb6ae9df..f73b07d3a5e 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -3,14 +3,16 @@ import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { Layout } from "@web/search/layout"; import { DashboardItem } from "./dashboard_item/dashboard_item"; -import { PieChart } from "./pie_chart/pie_chart"; +import { NumberCard } from "./number_card/number_card"; +import { items } from "./dashboard_items"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout, DashboardItem, PieChart }; - // static props = ["action", "actionId", "updateActionState", "className"]; + static components = { Layout, DashboardItem, PieChartCard, NumberCard }; setup() { + this.items = items; this.action = useService("action"); this.stats = useState(useService("awesome_dashboard.statistics")); } diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml index edff7d69bb1..13629344e07 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -7,25 +7,13 @@ - - - New orders: - - - Total orders: - - - Average quantity: - - - Canceled orders: - - - Average time: - - - - + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js index 0cb70818a03..1b0f3997e15 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -1,5 +1,4 @@ import { Component } from "@odoo/owl"; -import { useService } from "@web/core/utils/hooks"; export class DashboardItem extends Component { static template = "awesome_dashboard.DashboardItem"; diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js new file mode 100644 index 00000000000..00cb1744f60 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,60 @@ +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; + +export const items = [ + { + id: "nb_new_orders", + description: "Number of new orders", + Component: NumberCard, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders, + }), + }, + { + id: "total_amount", + description: "Total amount of new orders", + Component: NumberCard, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount, + }), + }, + { + id: "average_quantity", + description: "Average amount of T-shirt", + Component: NumberCard, + props: (data) => ({ + title: "Average amount of T-shirt per order this month", + value: data.average_quantity, + }), + }, + { + id: "nb_cancelled_orders", + description: "Number of canceled orders", + Component: NumberCard, + props: (data) => ({ + title: "Number of canceled orders this month", + value: data.nb_cancelled_orders, + }), + }, + { + id: "average_time", + description: "Average order time", + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'canceled'", + value: data.average_time, + }), + }, + { + id: "orders_by_size", + description: "Orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "T-shirt orders by size", + stats: data.orders_by_size, + }), + }, +]; diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..0d1ae8deadf --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: String, + value: Number, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..fbdd626bc7c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js new file mode 100644 index 00000000000..bdbdd26ea9a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js @@ -0,0 +1,11 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "../pie_chart/pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PieChart }; + static props = { + title: String, + stats: Object, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml new file mode 100644 index 00000000000..e9224b20db2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,8 @@ + + + +
+ +
+ +
From d0be616d061438329932010133212731db21d958 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Thu, 29 Jan 2026 15:03:45 +0100 Subject: [PATCH 83/86] [IMP] awesome_dashboard: Make the dashboard extensible --- awesome_dashboard/static/src/dashboard/dashboard.js | 3 +-- awesome_dashboard/static/src/dashboard/dashboard_items.js | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index f73b07d3a5e..110bc37bba9 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -4,7 +4,6 @@ import { useService } from "@web/core/utils/hooks"; import { Layout } from "@web/search/layout"; import { DashboardItem } from "./dashboard_item/dashboard_item"; import { NumberCard } from "./number_card/number_card"; -import { items } from "./dashboard_items"; import { PieChartCard } from "./pie_chart_card/pie_chart_card"; class AwesomeDashboard extends Component { @@ -12,7 +11,7 @@ class AwesomeDashboard extends Component { static components = { Layout, DashboardItem, PieChartCard, NumberCard }; setup() { - this.items = items; + this.items = registry.category("awesome_dashboard").getAll(); this.action = useService("action"); this.stats = useState(useService("awesome_dashboard.statistics")); } diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js index 00cb1744f60..18239f8da3b 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_items.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -1,7 +1,8 @@ +import { registry } from "@web/core/registry"; import { NumberCard } from "./number_card/number_card"; import { PieChartCard } from "./pie_chart_card/pie_chart_card"; -export const items = [ +const items = [ { id: "nb_new_orders", description: "Number of new orders", @@ -58,3 +59,7 @@ export const items = [ }), }, ]; + +items.forEach((item) => { + registry.category("awesome_dashboard").add(item.id, item); +}); From 673268f05f55ae80242811f8f9172701ac9b8bf9 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Thu, 29 Jan 2026 16:36:19 +0100 Subject: [PATCH 84/86] [IMP] awesome_dashboard: Add and remove dashboard items --- .../static/src/dashboard/dashboard.js | 55 ++++++++++++++++++- .../static/src/dashboard/dashboard.xml | 23 +++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index 110bc37bba9..f1ac1809295 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -1,4 +1,7 @@ import { Component, useState } from "@odoo/owl"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { Dialog } from "@web/core/dialog/dialog"; +import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { Layout } from "@web/search/layout"; @@ -8,12 +11,16 @@ import { PieChartCard } from "./pie_chart_card/pie_chart_card"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout, DashboardItem, PieChartCard, NumberCard }; + static components = { Layout, DashboardItem, NumberCard, PieChartCard }; setup() { this.items = registry.category("awesome_dashboard").getAll(); this.action = useService("action"); this.stats = useState(useService("awesome_dashboard.statistics")); + this.dialog = useService("dialog"); + this.hiddenItemIds = useState( + browser.localStorage.getItem("hiddenDashboardItemIds")?.split(",") || [], + ); } openCustomers() { @@ -31,6 +38,52 @@ class AwesomeDashboard extends Component { ], }); } + + openConfig() { + this.dialog.add(ConfigDialog, { + items: this.items, + hiddenItemIds: this.hiddenItemIds, + onUpdateConfig: this.updateConfig.bind(this), + }); + } + + updateConfig(newHiddenItemIds) { + this.hiddenItemIds = newHiddenItemIds; + } +} + +class ConfigDialog extends Component { + static template = "awesome_dashboard.ConfigDialog"; + static components = { Dialog, CheckBox }; + static props = { + close: Function, + items: { type: Array, elements: Object }, + hiddenItemIds: { type: Array, elements: String }, + onUpdateConfig: { type: Function, optional: true }, + }; + + setup() { + this.items = useState( + this.props.items.map((item) => ({ + ...item, + displayed: !this.props.hiddenItemIds.includes(item.id), + })), + ); + } + + onChange(checked, itemToChange) { + itemToChange.displayed = checked; + const newHiddenItemIds = this.items + .filter((item) => !item.displayed) + .map((item) => item.id); + + browser.localStorage.setItem("hiddenDashboardItemIds", newHiddenItemIds); + this.props.onUpdateConfig?.(newHiddenItemIds); + } + + done() { + this.props.close(); + } } registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml index 13629344e07..fedf54c0b9d 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -1,4 +1,3 @@ - @@ -7,9 +6,14 @@ + + + - + @@ -18,4 +22,19 @@ + + + + + + + + + + + + + From 0042e71d1e084ea2f59073241f8e0f93f814a384 Mon Sep 17 00:00:00 2001 From: Thomas des Touches Date: Fri, 30 Jan 2026 11:41:47 +0100 Subject: [PATCH 85/86] [CLN] estate: Apply formatting and fix import order --- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 120 ++++++++++--------- estate/models/estate_property_offer.py | 48 ++++---- estate/models/res_users.py | 12 +- estate/security/ir.model.access.csv | 1 + estate/views/estate_property_offer_views.xml | 2 +- estate/views/estate_property_type_views.xml | 3 +- 7 files changed, 96 insertions(+), 92 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8cd68e5692d..60cbfa736c4 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,9 +11,9 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', - 'views/estate_property_offer_views.xml', 'views/estate_menu_views.xml', 'views/res_user_views.xml', ], diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f83b32ca0d6..39a2a1da2f6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -4,122 +4,124 @@ class EstateProperty(models.Model): - _name = 'estate.property' - _description = 'Estate Property' - _order = 'id desc' + _name = "estate.property" + _description = "Estate Property" + _order = "id desc" - name = fields.Char('Title', required=True) - description = fields.Text('Description') - postcode = fields.Char('Post Code') - property_type_id = fields.Many2one('estate.property.type', string='Property Type') - property_tag_ids = fields.Many2many('estate.property.tag', string='Tags') + name = fields.Char("Title", required=True) + description = fields.Text("Description") + postcode = fields.Char("Post Code") + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + property_tag_ids = fields.Many2many("estate.property.tag", string="Tags") date_availability = fields.Date( - 'Available From', + "Available From", copy=False, - default=fields.Date.add(fields.Date.today(), months=3) + default=fields.Date.add(fields.Date.today(), months=3), ) - expected_price = fields.Float('Expected Price', required=True) - selling_price = fields.Float('Selling Price', readonly=True, copy=False) + expected_price = fields.Float("Expected Price", required=True) + selling_price = fields.Float("Selling Price", readonly=True, copy=False) - bedrooms = fields.Integer('# Bedrooms', default=2) - living_area = fields.Integer('Living Area') - facades = fields.Integer('# Facades') - garage = fields.Boolean('Garage') + bedrooms = fields.Integer("# Bedrooms", default=2) + living_area = fields.Integer("Living Area") + facades = fields.Integer("# Facades") + garage = fields.Boolean("Garage") - garden = fields.Boolean('Garden') - garden_area = fields.Integer('Garden area') + garden = fields.Boolean("Garden") + garden_area = fields.Integer("Garden area") garden_orientation = fields.Selection( - string='Garden Orientation', + string="Garden Orientation", selection=[ - ('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West') - ] + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], ) - total_area = fields.Float(string='Total Area', compute='_compute_total_area') + total_area = fields.Float(string="Total Area", compute="_compute_total_area") - user_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.uid) - partner_id = fields.Many2one('res.partner', string='Buyer', copy=False) + user_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.uid) + partner_id = fields.Many2one("res.partner", string="Buyer", copy=False) - offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') - best_offer = fields.Float(string='Best Offer', compute='_compute_best_offer') + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + best_offer = fields.Float(string="Best Offer", compute="_compute_best_offer") active = fields.Boolean(default=True) state = fields.Selection( string="Status", selection=[ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('canceled', 'Canceled') + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled"), ], - default='new', - required=True + default="new", + required=True, ) _check_expected_price = models.Constraint( - 'CHECK(expected_price > 0)', - 'The expected price of a property should be stricly positive' + "CHECK(expected_price > 0)", + "The expected price of a property should be stricly positive", ) _check_selling_price = models.Constraint( - 'CHECK(selling_price >= 0)', - 'The selling price of a property should be positive' + "CHECK(selling_price >= 0)", + "The selling price of a property should be positive", ) - @api.depends('living_area', 'garden_area') + @api.depends("living_area", "garden_area") def _compute_total_area(self): for record in self: record.total_area = record.living_area + record.garden_area - @api.depends('offer_ids.price') + @api.depends("offer_ids.price") def _compute_best_offer(self): for record in self: - record.best_offer = max(record.offer_ids.mapped('price'), default=0) + record.best_offer = max(record.offer_ids.mapped("price"), default=0) - @api.constrains('state', 'expected_price', 'selling_price') + @api.constrains("state", "expected_price", "selling_price") def _check_prices(self): for record in self: - if record.state == 'offer_accepted' \ - and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) == -1: - raise ValidationError('The selling price must be at least 90% of the expected price') + if ( + record.state == "offer_accepted" + and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) == -1 + ): + raise ValidationError("The selling price must be at least 90% of the expected price") - @api.onchange('garden') + @api.onchange("garden") def _onchange_garden(self): if self.garden: self.garden_area = 10 - self.garden_orientation = 'north' + self.garden_orientation = "north" else: self.garden_area = None self.garden_orientation = None def action_sell(self): for record in self: - if record.state == 'canceled': - raise UserError('Canceled properties cannot be sold') - record.state = 'sold' + if record.state == "canceled": + raise UserError("Canceled properties cannot be sold") + record.state = "sold" return True def action_cancel(self): for record in self: - if record.state == 'sold': - raise UserError('Sold properties cannot be canceled') - record.state = 'canceled' + if record.state == "sold": + raise UserError("Sold properties cannot be canceled") + record.state = "canceled" return True @api.ondelete(at_uninstall=False) def _ondelete(self): for record in self: - if record.state not in ('new', 'canceled'): - raise UserError('Only new and canceled properties can be deleted') + if record.state not in ("new", "canceled"): + raise UserError("Only new and canceled properties can be deleted") def _set_offer_received(self): for record in self: - if record.state == 'new': - record.state = 'offer_received' + if record.state == "new": + record.state = "offer_received" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 1d79d7ff48a..fe568790118 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -3,30 +3,26 @@ class EstatePropertyOffer(models.Model): - _name = 'estate.property.offer' - _description = 'Estate Property Offer' - _order = 'price desc' + _name = "estate.property.offer" + _description = "Estate Property Offer" + _order = "price desc" - price = fields.Float('Price') + price = fields.Float("Price") state = fields.Selection( - string='Status', + string="Status", copy=False, readonly=True, - selection=[('accepted', 'Accepted'), ('refused', 'Refused')] + selection=[("accepted", "Accepted"), ("refused", "Refused")], ) - validity = fields.Integer(string='Validity (days)', default=7) - date_deadline = fields.Date( - string='Deadline', - compute='_compute_deadline', - inverse="_compute_validity" - ) + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date(string="Deadline", compute="_compute_deadline", inverse="_compute_validity") - property_id = fields.Many2one('estate.property', string='Property', required=True) - partner_id = fields.Many2one('res.partner', string='Partner', required=True) - property_type_id = fields.Many2one(related='property_id.property_type_id', stored=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) - @api.depends('validity') + @api.depends("validity") def _compute_deadline(self): for record in self: creation_date = record.create_date or fields.Date.today() @@ -40,38 +36,38 @@ def _compute_validity(self): @api.model def create(self, vals): for record in vals: - property = self.env['estate.property'].browse(record['property_id']) - if record['price'] < property.best_offer: - raise UserError(f'The offer must be above {property.best_offer}') + property = self.env["estate.property"].browse(record["property_id"]) + if record["price"] < property.best_offer: + raise UserError(f"The offer must be above {property.best_offer}") property._set_offer_received() return super().create(vals) def action_accept(self): self.ensure_one() - self.state = 'accepted' + self.state = "accepted" property = self.property_id property.selling_price = self.price property.partner_id = self.partner_id - if property.state == 'offer_received': - property.state = 'offer_accepted' + if property.state == "offer_received": + property.state = "offer_accepted" for offer in property.offer_ids: if offer.id != self.id: - offer.state = 'refused' + offer.state = "refused" return True def action_refuse(self): for record in self: - record.state = 'refused' + record.state = "refused" property = self.property_id - if 'accepted' not in property.offer_ids.mapped('state'): - property.state = 'offer_received' + if "accepted" not in property.offer_ids.mapped("state"): + property.state = "offer_received" property.selling_price = None property.partner_id = None diff --git a/estate/models/res_users.py b/estate/models/res_users.py index a445650f852..ee338f412f7 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -1,8 +1,12 @@ -from odoo import api, fields, models -from odoo.exceptions import UserError, ValidationError +from odoo import fields, models class ResUsers(models.Model): - _inherit = ['res.users'] + _name = "res.users" + _inherit = ["res.users"] - property_ids = fields.One2many('estate.property', 'user_id', domains = "[('state', 'in', ('new', 'offer_received'))]") + property_ids = fields.One2many( + "estate.property", + "user_id", + domain="[('state', 'in', ('new', 'offer_received'))]", + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 0c0b62b7fee..e6f4e755633 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_type_line,access_estate_property_type_line,estate.model_estate_property_type_line,base.group_user,1,1,1,1 estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 42273a0a99c..7caa56cddd6 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,5 +1,5 @@ - + Property Offers estate.property.offer diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index f85bda9b6e1..324ba42cbf0 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -24,7 +24,8 @@
-