From 39c9e6460b840e5ce1b4f446a5d5bc532feb0feb Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Mon, 16 Mar 2026 13:17:37 +0100 Subject: [PATCH 01/24] [ADD] Tutorial - Ch 1, 2, 3 - Add module, models, and basic fields --- estate/__init__.py | 1 + estate/__manifest__.py | 7 +++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 23 +++++++++++++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py 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 new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..8677f92e3d1 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,7 @@ +{ + "name": "Real Estate", + "version": "1.0", + "depends": ["base"], + "author": "Anmol Dhaliwal", + "category": "Category", +} 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..a57d34606df --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,23 @@ +from odoo import fields, models + + +class RecurringPlan(models.Model): + _name = "estate.property" + _description = "A specific property" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date('Date Available') + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string='Garden Orientation', + selection=[('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')] + ) \ No newline at end of file From 9448e1868e281341cd826008cabe8053ec6e3445 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Mon, 16 Mar 2026 13:59:17 +0100 Subject: [PATCH 02/24] [IMP] Tutorial - Chapter 4 - Security --- estate/__manifest__.py | 3 +++ estate/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8677f92e3d1..ecc0215e531 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,4 +4,7 @@ "depends": ["base"], "author": "Anmol Dhaliwal", "category": "Category", + "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..ab63520e22b --- /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 \ No newline at end of file From c8a3e916441b8bf76c1dd309beacf586808219db Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Mon, 16 Mar 2026 14:16:54 +0100 Subject: [PATCH 03/24] [IMP] Tutorial - Chapter 5 - Actions and Menus --- estate/__manifest__.py | 4 +++- estate/views/estate_menus.xml | 5 +++++ estate/views/estate_property_views.xml | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index ecc0215e531..0e32a1d1409 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,8 @@ "author": "Anmol Dhaliwal", "category": "Category", "data": [ - "security/ir.model.access.csv" + "security/ir.model.access.csv", + "views/estate_menus.xml", + "views/estate_property_views.xml" ] } diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..945e90b5550 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..67144bdb931 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,5 @@ + + Estate Properties Action + estate.property + list,form + \ No newline at end of file From 077ffd7a66754478f2d9488b0247fbe0e8a9df26 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Tue, 17 Mar 2026 09:40:23 +0100 Subject: [PATCH 04/24] [IMP] Tutorial - Chapter 5 - Fields, Attributes, View, Default Values --- estate/__init__.py | 2 +- estate/__manifest__.py | 2 +- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 32 +++++++++++++++++++++----- estate/views/estate_menus.xml | 11 +++++---- estate/views/estate_property_views.xml | 13 +++++++---- 6 files changed, 44 insertions(+), 18 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/__manifest__.py b/estate/__manifest__.py index 0e32a1d1409..6c3507e7e3a 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,7 +6,7 @@ "category": "Category", "data": [ "security/ir.model.access.csv", + "views/estate_property_views.xml", "views/estate_menus.xml", - "views/estate_property_views.xml" ] } 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 a57d34606df..4f2625b0a8d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ from odoo import fields, models - +from dateutil.relativedelta import relativedelta class RecurringPlan(models.Model): _name = "estate.property" @@ -8,10 +8,10 @@ class RecurringPlan(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date('Date Available') + date_availability = fields.Date('Date Available', copy=False, default=(fields.Date.today() + relativedelta(months=3))) expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() @@ -19,5 +19,25 @@ class RecurringPlan(models.Model): garden_area = fields.Integer() garden_orientation = fields.Selection( string='Garden Orientation', - selection=[('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')] - ) \ No newline at end of file + selection=[ + ('north', 'North'), + ('east', 'East'), + ('south', 'South'), + ('west', 'West') + ] + ) + active = fields.Boolean(default=True) + state = fields.Selection( + string='State', + selection=[ + ('new', 'New'), + ('offer_received', + 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + required=True, + copy=False, + default='new' + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 945e90b5550..fdfd89ccb38 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,5 +1,8 @@ - - - + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 67144bdb931..1789a908d0d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,5 +1,8 @@ - - Estate Properties Action - estate.property - list,form - \ No newline at end of file + + + + Action Button + estate.property + list,form + + \ No newline at end of file From b266f26e35ac7aeeb70a2a7ff8449939a32aee09 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Tue, 17 Mar 2026 11:11:30 +0100 Subject: [PATCH 05/24] [IMP] Tutorial - Chapter 6 - List, Form, and Search Views --- estate/models/estate_property.py | 7 +-- estate/views/estate_list_views.xml | 3 ++ estate/views/estate_property_views.xml | 74 ++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 estate/views/estate_list_views.xml diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4f2625b0a8d..12e3cfcb90b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,18 +1,19 @@ from odoo import fields, models from dateutil.relativedelta import relativedelta + class RecurringPlan(models.Model): _name = "estate.property" _description = "A specific property" - name = fields.Char(required=True) + name = fields.Char('Title', required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date('Date Available', copy=False, default=(fields.Date.today() + relativedelta(months=3))) + date_availability = fields.Date('Available From', copy=False, default=(fields.Date.today() + relativedelta(months=3))) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) - living_area = fields.Integer() + living_area = fields.Integer('Living Area (sqm)') facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() diff --git a/estate/views/estate_list_views.xml b/estate/views/estate_list_views.xml new file mode 100644 index 00000000000..bc404c863b7 --- /dev/null +++ b/estate/views/estate_list_views.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1789a908d0d..3b1162f6574 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,78 @@ estate.property list,form + + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + \ No newline at end of file From 1d8030970b30c0ab44a2dd7c7dbbfb024ed817fb Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Tue, 17 Mar 2026 11:20:01 +0100 Subject: [PATCH 06/24] [FIX] Datetime calculation as lambda and renaming values --- estate/models/estate_property.py | 2 +- estate/security/ir.model.access.csv | 2 +- estate/views/estate_list_views.xml | 2 +- estate/views/estate_menus.xml | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 12e3cfcb90b..63c10bada59 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -9,7 +9,7 @@ class RecurringPlan(models.Model): name = fields.Char('Title', required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date('Available From', copy=False, default=(fields.Date.today() + relativedelta(months=3))) + date_availability = fields.Date('Available From', copy=False, default=lambda _: fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index ab63520e22b..2e2c292cbd8 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access.estate.property.user,access_estate_property_user,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_list_views.xml b/estate/views/estate_list_views.xml index bc404c863b7..6fa84137e47 100644 --- a/estate/views/estate_list_views.xml +++ b/estate/views/estate_list_views.xml @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index fdfd89ccb38..ee16a48fdaa 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,8 +1,8 @@ - - + + - \ No newline at end of file + From e9e2d4c27587d6207842574e2dad28936b7a7a9a Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Tue, 17 Mar 2026 15:26:27 +0100 Subject: [PATCH 07/24] [IMP] Tutorial - Chapter 7 - Relations Between Models --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 3 ++ estate/models/estate_property.py | 8 +++- estate/models/estate_property_offer.py | 14 +++++++ estate/models/estate_property_tag.py | 8 ++++ estate/models/estate_property_type.py | 8 ++++ estate/security/ir.model.access.csv | 5 ++- estate/views/estate_menus.xml | 8 +++- estate/views/estate_property_offer_views.xml | 33 ++++++++++++++++ estate/views/estate_property_tag_views.xml | 8 ++++ estate/views/estate_property_type_views.xml | 41 ++++++++++++++++++++ estate/views/estate_property_views.xml | 32 ++++++++++----- 12 files changed, 157 insertions(+), 14 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6c3507e7e3a..4813ede88e1 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,6 +7,9 @@ "data": [ "security/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offer_views.xml", "views/estate_menus.xml", ] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..09b2099fe84 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 63c10bada59..a35b7b51b53 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -2,7 +2,7 @@ from dateutil.relativedelta import relativedelta -class RecurringPlan(models.Model): +class EstateProperty(models.Model): _name = "estate.property" _description = "A specific property" @@ -42,3 +42,9 @@ class RecurringPlan(models.Model): copy=False, default='new' ) + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + salesperson_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.uid) + buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) + tag_ids = fields.Many2many('estate.property.tag', string='Tags') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..f8a451f8879 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate.property.offer' + _description = "An offer made on a property" + + price = fields.Float(string='Price') + status = fields.Selection(copy=False, selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ],) + partner_id = fields.Many2one('res.partner', string='Partner', required=True) + property_id = fields.Many2one('estate.property', string='Property', required=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..7ac8b138354 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate.property.tag' + _description = "A property tag" + + name = fields.Char(string='Tag Name', required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..77e26c54c0f --- /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 = "A type of property" + + name = fields.Char(string='Title', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 2e2c292cbd8..b7c9f77944e 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access.estate.property.user,access_estate_property_user,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access.estate.property.user,access_estate_property_user,estate.model_estate_property,base.group_user,1,1,1,1 +access.estate.property.type.user,access_estate_property_type_user,estate.model_estate_property_type,base.group_user,1,1,1,1 +access.estate.property.tag.user,access_estate_property_tag_user,estate.model_estate_property_tag,base.group_user,1,1,1,1 +access.estate.property.offer.user,access_estate_property_offer_user,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index ee16a48fdaa..a2b5b918c3d 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,8 +1,12 @@ - - + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..a8ba6271d78 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,33 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ +

+ +

+ + + + + +
+
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..bb6cef1a6bc --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + View Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..952a742f18a --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,41 @@ + + + + View Property Types + estate.property.type + list,form + + + estate.property.type.list + estate.property.type + + + + + + + + + estate.property.type.form + estate.property.type + +
+ +

+ +

+
+
+
+
+ + + estate.property.type.search + estate.property.type + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3b1162f6574..12ff2639696 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,7 +1,7 @@ - Action Button + View Properties estate.property list,form @@ -12,6 +12,8 @@ + + @@ -31,17 +33,15 @@

+ - - - - + + + - - - - + + @@ -56,6 +56,17 @@ + + + + + + + + + + + @@ -68,6 +79,7 @@ + @@ -79,4 +91,4 @@ -
\ No newline at end of file + From cde4aaf07ca57cb2086406ed80bb73952a8e53dd Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Tue, 17 Mar 2026 15:39:00 +0100 Subject: [PATCH 08/24] [FIX] styling fixes --- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 1 - estate/models/estate_property_offer.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 09b2099fe84..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,4 +1,4 @@ from . import estate_property from . import estate_property_type from . import estate_property_tag -from . import estate_property_offer \ No newline at end of file +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a35b7b51b53..51a0c38f274 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -47,4 +47,3 @@ class EstateProperty(models.Model): buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) tag_ids = fields.Many2many('estate.property.tag', string='Tags') offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') - \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index f8a451f8879..9751fb56685 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -9,6 +9,6 @@ class EstatePropertyType(models.Model): status = fields.Selection(copy=False, selection=[ ('accepted', 'Accepted'), ('refused', 'Refused'), - ],) + ]) partner_id = fields.Many2one('res.partner', string='Partner', required=True) property_id = fields.Many2one('estate.property', string='Property', required=True) From 5bc47909bbd86f1028b022565b86a25b4d890945 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Tue, 17 Mar 2026 16:49:20 +0100 Subject: [PATCH 09/24] [IMP] Tutorial - Chapter 8 - Computed Fields and Onchanges --- estate/models/estate_property.py | 25 ++++++++++++++++++-- estate/models/estate_property_offer.py | 13 +++++++++- estate/views/estate_property_offer_views.xml | 4 ++++ estate/views/estate_property_views.xml | 2 ++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 51a0c38f274..94ada350454 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 from dateutil.relativedelta import relativedelta @@ -17,7 +17,7 @@ class EstateProperty(models.Model): facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer() + garden_area = fields.Integer(string='Garden Area (sqm)') garden_orientation = fields.Selection( string='Garden Orientation', selection=[ @@ -47,3 +47,24 @@ class EstateProperty(models.Model): buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) tag_ids = fields.Many2many('estate.property.tag', string='Tags') offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + total_area = fields.Integer(compute='_compute_total_area', string='Total Area (sqm)') + best_price = fields.Float(string='Best Price', compute='_compute_best_price') + + @api.depends('living_area', 'garden_area', 'garden') + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + (record.garden_area if record.garden else 0) + + @api.depends('offer_ids') + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped('price')) if record.offer_ids else 0 + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = None \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 9751fb56685..e38a519e581 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ -from odoo import fields, models +from odoo import api, fields, models +from dateutil.relativedelta import relativedelta class EstatePropertyType(models.Model): @@ -12,3 +13,13 @@ class EstatePropertyType(models.Model): ]) partner_id = fields.Many2one('res.partner', string='Partner', required=True) property_id = fields.Many2one('estate.property', string='Property', required=True) + validity = fields.Integer(string='Validity (days)', default=7) + date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline') + + @api.depends('validity') + def _compute_date_deadline(self): + for record in self: + record.date_deadline = (record.create_date + relativedelta(days=record.validity)) if record.create_date else (fields.Date.today() + relativedelta(days=record.validity)) + def _inverse_date_deadline(self): + for record in self: + record.validity = relativedelta(record.date_deadline, record.create_date if record.create_date else fields.Date.today()).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index a8ba6271d78..9776ce988e1 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -7,6 +7,8 @@ + + @@ -24,6 +26,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 12ff2639696..e32434bab50 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -41,6 +41,7 @@ + @@ -54,6 +55,7 @@ + From f7a41c94d45cbf6050bae48b9d756ddd60e5592b Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Wed, 18 Mar 2026 09:50:53 +0100 Subject: [PATCH 10/24] [IMP] Tutorial - Chapter 9 - Ready For Some Action? --- estate/models/estate_property.py | 26 +++++++++++++++++--- estate/models/estate_property_offer.py | 20 ++++++++++++++- estate/views/estate_property_offer_views.xml | 2 ++ estate/views/estate_property_views.xml | 5 ++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 94ada350454..39187b138c8 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, fields, models, exceptions from dateutil.relativedelta import relativedelta @@ -54,7 +54,7 @@ class EstateProperty(models.Model): def _compute_total_area(self): for record in self: record.total_area = record.living_area + (record.garden_area if record.garden else 0) - + @api.depends('offer_ids') def _compute_best_price(self): for record in self: @@ -67,4 +67,24 @@ def _onchange_garden(self): self.garden_orientation = 'north' else: self.garden_area = 0 - self.garden_orientation = None \ No newline at end of file + self.garden_orientation = None + + def action_sold(self): + for record in self: + if record.state == 'cancelled': + raise exceptions.UserError('A cancelled listing cannot be sold') + elif record.state == 'sold': + raise exceptions.UserError('This listing has already been sold') + else: + record.state = 'sold' + return True + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise exceptions.UserError('Sold listings cannot be cancelled') + elif record.state == 'cancelled': + raise exceptions.UserError('This listing is already cancelled') + else: + record.state = 'cancelled' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index e38a519e581..6d3ba539af1 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models +from odoo import api, exceptions, fields, models from dateutil.relativedelta import relativedelta @@ -20,6 +20,24 @@ class EstatePropertyType(models.Model): def _compute_date_deadline(self): for record in self: record.date_deadline = (record.create_date + relativedelta(days=record.validity)) if record.create_date else (fields.Date.today() + relativedelta(days=record.validity)) + def _inverse_date_deadline(self): for record in self: record.validity = relativedelta(record.date_deadline, record.create_date if record.create_date else fields.Date.today()).days + + def action_accept(self): + for record in self: + for offer in record.property_id.offer_ids: + if offer.status == 'accepted': + raise exceptions.UserError('An offer has already been accepted for this property') + else: + record.status = 'accepted' + record.property_id.state = 'sold' + record.property_id.buyer_id = record.partner_id + record.property_id.selling_price = record.price + return True + + def action_refuse(self): + for record in self: + record.status = 'refused' + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 9776ce988e1..8097c74844e 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3018b0a56d3..28b6c5031bc 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,22 +4,23 @@ View Properties estate.property list,form + {'search_default_available_properties': 1} estate.property.list estate.property - + - + - + @@ -30,17 +31,17 @@
-

- + - - + @@ -58,14 +59,14 @@ - - + + - + @@ -90,7 +91,7 @@ - + From 0b90b2f7485eafbc6f5a0e4dc7fd8c6e584f6fe3 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Wed, 18 Mar 2026 16:44:28 +0100 Subject: [PATCH 13/24] [FIX] Rename access record entries --- estate/security/ir.model.access.csv | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index b7c9f77944e..1062265a461 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access.estate.property.user,access_estate_property_user,estate.model_estate_property,base.group_user,1,1,1,1 -access.estate.property.type.user,access_estate_property_type_user,estate.model_estate_property_type,base.group_user,1,1,1,1 -access.estate.property.tag.user,access_estate_property_tag_user,estate.model_estate_property_tag,base.group_user,1,1,1,1 -access.estate.property.offer.user,access_estate_property_offer_user,estate.model_estate_property_offer,base.group_user,1,1,1,1 +access.estate.property.user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1 +access.estate.property.type.user,access_estate_property_type_user,model_estate_property_type,base.group_user,1,1,1,1 +access.estate.property.tag.user,access_estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1 +access.estate.property.offer.user,access_estate_property_offer_user,model_estate_property_offer,base.group_user,1,1,1,1 From f7402bc870e2948ec100fc1b52d83d34857a287c Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Wed, 18 Mar 2026 17:01:27 +0100 Subject: [PATCH 14/24] [FIX] Rename access record entries (again) --- estate/security/ir.model.access.csv | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 1062265a461..9a7a6e57d11 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access.estate.property.user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1 -access.estate.property.type.user,access_estate_property_type_user,model_estate_property_type,base.group_user,1,1,1,1 -access.estate.property.tag.user,access_estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1 -access.estate.property.offer.user,access_estate_property_offer_user,model_estate_property_offer,base.group_user,1,1,1,1 +access_estate_property_user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type_user,access_estate_property_type_user,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag_user,user,access_estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer_user,user,access_estate_property_offer_user,model_estate_property_offer,base.group_user,1,1,1,1 From d2011ff26345b4683a19ccf60290f6e0fab54e92 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Thu, 19 Mar 2026 09:04:07 +0100 Subject: [PATCH 15/24] [FIX] Rename access record entries (again x2) --- estate/models/estate_property.py | 6 ++++++ estate/security/ir.model.access.csv | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 32d47f4c72f..cec3c91d763 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -104,3 +104,9 @@ def _validate_selling_price(self): for record in self: if float_compare(record.selling_price, record.expected_price * 0.9, 2) == -1 and not float_is_zero(record.selling_price, 2): raise exceptions.ValidationError('The selling price must be at least 90%% of the expected price') + + # @api.ondelete(at_uninstall=False) + # def _check_before_delete(self): + # for record in self: + # if record.state not in ('new', 'cancelled'): + # raise exceptions.UserError('A property cannot be deleted unless its state is New or Cancelled') diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 9a7a6e57d11..1d519f0e98b 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property_user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1 -access_estate_property_type_user,access_estate_property_type_user,model_estate_property_type,base.group_user,1,1,1,1 -access_estate_property_tag_user,user,access_estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1 -access_estate_property_offer_user,user,access_estate_property_offer_user,model_estate_property_offer,base.group_user,1,1,1,1 +estate_property_user,estate_property_user,model_estate_property,base.group_user,1,1,1,1 +estate_property_type_user,estate_property_type_user,model_estate_property_type,base.group_user,1,1,1,1 +estate_property_tag_user,user,estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1 +estate_property_offer_user,user,estate_property_offer_user,model_estate_property_offer,base.group_user,1,1,1,1 From 286105af9396fe52a54a41e33d5cf1ee289c043d Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Thu, 19 Mar 2026 09:12:52 +0100 Subject: [PATCH 16/24] [FIX] Rename access record entries (again x3) --- estate/security/ir.model.access.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 1d519f0e98b..0b5f1d26656 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink estate_property_user,estate_property_user,model_estate_property,base.group_user,1,1,1,1 estate_property_type_user,estate_property_type_user,model_estate_property_type,base.group_user,1,1,1,1 -estate_property_tag_user,user,estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1 -estate_property_offer_user,user,estate_property_offer_user,model_estate_property_offer,base.group_user,1,1,1,1 +estate_property_tag_user,estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1 +estate_property_offer_user,estate_property_offer_user,model_estate_property_offer,base.group_user,1,1,1,1 From 80e0a16f36ff63f47b166f95a1c49fa4ebe7ac6e Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Thu, 19 Mar 2026 10:04:43 +0100 Subject: [PATCH 17/24] [LINT] Cleaning up various .py and .xml files --- .gitignore | 3 + estate/__manifest__.py | 2 +- estate/models/estate_property.py | 43 ++++++--- estate/models/estate_property_offer.py | 38 +++++--- estate/models/estate_property_tag.py | 7 +- estate/models/estate_property_type.py | 11 ++- estate/views/estate_menus.xml | 6 +- estate/views/estate_property_offer_views.xml | 29 +++--- estate/views/estate_property_tag_views.xml | 5 +- estate/views/estate_property_type_views.xml | 19 ++-- estate/views/estate_property_views.xml | 93 +++++++++++--------- 11 files changed, 149 insertions(+), 107 deletions(-) diff --git a/.gitignore b/.gitignore index b6e47617de1..4502b2438eb 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Linting +pyproject.toml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 593f4e1ac96..0f8bc88879d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,5 +12,5 @@ "views/estate_property_tag_views.xml", "views/estate_menus.xml", ], - 'license': 'OEEL-1' + "license": "OEEL-1", } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index cec3c91d763..ba4f75c01ad 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -4,14 +4,18 @@ class EstateProperty(models.Model): - _name = "estate.property" - _description = "A specific property" + _name = 'estate.property' + _description = 'A specific property' _order = 'id desc' name = fields.Char('Title', required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date('Available From', copy=False, default=lambda _: fields.Date.today() + relativedelta(months=3)) + date_availability = fields.Date( + 'Available From', + copy=False, + default=lambda _: fields.Date.today() + relativedelta(months=3), + ) expected_price = fields.Float(required=True) _expected_price = models.Constraint( 'CHECK(expected_price > 0)', @@ -34,41 +38,48 @@ class EstateProperty(models.Model): ('north', 'North'), ('east', 'East'), ('south', 'South'), - ('west', 'West') - ] + ('west', 'West'), + ], ) active = fields.Boolean(default=True) state = fields.Selection( string='State', selection=[ ('new', 'New'), - ('offer_received', - 'Offer Received'), + ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled'), ], required=True, copy=False, - default='new' + default='new', ) property_type_id = fields.Many2one('estate.property.type', string='Property Type') - salesperson_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.uid) + salesperson_id = fields.Many2one( + 'res.users', string='Salesperson', default=lambda self: self.env.uid + ) buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) tag_ids = fields.Many2many('estate.property.tag', string='Tags') offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') - total_area = fields.Integer(compute='_compute_total_area', string='Total Area (sqm)') + total_area = fields.Integer( + compute='_compute_total_area', string='Total Area (sqm)' + ) best_price = fields.Float(string='Best Price', compute='_compute_best_price') @api.depends('living_area', 'garden_area', 'garden') def _compute_total_area(self): for record in self: - record.total_area = record.living_area + (record.garden_area if record.garden else 0) + record.total_area = record.living_area + ( + record.garden_area if record.garden else 0 + ) @api.depends('offer_ids') def _compute_best_price(self): for record in self: - record.best_price = max(record.offer_ids.mapped('price')) if record.offer_ids else 0 + record.best_price = ( + max(record.offer_ids.mapped('price')) if record.offer_ids else 0 + ) @api.onchange('garden') def _onchange_garden(self): @@ -102,8 +113,12 @@ def action_cancel(self): @api.constrains('selling_price', 'expected_price') def _validate_selling_price(self): for record in self: - if float_compare(record.selling_price, record.expected_price * 0.9, 2) == -1 and not float_is_zero(record.selling_price, 2): - raise exceptions.ValidationError('The selling price must be at least 90%% of the expected price') + if float_compare( + record.selling_price, record.expected_price * 0.9, 2 + ) == -1 and not float_is_zero(record.selling_price, 2): + raise exceptions.ValidationError( + 'The selling price must be at least 90%% of the expected price' + ) # @api.ondelete(at_uninstall=False) # def _check_before_delete(self): diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index d72f81c9f1e..8d130303d41 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -4,7 +4,7 @@ class EstatePropertyOffer(models.Model): _name = 'estate.property.offer' - _description = "An offer made on a property" + _description = 'An offer made on a property' _order = 'price desc' price = fields.Float(string='Price') @@ -12,30 +12,48 @@ class EstatePropertyOffer(models.Model): 'CHECK(price > 0)', 'The offer price must be strictly positive.', ) - status = fields.Selection(copy=False, selection=[ - ('accepted', 'Accepted'), - ('refused', 'Refused'), - ]) + status = fields.Selection( + copy=False, + selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ], + ) partner_id = fields.Many2one('res.partner', string='Partner', required=True) property_id = fields.Many2one('estate.property', string='Property', required=True) validity = fields.Integer(string='Validity (days)', default=7) - date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline') - property_type_id = fields.Many2one('estate.property.type', related='property_id.property_type_id', store=True) + date_deadline = fields.Date( + string='Deadline', + compute='_compute_date_deadline', + inverse='_inverse_date_deadline', + ) + property_type_id = fields.Many2one( + 'estate.property.type', related='property_id.property_type_id', store=True + ) @api.depends('validity') def _compute_date_deadline(self): for record in self: - record.date_deadline = (record.create_date + relativedelta(days=record.validity)) if record.create_date else (fields.Date.today() + relativedelta(days=record.validity)) + record.date_deadline = ( + (record.create_date + relativedelta(days=record.validity)) + if record.create_date + else (fields.Date.today() + relativedelta(days=record.validity)) + ) def _inverse_date_deadline(self): for record in self: - record.validity = relativedelta(record.date_deadline, record.create_date if record.create_date else fields.Date.today()).days + record.validity = relativedelta( + record.date_deadline, + record.create_date if record.create_date else fields.Date.today(), + ).days def action_accept(self): for record in self: for offer in record.property_id.offer_ids: if offer.status == 'accepted': - raise exceptions.UserError('An offer has already been accepted for this property') + raise exceptions.UserError( + 'An offer has already been accepted for this property' + ) else: record.status = 'accepted' record.property_id.state = 'offer_accepted' diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 85fb8bf3c02..d6023c2bc55 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -3,12 +3,9 @@ class EstatePropertyType(models.Model): _name = 'estate.property.tag' - _description = "A property tag" + _description = 'A property tag' _order = 'name' name = fields.Char(string='Tag Name', required=True) - _unique_name = models.Constraint( - 'UNIQUE(name)', - 'The name must be unique.' - ) + _unique_name = models.Constraint('UNIQUE(name)', 'The name must be unique.') color = fields.Integer() diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index fb232647b38..1a4d6510ca6 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -3,18 +3,17 @@ class EstatePropertyType(models.Model): _name = 'estate.property.type' - _description = "A type of property" + _description = 'A type of property' _order = 'sequence asc' name = fields.Char(string='Title', required=True) - _unique_name = models.Constraint( - 'UNIQUE(name)', - 'The name must be unique.' + _unique_name = models.Constraint('UNIQUE(name)', 'The name must be unique.') + property_ids = fields.One2many( + 'estate.property', 'property_type_id', string='Properties' ) - property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties') sequence = fields.Integer('Sequence', default=1) offer_ids = fields.One2many('estate.property.offer', 'property_type_id') - offer_count = fields.Integer(compute="_compute_offer_count") + offer_count = fields.Integer(compute='_compute_offer_count') @api.depends('offer_ids') def _compute_offer_count(self): diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index a2b5b918c3d..802df407e49 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -2,11 +2,11 @@ - + - - + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index b88cf5d47b2..c2fc871bd4d 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -11,13 +11,16 @@ estate.property.offer.list estate.property.offer - - - - - - - - - + + + @@ -50,7 +51,7 @@ estate.property.type - + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 28b6c5031bc..daae05b83cc 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -11,16 +11,18 @@ estate.property.list estate.property - - - - - - - - - - + + + + + + + + + + @@ -31,48 +33,52 @@
-

- +

- + - - - + + + - - - + + + - - - - - - - - - + + + + + + + + + - + - - + + @@ -86,16 +92,17 @@ estate.property - - - - - - - - - - + + + + + + + + + + From c267b65afdcdebe332211ae57bd1baf431118b7e Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Thu, 19 Mar 2026 13:21:03 +0100 Subject: [PATCH 18/24] [IMP] Tutorial - Chapter 12 - Inheritance --- estate/__manifest__.py | 27 +++++++++++++------------- estate/models/__init__.py | 1 + estate/models/estate_property.py | 16 +++++++++------ estate/models/estate_property_offer.py | 17 +++++++++++++++- estate/models/res_users.py | 12 ++++++++++++ estate/views/res_users_views.xml | 15 ++++++++++++++ 6 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 0f8bc88879d..1da5968ce99 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,16 +1,17 @@ { - "name": "Real Estate", - "version": "1.0", - "depends": ["base"], - "author": "Anmol Dhaliwal", - "category": "Category", - "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_menus.xml", + 'name': 'Real Estate', + 'version': '1.0', + 'depends': ['base'], + 'author': 'Anmol Dhaliwal', + 'category': 'Category', + '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_menus.xml', + 'views/res_users_views.xml', ], - "license": "OEEL-1", + 'license': 'OEEL-1', } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..9a2189b6382 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 res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ba4f75c01ad..6b4bc2a7a10 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -61,7 +61,9 @@ class EstateProperty(models.Model): ) buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) tag_ids = fields.Many2many('estate.property.tag', string='Tags') - offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + offer_ids = fields.One2many( + 'estate.property.offer', 'property_id', string='Offers' + ) total_area = fields.Integer( compute='_compute_total_area', string='Total Area (sqm)' ) @@ -120,8 +122,10 @@ def _validate_selling_price(self): 'The selling price must be at least 90%% of the expected price' ) - # @api.ondelete(at_uninstall=False) - # def _check_before_delete(self): - # for record in self: - # if record.state not in ('new', 'cancelled'): - # raise exceptions.UserError('A property cannot be deleted unless its state is New or Cancelled') + @api.ondelete(at_uninstall=False) + def _check_before_delete(self): + for record in self: + if record.state not in ('new', 'cancelled'): + raise exceptions.UserError( + 'A property cannot be deleted unless its state is New or Cancelled' + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 8d130303d41..0beb569417c 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,6 @@ from odoo import api, exceptions, fields, models from dateutil.relativedelta import relativedelta +from odoo.tools import float_compare class EstatePropertyOffer(models.Model): @@ -20,7 +21,9 @@ class EstatePropertyOffer(models.Model): ], ) partner_id = fields.Many2one('res.partner', string='Partner', required=True) - property_id = fields.Many2one('estate.property', string='Property', required=True) + property_id = fields.Many2one( + 'estate.property', string='Property', required=True, ondelete='cascade' + ) validity = fields.Integer(string='Validity (days)', default=7) date_deadline = fields.Date( string='Deadline', @@ -65,3 +68,15 @@ def action_refuse(self): for record in self: record.status = 'refused' return True + + @api.model_create_multi + def create(self, vals_list): + for val in vals_list: + property_for_offer = self.env['estate.property'].browse(val['property_id']) + if float_compare(val['price'], property_for_offer.best_price, 2) == -1: + raise exceptions.UserError( + 'New offers cannot be lower than existing offers' + ) + property_for_offer.state = 'offer_received' + + return super().create(vals_list) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..a59fda82478 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'estate.property', + 'salesperson_id', + string='Property', + domain="['|', ('state', '=', 'New'), ('state', '=', 'Offer Received')]", + ) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..b73a5fae7d3 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form + res.users + + + + + + + + + + From aa77f292bf94fb5e81d8d3b7c6f0dc167f875f0b Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Thu, 19 Mar 2026 14:51:45 +0100 Subject: [PATCH 19/24] [IMP] Tutorial - Chapter 13 - Invoicing --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 9 +++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 34 ++++++++++++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..a531e7d3d37 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,9 @@ +{ + 'name': 'Real Estate Accounting', + 'version': '1.0', + 'depends': ['base', 'estate', 'account'], + 'author': 'Anmol Dhaliwal', + 'category': 'Category', + 'data': [], + 'license': 'OEEL-1', +} 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..dc4dc8a7e0b --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,34 @@ +from odoo import Command, models + + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + def action_sold(self): + invoice = self.env['account.move'].create( + { + 'move_type': 'out_invoice', + 'partner_id': self.buyer_id.id, + 'invoice_line_ids': [ + Command.create( + { + 'name': 'Down Payment', + 'quantity': 1, + 'price_unit': self.selling_price * 0.06, + } + ), + Command.create( + { + 'name': 'Administrative Fees', + 'quantity': 1, + 'price_unit': 100.00, + } + ), + ], + } + ) + res = super().action_sold() + return res + + def _create_invoices(self): + pass From 9ce82fbe3f46d8668d5c8363cf80325b7e356310 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Thu, 19 Mar 2026 15:56:09 +0100 Subject: [PATCH 20/24] [IMP] Tutorial - Chapter 14 - Kanban View --- estate/views/estate_property_views.xml | 33 +++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index daae05b83cc..857b048653a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ View Properties estate.property - list,form + list,form,kanban {'search_default_available_properties': 1} @@ -106,4 +106,35 @@ + + + estate.property.kanban + estate.property + + + + +
+ + +
Expected Price: +
+
Best Offer: +
+
+ Selling Price: +
+ +
+
+
+
+
+
From dcdfb4c5b222b61394484000a3e2963ea1b7c665 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Thu, 19 Mar 2026 15:57:33 +0100 Subject: [PATCH 21/24] [LINT] Remove unused variable declaration --- estate_account/models/estate_property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index dc4dc8a7e0b..4a12ba6c068 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -5,7 +5,7 @@ class EstateProperty(models.Model): _inherit = 'estate.property' def action_sold(self): - invoice = self.env['account.move'].create( + self.env['account.move'].create( { 'move_type': 'out_invoice', 'partner_id': self.buyer_id.id, From 55b2bc5f3fa2059a7733b67c8acff113a54d7a6a Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Mon, 23 Mar 2026 09:25:34 +0100 Subject: [PATCH 22/24] [ADD] Web Framework Tutorial - Chapter 1 --- awesome_owl/static/src/card/card.js | 23 +++++++++++ awesome_owl/static/src/card/card.xml | 16 ++++++++ awesome_owl/static/src/counter/counter.js | 19 +++++++++ awesome_owl/static/src/counter/counter.xml | 7 ++++ awesome_owl/static/src/playground.js | 17 +++++++- awesome_owl/static/src/playground.xml | 20 ++++++++-- awesome_owl/static/src/todo_list/todo_item.js | 25 ++++++++++++ .../static/src/todo_list/todo_item.xml | 11 +++++ awesome_owl/static/src/todo_list/todo_list.js | 40 +++++++++++++++++++ .../static/src/todo_list/todo_list.xml | 16 ++++++++ 10 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml 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.js create mode 100644 awesome_owl/static/src/todo_list/todo_list.xml diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..84ccaa5c1bd --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,23 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + + static props = { + title: {type: String}, + slots: { + type: Object, + shape: { + default: true + } + } + }; + + setup() { + this.isOpen = useState({ toggleOpen: true }); + } + + toggleCard() { + this.isOpen.toggleOpen = !this.isOpen.toggleOpen; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..76f5a132fc8 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,16 @@ + + + +
+
+
+ + +
+

+ +

+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..e06375f1f7d --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,19 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + static props = { + onChange: { type: Function, optional: true } + } + + setup() { + this.state = useState({ value: 0 }); + } + + increment() { + this.state.value++; + if (this.props.onChange) { + this.props.onChange(); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..49a6e493140 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,7 @@ + + + +

Counter:

+ +
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..66056e878a9 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,20 @@ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +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 = { Card, Counter, TodoList }; + + setup() { + this.state = useState({ sum: 2 }); + } + + value1 = "
some text 1
"; + value2 = markup("
some text 2
"); + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..84d530d4504 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,24 @@ - + -
hello world
+
+ + +

The sum is:

+
+
+ + + + + content of card 2 + +
+
+ +
-
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..281d2a51b61 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,25 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + static props = { + todo: { + type: Object, + shape: { + id: { type: Number }, + description: { type: String }, + isCompleted: { type: Boolean } + } + }, + toggleState: { type: Function }, + removeTodo: { type: Function } + } + + onChange() { + this.props.toggleState(this.props.todo.id); + } + + onDelete() { + this.props.removeTodo(this.props.todo.id) + } +} 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..b6006ef69bb --- /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.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..c0ea0374967 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,40 @@ +import { Component, useState, useRef, onMounted } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + addTodo(ev) { + if (ev.keyCode !== 13 || ev.target.value.length === 0) { + return + } + this.todos.push({ + id: this.count, + description: ev.target.value, + isCompleted: false + }); + this.count++; + } + + setup() { + this.todos = useState([]); + this.count = 1; + this.todoRef = useRef("todo_input") + onMounted(() => { + this.todoRef.el.focus(); + }); + } + + toggleTodoItem(id) { + const todoItem = this.todos.find(todo => { + return todo.id === id + }); + todoItem.isCompleted = !todoItem.isCompleted; + } + + deleteTodoItem(id) { + const todoItemIndex = this.todos.findIndex((todo) => todo.id === id); + this.todos.splice(todoItemIndex, 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..933ff634276 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,16 @@ + + + + +
    +
  • + +
  • +
+
+
From 0edbed8f162e705b784aa253eb67a87bbaa62b13 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Mon, 23 Mar 2026 16:38:12 +0100 Subject: [PATCH 23/24] [ADD] Web Framework Tutorial - Chapter 2 - Ch 1...8 --- awesome_dashboard/static/src/dashboard.js | 8 -- awesome_dashboard/static/src/dashboard.xml | 8 -- .../static/src/dashboard/dashboard.js | 43 ++++++++++ .../static/src/dashboard/dashboard.scss | 3 + .../static/src/dashboard/dashboard.xml | 57 ++++++++++++ .../static/src/dashboard/dashboard_item.js | 21 +++++ .../static/src/dashboard/dashboard_item.xml | 13 +++ .../static/src/dashboard/pie_chart.js | 86 +++++++++++++++++++ .../static/src/dashboard/pie_chart.xml | 12 +++ .../src/dashboard/statistics_service.js | 24 ++++++ .../static/src/dashboard_loader.js | 14 +++ 11 files changed, 273 insertions(+), 16 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js delete mode 100644 awesome_dashboard/static/src/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart.xml create mode 100644 awesome_dashboard/static/src/dashboard/statistics_service.js create mode 100644 awesome_dashboard/static/src/dashboard_loader.js diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index c4fb245621b..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..bef9299b7f0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,43 @@ +import { Component, onWillStart, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { AwesomeDashboardItem } from "./dashboard_item"; +import { rpc } from "@web/core/network/rpc"; +import { PieChart } from "./pie_chart"; + +class AwesomeDashboard extends Component { + static components = { AwesomeDashboardItem, PieChart, Layout }; + static template = "awesome_dashboard.AwesomeDashboard"; + + setup() { + this.action = useService("action"); + this.stats = useState(useService("awesome_dashboard.statistics")); + + onWillStart(async () => { + const res = await rpc("/awesome_dashboard/statistics"); + console.log(res); + Object.assign(this.stats, res); + }); + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [ + [false, "form"], + [false, "list"], + ], + }); + } +} + +registry + .category("lazy_components") + .add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..32862ec0d82 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: gray; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..dd22af45769 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,57 @@ + + + + + + + + + +
Average amount of t-shirt by order this month
+
+ + +
+ Average time for an order to go from 'new' to 'sent' or + 'cancelled' +
+
+ + +
Number of new orders this month
+
+ + +
Number of cancelled orders this month
+
+ + +
Total amount of new orders this month
+
+ + +
Shirt orders by size
+ +
+ + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js new file mode 100644 index 00000000000..e3e590364f1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,21 @@ +import { Component } from "@odoo/owl"; +import { Layout } from "@web/search/layout"; + +export class AwesomeDashboardItem extends Component { + static components = { Layout }; + static template = "awesome_dashboard.AwesomeDashboardItem"; + + static defaultProps = { + size: 1, + }; + + static props = { + size: { type: Number, optional: true }, + slots: { + type: Object, + shape: { + default: true, + }, + }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item.xml new file mode 100644 index 00000000000..788ff534ac9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.xml @@ -0,0 +1,13 @@ + + + +
+

+ +

+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart.js new file mode 100644 index 00000000000..25cba9eb5ad --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart.js @@ -0,0 +1,86 @@ +import { Component, useEffect, useRef, onWillStart } from "@odoo/owl"; +import { Layout } from "@web/search/layout"; +import { loadJS } from "@web/core/assets"; + +const D3_COLORS = [ + "#1f77b4", + "#ff7f0e", + "#aec7e8", + "#ffbb78", + "#2ca02c", + "#98df8a", + "#d62728", + "#ff9896", + "#9467bd", + "#c5b0d5", + "#8c564b", + "#c49c94", + "#e377c2", + "#f7b6d2", + "#7f7f7f", + "#c7c7c7", + "#bcbd22", + "#dbdb8d", + "#17becf", + "#9edae5", +]; + +export class PieChart extends Component { + static components = { Layout }; + static template = "awesome_dashboard.PieChart"; + + static defaultProps = { + s: 0, + m: 0, + l: 0, + xl: 0, + xxl: 0, + }; + + setup() { + onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js")); + this.canvasRef = useRef("canvas"); + useEffect(() => this.renderChart()); + } + + destroyChart() { + if (this.chart) { + this.chart.destroy(); + } + } + + renderChart() { + this.destroyChart(); + const ctx = this.canvasRef.el.getContext("2d"); + this.chart = new Chart(ctx, this.getChartConfig()); + } + +getChartConfig() { + const data = this.props.data || {}; + const labels = Object.keys(data); + const counts = Object.values(data); + + return { + type: "pie", + data: { + labels: labels, + datasets: [ + { + data: counts, + backgroundColor: labels.map((_, index) => D3_COLORS[index % 20]), + hoverOffset: 4 + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + } + } + }, + }; +} +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart.xml new file mode 100644 index 00000000000..65b8b37895d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart.xml @@ -0,0 +1,12 @@ + + + +
+
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js new file mode 100644 index 00000000000..4b7cb82181f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,24 @@ +import { memoize } from "@web/core/utils/functions"; +import { reactive } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; + +const statisticsService = { + start() { + const stats = reactive({ isReady: false }); + + async function loadData() { + const res = memoize(() => rpc("/awesome_dashboard/statistics")); + Object.assign(stats, res, { isReady: true }); + } + + setInterval(loadData, 600); + loadData(); + + return stats; + }, +}; + +registry + .category("services") + .add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..1212ccf2a5c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,14 @@ +import { LazyComponent } from "@web/core/assets"; +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +export class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry + .category("actions") + .add("awesome_dashboard.dashboard", AwesomeDashboardLoader); From b1bc98e4a9deafa03a3e9a8169e110b895daefe3 Mon Sep 17 00:00:00 2001 From: Anmol Dhaliwal Date: Wed, 25 Mar 2026 16:16:50 +0100 Subject: [PATCH 24/24] [IMP] Web Framework Tutorial - Chapter 2 - Ch 9, 10, 11 --- .../static/src/dashboard/dashboard.js | 77 ++++++++++++++++++- .../static/src/dashboard/dashboard.xml | 70 +++++++---------- .../static/src/dashboard/dashboard_items.js | 65 ++++++++++++++++ .../src/dashboard/number_card/number_card.js | 9 +++ .../src/dashboard/number_card/number_card.xml | 9 +++ .../dashboard/{ => pie_chart}/pie_chart.js | 0 .../dashboard/{ => pie_chart}/pie_chart.xml | 0 .../pie_chart_card/pie_chart_card.js | 13 ++++ .../pie_chart_card/pie_chart_card.xml | 9 +++ 9 files changed, 206 insertions(+), 46 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 rename awesome_dashboard/static/src/dashboard/{ => pie_chart}/pie_chart.js (100%) rename awesome_dashboard/static/src/dashboard/{ => pie_chart}/pie_chart.xml (100%) 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 bef9299b7f0..da1ae0c84bf 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -4,7 +4,10 @@ import { Layout } from "@web/search/layout"; import { useService } from "@web/core/utils/hooks"; import { AwesomeDashboardItem } from "./dashboard_item"; import { rpc } from "@web/core/network/rpc"; -import { PieChart } from "./pie_chart"; +import { PieChart } from "./pie_chart/pie_chart"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; class AwesomeDashboard extends Component { static components = { AwesomeDashboardItem, PieChart, Layout }; @@ -13,6 +16,15 @@ class AwesomeDashboard extends Component { setup() { this.action = useService("action"); this.stats = useState(useService("awesome_dashboard.statistics")); + this.items = registry.category("awesome_dashboard").getAll(); + this.dialog = useService("dialog"); + + const hiddenItems = JSON.parse( + browser.localStorage + .getItem("disabled_dashboard_items") + ?.split(",") || '[]', + ); + this.state = useState({ disabledItems: hiddenItems }); onWillStart(async () => { const res = await rpc("/awesome_dashboard/statistics"); @@ -36,8 +48,65 @@ class AwesomeDashboard extends Component { ], }); } + + openConfig() { + this.dialog.add(ConfigDialog, { + items: this.items, + disabled: this.state.disabledItems, + onUpdate: (newDisabledItems) => { + this.state.disabledItems = newDisabledItems; + browser.localStorage.setItem( + "disabled_dashboard_items", + JSON.stringify(newDisabledItems), + ); + }, + }); + } +} + +class ConfigDialog extends Component { + static template = "awesome_dashboard.config"; + static components = { CheckBox, Dialog }; + + static props = { + items: Array, + close: Function, + disabled: Array, + onUpdate: Function, + }; + + setup() { + this.items = useState( + this.props.items.map((item) => ({ + ...item, + isEnabled: !this.props.disabled.includes(item.id), + })), + ); + } + + apply() { + const disabledIds = this.items + .filter((i) => !i.isEnabled) + .map((i) => i.id); + + this.props.onUpdate(disabledIds); + this.props.close(); + } + + onCheck(item, value) { + console.log(item, value) + item.isEnabled = value; + const updatedDisabledItemsList = Object.values(this.items) + .filter((item) => !item.isEnabled) + .map((item) => item.id); + + browser.localStorage.setItem( + "disabled_dashboard_items", + updatedDisabledItemsList, + ); + + this.props.onUpdate(updatedDisabledItemsList); + } } -registry - .category("lazy_components") - .add("AwesomeDashboard", AwesomeDashboard); +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 dd22af45769..51e490e7843 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -10,48 +10,34 @@ Leads - -
Average amount of t-shirt by order this month
-
- - -
- Average time for an order to go from 'new' to 'sent' or - 'cancelled' -
-
- - -
Number of new orders this month
-
- - -
Number of cancelled orders this month
-
- - -
Total amount of new orders this month
-
- - -
Shirt orders by size
- -
+ + + + + + + + + + + + +

Which cards do you want to see?

+ + + + + + + + +
+
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..ea84f5acce2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,65 @@ +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; +import {registry} from "@web/core/registry"; + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity, + }), + }, + { + id: "average_time", + description: "Average order processing time", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled' ", + value: data.average_time, + }), + }, + { + 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: "nb_cancelled_orders", + description: "Number of cancelled orders", + Component: NumberCard, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders, + }), + }, + { + id: "total_amount", + description: "Total new orders", + Component: NumberCard, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount, + }), + }, + { + id: "orders_by_size", + description: "Shirt orders by size", + Component: PieChartCard, + props: (data) => ({ + title: "Shirt orders by size", + data: data.orders_by_size, + }), + }, +]; + +items.forEach((item) => { + registry.category("awesome_dashboard").add(item.id, item); +}); 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..0a18342f386 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,9 @@ + + + +
+

+

+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js similarity index 100% rename from awesome_dashboard/static/src/dashboard/pie_chart.js rename to awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml similarity index 100% rename from awesome_dashboard/static/src/dashboard/pie_chart.xml rename to awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml 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..3352040539d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js @@ -0,0 +1,13 @@ +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, + data: 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..064c5263f64 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,9 @@ + + + +
+

+ +
+
+