From 9a8f241210fa65f4376700f30772e464d9199f4b Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 16 Feb 2026 14:24:24 +0100 Subject: [PATCH 01/50] [IMP] awesomeclicker: add editor metadata in Python file Added a new line 'editor': "jupao" in the module code to indicate the editor used for this script. This helps with code clarity and consistency within the module. --- awesome_clicker/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py index 56dc2f779b9..155a2286e64 100644 --- a/awesome_clicker/__manifest__.py +++ b/awesome_clicker/__manifest__.py @@ -11,6 +11,7 @@ """, 'author': "Odoo", + 'editor': "jupao", 'website': "https://www.odoo.com/", 'category': 'Tutorials', 'version': '0.1', From 169daed6846ce2e34eb6ed2df0251a979579b84a Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 16 Feb 2026 17:28:26 +0100 Subject: [PATCH 02/50] [ADD] estate: initialize tutorial - Initialize module structure - Create estate.property model - Add basic fields and attributes - Add security access rules - Add action and menus for UI --- estate/__init__.py | 1 + estate/__manifest__.py | 29 ++++++++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 54 +++++++++++++++++++++++++++ estate/security/ir.model.access.csv | 2 + estate/views/estate_menus.xml | 21 +++++++++++ estate/views/state_property_views.xml | 11 ++++++ settings.json | 7 ++++ 8 files changed, 126 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 create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/state_property_views.xml create mode 100644 settings.json diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..915e38ce961 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Real Estate", + + 'summary': """ + Starting module for "Master the Odoo web framework, chapter 1: Build a new application" + """, + + 'description': """ + Starting module for "Master the Odoo web framework, chapter 1: Build a new application" + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials', + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['base'], + + 'data': [ + 'security/ir.model.access.csv', + 'views/state_property_views.xml', + 'views/estate_menus.xml' + ], + + 'assets': {}, + 'license': 'AGPL-3' +} 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..5d40d94d069 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,54 @@ +from odoo import models, fields +from datetime import timedelta + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + + date_availability = fields.Date( + copy=False, + default=lambda self: fields.Date.today() + timedelta(days=90) + ) + + 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 (sqm)") + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer("Garden Area (sqm)") + + garden_orientation = fields.Selection( + [ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], + ) + + active = fields.Boolean(default=True) + + state = fields.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/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..0e11f47e58d --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..6cc40b0f2da --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/estate/views/state_property_views.xml b/estate/views/state_property_views.xml new file mode 100644 index 00000000000..1eec051bbd7 --- /dev/null +++ b/estate/views/state_property_views.xml @@ -0,0 +1,11 @@ + + + + + + Properties + estate.property + list,form + + + diff --git a/settings.json b/settings.json new file mode 100644 index 00000000000..efb1648f8e2 --- /dev/null +++ b/settings.json @@ -0,0 +1,7 @@ +{ + "python.linting.flake8Enabled": true, + "python.linting.enabled": true, + "python.linting.flake8Args": [ + "--ignore=E501,E301,E302" + ] +} From aa17f8bda3f27abbfe4b935745d3635ee21c4287 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Tue, 17 Feb 2026 11:13:26 +0100 Subject: [PATCH 03/50] [ADD] estate: define list, form and search views for properties Add custom views for the `estate.property` model to improve user interface and data interaction. The commit introduces: - A list view showing key fields: name, postcode, bedrooms, living area, expected price, selling price, and available date. - A form view with structured groups and a notebook for detailed information including description, property features, and reserved fields like active and state. - A search view with filters and group-by options to easily find and categorize properties, including an "Available" filter for new and offer_received properties, and a group-by on postcode. This improves the usability of the real estate module, replacing default auto-generated views with tailored ones that match business requirements. Closes task-6 --- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 2 +- estate/views/estate_menus.xml | 2 +- estate/views/estate_property_views.xml | 114 +++++++++++++++++++++++++ estate/views/state_property_views.xml | 11 --- 5 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 estate/views/estate_property_views.xml delete mode 100644 estate/views/state_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 915e38ce961..c17cbfdb6fe 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -20,7 +20,7 @@ 'data': [ 'security/ir.model.access.csv', - 'views/state_property_views.xml', + 'views/estate_property_views.xml', 'views/estate_menus.xml' ], diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 5d40d94d069..edb6541dbc2 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -10,7 +10,7 @@ class EstateProperty(models.Model): description = fields.Text() postcode = fields.Char() - date_availability = fields.Date( + available_from = fields.Date( copy=False, default=lambda self: fields.Date.today() + timedelta(days=90) ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 6cc40b0f2da..4dd506fd2f2 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,4 +1,4 @@ - + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..b9e2f8c8a09 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,114 @@ + + + + + + Properties + estate.property + list,form + + + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ + +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/estate/views/state_property_views.xml b/estate/views/state_property_views.xml deleted file mode 100644 index 1eec051bbd7..00000000000 --- a/estate/views/state_property_views.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Properties - estate.property - list,form - - - From e44c44ee2a1680be8447dace847ce2b22d8af059 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Tue, 17 Feb 2026 15:08:16 +0100 Subject: [PATCH 04/50] [IMP] estate: add property types, tags and offers models Introduce estate.property.type and estate.property.tag models. Extend estate.property with: - Many2one relation to property type - Many2many relation to property tags - One2many relation to property offers Add corresponding views and menus where required. Fix flake8 issues to satisfy ci/style checks. --- .flake8 | 4 ++ awesome_dashboard/__init__.py | 1 - awesome_dashboard/controllers/__init__.py | 2 +- awesome_dashboard/controllers/controllers.py | 1 - awesome_gallery/models/ir_action.py | 2 +- awesome_owl/__init__.py | 2 +- awesome_owl/controllers/__init__.py | 2 +- estate/__manifest__.py | 1 + estate/models/__init__.py | 5 ++- estate/models/estate_property.py | 27 ++++++++++++ estate/models/estate_property_offer.py | 24 +++++++++++ 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 | 38 +++++++++++++++++ estate/views/estate_property_offer_views.xml | 33 ++++++++++++++ estate/views/estate_property_views.xml | 45 ++++++++++++-------- 17 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 .flake8 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 diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000000..20e70b32c5c --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +ignore = E501,E301,E302,F401 +max-line-length = 88 +exclude = __pycache__,.git \ No newline at end of file diff --git a/awesome_dashboard/__init__.py b/awesome_dashboard/__init__.py index b0f26a9a602..153a9e31ebb 100644 --- a/awesome_dashboard/__init__.py +++ b/awesome_dashboard/__init__.py @@ -1,3 +1,2 @@ # -*- coding: utf-8 -*- - from . import controllers diff --git a/awesome_dashboard/controllers/__init__.py b/awesome_dashboard/controllers/__init__.py index 457bae27e11..b0f26a9a602 100644 --- a/awesome_dashboard/controllers/__init__.py +++ b/awesome_dashboard/controllers/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import controllers \ No newline at end of file +from . import controllers diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py index 05977d3bd7f..c46cfc63bfa 100644 --- a/awesome_dashboard/controllers/controllers.py +++ b/awesome_dashboard/controllers/controllers.py @@ -33,4 +33,3 @@ def get_statistics(self): }, 'total_amount': random.randint(100, 1000) } - diff --git a/awesome_gallery/models/ir_action.py b/awesome_gallery/models/ir_action.py index eae20acbf5c..d796bc8beae 100644 --- a/awesome_gallery/models/ir_action.py +++ b/awesome_gallery/models/ir_action.py @@ -7,4 +7,4 @@ class ActWindowView(models.Model): view_mode = fields.Selection(selection_add=[ ('gallery', "Awesome Gallery") - ], ondelete={'gallery': 'cascade'}) + ], ondelete={'gallery': 'cascade'}) diff --git a/awesome_owl/__init__.py b/awesome_owl/__init__.py index 457bae27e11..b0f26a9a602 100644 --- a/awesome_owl/__init__.py +++ b/awesome_owl/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import controllers \ No newline at end of file +from . import controllers diff --git a/awesome_owl/controllers/__init__.py b/awesome_owl/controllers/__init__.py index 457bae27e11..b0f26a9a602 100644 --- a/awesome_owl/controllers/__init__.py +++ b/awesome_owl/controllers/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import controllers \ No newline at end of file +from . import controllers diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c17cbfdb6fe..a61ff76420b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -20,6 +20,7 @@ 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_offer_views.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml' ], diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..2a1d3a13388 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ -from . import estate_property \ No newline at end of file +from . import estate_property_type +from . import estate_property_tag +from . import estate_property +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index edb6541dbc2..ad734658cf2 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -52,3 +52,30 @@ class EstateProperty(models.Model): copy=False, default='new' ) + # Many2one links + property_type_id = fields.Many2one( + 'estate.property.type', + string='Property Type' + ) + buyer_id = fields.Many2one( + 'res.partner', + string='Buyer', + copy=False + ) + salesperson_id = fields.Many2one( + 'res.users', + string='Salesperson', + default=lambda self: self.env.user + ) + # Many2many links + tag_ids = fields.Many2many( + 'estate.property.tag', + string="Tags" + ) + + # One2many links + offer_ids = fields.One2many( + 'estate.property.offer', + 'property_id', + string="Offers" + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..44a90e2e97f --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,24 @@ +from odoo import models, fields + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Property Offer" + + price = fields.Float(required=True) + status = fields.Selection( + [('accepted', 'Accepted'), ('refused', 'Refused')], + string="Status", + copy=False + ) + partner_id = fields.Many2one( + 'res.partner', + string="Buyer", + required=True + ) + property_id = fields.Many2one( + 'estate.property', + string="Property", + required=True, + ondelete='cascade' + ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..7d6f197c1eb --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Property Tag" + + name = fields.Char(required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..ad121b33163 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 0e11f47e58d..89f97c50842 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 4dd506fd2f2..151ff098ebe 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -18,4 +18,42 @@ parent="estate_menu_properties" action="estate_property_action"/> + + + + + + + Property Type + estate.property.type + list,form + + + + + + + + + + + Property Tags + estate.property.tag + list,form + + +
+ diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..64327ab0023 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,33 @@ + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + +
+
+
+ + + + estate.property.offer.list + estate.property.offer + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index b9e2f8c8a09..806b2d387ce 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -15,13 +15,15 @@ estate.property - - - - - - - + + + + + + + + + @@ -42,6 +44,7 @@ + @@ -66,6 +69,21 @@ + + + + + + + + + + + + + + + @@ -94,6 +112,9 @@ name="group_by_postcode" context="{'group_by':'postcode'}"/> + + + @@ -101,14 +122,4 @@ - - - - - - - - - - From 7b475b6ebe27a2e53c391b4a62b66c24bb5fb9a9 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Tue, 17 Feb 2026 16:44:52 +0100 Subject: [PATCH 05/50] [IMP] estate: chapter 8 - computed fields and onchange - estate.property: * added total_area computed field (living_area + garden_area) * added best_price computed field (maximum of offer prices) * added @onchange on 'garden' to set default garden_area=10 and garden_orientation='north' - estate.property.offer: * added validity_date computed field (with inverse) --- .gitignore | 3 ++ estate/__manifest__.py | 1 - estate/models/estate_property.py | 41 +++++++++++++++-- estate/models/estate_property_offer.py | 25 ++++++++++- estate/views/estate_menus.xml | 47 +++++--------------- estate/views/estate_property_offer_views.xml | 2 + estate/views/estate_property_views.xml | 6 ++- 7 files changed, 82 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index b6e47617de1..e64747d9b18 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +.flake8 +settings.json diff --git a/estate/__manifest__.py b/estate/__manifest__.py index a61ff76420b..03b06879b35 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- { 'name': "Real Estate", diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ad734658cf2..93255ce963b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ -from odoo import models, fields -from datetime import timedelta +from odoo import models, fields, api +from dateutil.relativedelta import relativedelta class EstateProperty(models.Model): @@ -12,7 +12,7 @@ class EstateProperty(models.Model): available_from = fields.Date( copy=False, - default=lambda self: fields.Date.today() + timedelta(days=90) + default=lambda self: fields.Date.today() + relativedelta(months=3) ) expected_price = fields.Float(required=True) @@ -38,6 +38,15 @@ class EstateProperty(models.Model): ], ) + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = False + active = fields.Boolean(default=True) state = fields.Selection( @@ -79,3 +88,29 @@ class EstateProperty(models.Model): 'property_id', string="Offers" ) + + # Computed Fields + total_area = fields.Float( + string="Total Area (sqm)", + compute="_compute_total_area", + store=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 + + best_offer = fields.Float( + string="Best Offer", + compute="_compute_best_offer", + store=True + ) + + @api.depends('offer_ids.price') + def _compute_best_offer(self): + for record in self: + if record.offer_ids: + record.best_offer = max(record.offer_ids.mapped('price')) + else: + record.best_offer = 0.0 diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 44a90e2e97f..ca926cd8102 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ -from odoo import models, fields +from odoo import models, fields, api +from datetime import timedelta class EstatePropertyOffer(models.Model): @@ -22,3 +23,25 @@ class EstatePropertyOffer(models.Model): required=True, ondelete='cascade' ) + + validity = fields.Integer( + string="Validity (days)", + default=7 + ) + + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + store=True + ) + @api.depends('validity', 'create_date') + def _compute_date_deadline(self): + for record in self: + create_date = record.create_date.date() if record.create_date else fields.Date.today() + record.date_deadline = create_date + timedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + create_date = record.create_date.date() if record.create_date else fields.Date.today() + record.validity = (record.date_deadline - create_date).days diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 151ff098ebe..48f0387f95d 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -2,28 +2,21 @@ - + - - + + + - - + + + + + + - - + @@ -32,28 +25,10 @@ list,form - - - - - - Property Tags estate.property.tag list,form - - - diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 64327ab0023..3adbd536c90 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -10,6 +10,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 806b2d387ce..d76410daaa1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -50,6 +50,7 @@ + @@ -65,8 +66,7 @@ - - + @@ -74,6 +74,8 @@ + + From 8b7b934fe965c96ff81dfc1d5df3e23b1d0d8f03 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Tue, 17 Feb 2026 17:23:37 +0100 Subject: [PATCH 06/50] [REF] estate: add action buttons for properties and offers Chapter 9: link business logic to UI actions - estate.property: * Add "Cancel" and "Sold" buttons in form view header. * Enforce state constraints: a cancelled property cannot be sold and a sold property cannot be cancelled. * Raise UserError when state transitions are invalid. - estate.property.offer: * Add "Accept" and "Refuse" buttons. * Accepting an offer sets the buyer and selling price on the related property. * Only one offer can be accepted per property. - Updated views to include buttons in header sections. - Added Python methods implementing the business logic for these actions. --- estate/models/estate_property.py | 15 +++++++++++++++ estate/models/estate_property_offer.py | 18 ++++++++++++++++++ estate/views/estate_property_offer_views.xml | 4 ++++ estate/views/estate_property_views.xml | 6 ++++++ 4 files changed, 43 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 93255ce963b..00bfcee8952 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import models, fields, api +from odoo.exceptions import UserError from dateutil.relativedelta import relativedelta @@ -114,3 +115,17 @@ def _compute_best_offer(self): record.best_offer = max(record.offer_ids.mapped('price')) else: record.best_offer = 0.0 + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise UserError("Sold properties cannot be cancelled.") + record.state = 'cancelled' + return True + + def action_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError("Cancelled properties cannot be sold.") + record.state = 'sold' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index ca926cd8102..d8b7225d367 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ from odoo import models, fields, api +from odoo.exceptions import UserError from datetime import timedelta @@ -45,3 +46,20 @@ def _inverse_date_deadline(self): for record in self: create_date = record.create_date.date() if record.create_date else fields.Date.today() record.validity = (record.date_deadline - create_date).days + + def action_accept(self): + for offer in self: + property = offer.property_id + # Check if another offer was already accepted + if property.offer_ids.filtered(lambda o: o.status == 'accepted'): + raise UserError("Only one offer can be accepted per property.") + offer.status = 'accepted' + property.selling_price = offer.price + property.buyer_id = offer.partner_id + property.state = 'offer_accepted' + return True + + def action_refuse(self): + for offer in self: + offer.status = 'refused' + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 3adbd536c90..4d9402cc0fd 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -6,6 +6,10 @@ estate.property.offer
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d76410daaa1..92e1c2bf3a0 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -35,6 +35,10 @@ +
+

@@ -76,6 +80,8 @@ +

- @@ -75,7 +75,7 @@ - + From e30a69a683f3cef4f0139b4395f9583429892262 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Wed, 18 Feb 2026 16:19:34 +0100 Subject: [PATCH 08/50] [IMP] estate: enhance property views, search and stats -Add inline property type class -Add widget bar - Color decorations for property and offer list views based on state/status - Editable list views for offers and tags - Hide availability date by default in property list - Set default 'Available' filter in property search - Make living area search return properties with area >= entered value - Add stat button on property type form to show related offers - Refine offer count display and button layout --- estate/__manifest__.py | 2 + estate/models/estate_property.py | 1 + estate/models/estate_property_offer.py | 17 ++++ estate/models/estate_property_tag.py | 3 +- estate/models/estate_property_type.py | 57 ++++++++++++- estate/views/estate_menus.xml | 16 +--- estate/views/estate_property_offer_views.xml | 21 ++++- estate/views/estate_property_tag_views.xml | 19 +++++ estate/views/estate_property_type_views.xml | 73 ++++++++++++++++ estate/views/estate_property_views.xml | 89 +++++++++++++------- 10 files changed, 247 insertions(+), 51 deletions(-) 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 03b06879b35..65f93a2a062 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -21,6 +21,8 @@ 'security/ir.model.access.csv', 'views/estate_property_offer_views.xml', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', 'views/estate_menus.xml' ], diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index c10cb14b29e..16001b122fa 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -7,6 +7,7 @@ class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property" + _order = "id desc" name = fields.Char(required=True) description = fields.Text() diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index dc95dccc8ee..1dc67fb0a49 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -6,6 +6,7 @@ class EstatePropertyOffer(models.Model): _name = "estate.property.offer" _description = "Property Offer" + _order = "price desc" price = fields.Float(required=True) status = fields.Selection( @@ -25,6 +26,13 @@ class EstatePropertyOffer(models.Model): ondelete='cascade' ) + property_type_id = fields.Many2one( + 'estate.property.type', + string="Property Type", + related='property_id.property_type_id', + store=True + ) + validity = fields.Integer( string="Validity (days)", default=7 @@ -71,3 +79,12 @@ def action_refuse(self): 'The offer price must be strictly positive.', ), ] + + @api.model + def create(self, vals): + offer = super().create(vals) + property = offer.property_id + # Si la propriété est encore "new", on passe à "offer_received" + if property.state == 'new': + property.state = 'offer_received' + return offer diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index b1ac0e4ffb3..e7ba115d30a 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -5,7 +5,8 @@ class EstatePropertyTag(models.Model): _name = "estate.property.tag" _description = "Property Tag" - + _order = "name" + color = fields.Integer("Color") name = fields.Char(required=True) @api.constrains('name') diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index e732c4e8570..c5ba7af01ea 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,16 +1,69 @@ from odoo import models, fields, api from odoo.exceptions import ValidationError - class EstatePropertyType(models.Model): _name = "estate.property.type" _description = "Real Estate Property Type" + _order = "sequence, name" + + sequence = fields.Integer( + string="Sequence", + default=10, + help="Used to order property types manually" + ) name = fields.Char(required=True) + # One2many vers le vrai modèle estate.property + property_ids = fields.One2many( + 'estate.property', + 'property_type_id', # le Many2one dans estate.property + string="Properties" + ) + + offer_ids = fields.One2many( + 'estate.property.offer', + 'property_type_id', + string="Offers" + ) + + offer_count = fields.Integer( + string="Offer Count", + compute='_compute_offer_count' + ) + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) + @api.constrains('name') def _check_name_unique(self): for record in self: - existing = self.search([('name', '=', record.name), ('id', '!=', record.id)]) + existing = self.search([ + ('name', '=', record.name), + ('id', '!=', record.id) + ]) if existing: raise ValidationError("The property type name must be unique.") + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + + name = fields.Char(required=True) + expected_price = fields.Float() + state = fields.Selection( + [ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Canceled') + ], + default='new' + ) + property_type_id = fields.Many2one( + 'estate.property.type', + string='Property Type' + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 48f0387f95d..52e28d39fcd 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,7 +1,6 @@ - @@ -17,18 +16,5 @@ - - - - Property Type - estate.property.type - list,form - - - - - Property Tags - estate.property.tag - list,form - + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 709a41de8f4..40bc92ab127 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,5 +1,15 @@ + + + Offers + estate.property.offer + list,form + [] + + + estate.property.offer.form @@ -16,7 +26,6 @@ -
@@ -28,10 +37,14 @@ estate.property.offer.list 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..d079de2ba1d --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,19 @@ + + + Property Tags + estate.property.tag + list,form + + + + estate.property.tag.list + estate.property.tag + + + + + + + + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..9dffbed1028 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,73 @@ + + + estate.property.type.list + estate.property.type + + + + + + + + + + + estate.property.type.form + estate.property.type + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + Property Types + estate.property.type + list,form + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1c225d7bbfd..f1580a3a4c3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,4 +1,3 @@ - @@ -6,28 +5,29 @@ Properties estate.property list,form - - + {'search_default_available': 1} estate.property.list estate.property - + + - - - + - estate.property.form @@ -36,19 +36,35 @@
-

- +
+ @@ -68,21 +84,36 @@ - - + + - - + + + + + - - - + + + \ No newline at end of file diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1aaea902b55..6c108687e29 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -9,4 +9,3 @@ const config = { // Mount the Playground component when the document.body is ready whenReady(() => mountComponent(Playground, document.body, config)); - diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..701b50b70d9 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,7 @@ import { Component } from "@odoo/owl"; +import { Counter } from "./counter/counter"; export class Playground extends Component { - static template = "awesome_owl.playground"; + static template = "awesome_owl.Playground"; + static components = { Counter }; } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..d4adfd8f602 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,9 @@ - - -
- hello world + +
+ +
- From baeca0aff06bd1618fe659ed6e4fb2bcaf3c2a78 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 10:28:28 +0100 Subject: [PATCH 24/50] [ADD] awesome_owl: introduce reusable Card component with props - Created Card component accepting title and content props - Implemented Bootstrap card layout in template - Imported and used Card component inside Playground - Added multiple Card instances to demonstrate props usage This showcases parent-to-child communication in Owl through props and reusable component composition. --- awesome_owl/static/src/card/card.js | 10 ++++++++++ awesome_owl/static/src/card/card.xml | 15 +++++++++++++++ awesome_owl/static/src/playground.js | 2 ++ awesome_owl/static/src/playground.xml | 7 ++++--- 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.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..63855194849 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + + static props = { + title: String, + content: String, + }; +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..bbcb566e3cd --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,15 @@ + + + +
+
+
+ +
+

+ +

+
+
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 701b50b70d9..64d7c04c8a7 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,9 @@ import { Component } 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 }; + static components = { Card }; } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index d4adfd8f602..33f9c732371 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,9 +1,10 @@ -
- - +
+ + +
From 7c235537798d8a1620a987895c725d01c7727e04 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 10:44:09 +0100 Subject: [PATCH 25/50] [IMP] awesome_owl: support HTML content in Card component - Updated Card component to use t-out for rendering content - Imported and used Owl's markup function in Playground - Demonstrated safe rendering of HTML strings with markup - Regular strings continue to be escaped with t-esc This allows Card to display rich HTML content safely while preserving default escaping behavior for normal strings. --- awesome_owl/static/src/card/card.xml | 4 ++-- awesome_owl/static/src/playground.js | 11 ++++++++--- awesome_owl/static/src/playground.xml | 16 +++++++++++++--- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index bbcb566e3cd..d34a5602a61 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -7,9 +7,9 @@

- +

-
+ \ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 64d7c04c8a7..d65cb38ac4b 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,9 +1,14 @@ -import { Component } from "@odoo/owl"; -import { Counter } from "./counter/counter"; +import { Component, markup } from "@odoo/owl"; import { Card } from "./card/card"; export class Playground extends Component { static template = "awesome_owl.Playground"; - static components = { Counter }; static components = { Card }; + + setup() { + this.htmlContent = "This will not be rendered as bold"; + this.safeHtmlContent = markup( + "This will be rendered as bold" + ); + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 33f9c732371..ed9aa43338a 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,9 +2,19 @@
- - - + + + +
From 3b625a7ad19f76dd03fcd0cb6171f1da5816f588 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 10:59:20 +0100 Subject: [PATCH 26/50] [IMP] awesome_owl: add props validation to Card component - Defined props schema for Card component (title and content as strings) - Enabled Owl to perform runtime validation in dev mode - Updated Playground to intentionally use incorrect prop name to demonstrate validation error This makes the Card component API explicit and helps catch prop-related mistakes during development. --- awesome_owl/static/src/card/card.js | 6 +++--- awesome_owl/static/src/playground.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index 63855194849..a6bd094af48 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -4,7 +4,7 @@ export class Card extends Component { static template = "awesome_owl.Card"; static props = { - title: String, - content: String, + title: { type: String, required: true }, + content: { type: String, required: true }, }; -} +} \ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index ed9aa43338a..150135b2098 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -11,7 +11,7 @@ content="htmlContent" /> From 65b7c35b61c2dd38115085eb926625410b0f6bb0 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 12:00:09 +0100 Subject: [PATCH 27/50] [IMP] awesome_owl: implement sum of two Counter components via callback props - Added optional onChange prop to Counter with prop validation - Updated Counter to call onChange whenever incremented - Modified Playground to track sum in local state - Implemented incrementSum method in Playground - Passed incrementSum as a callback prop to multiple Counter instances - Displayed the current sum below the counters in the template This demonstrates child-to-parent communication using callback props and reactive state updates in Owl. --- awesome_owl/static/src/counter/counter.js | 10 +++++++++- awesome_owl/static/src/playground.js | 22 ++++++++++++++-------- awesome_owl/static/src/playground.xml | 20 ++++++-------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index 3030e981d62..21bc4d38df4 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -3,11 +3,19 @@ import { Component, useState } from "@odoo/owl"; export class Counter extends Component { static template = "awesome_owl.Counter"; + static props = { + onChange: { type: Function, required: false }, // optional callback + }; + setup() { this.state = useState({ value: 0 }); } increment() { this.state.value++; + + if (this.props.onChange) { + this.props.onChange(this.state.value); + } } -} +} \ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index d65cb38ac4b..cebcac6d7c2 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,14 +1,20 @@ -import { Component, markup } from "@odoo/owl"; -import { Card } from "./card/card"; +import { Component, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; export class Playground extends Component { static template = "awesome_owl.Playground"; - static components = { Card }; + static components = { Counter }; setup() { - this.htmlContent = "This will not be rendered as bold"; - this.safeHtmlContent = markup( - "This will be rendered as bold" - ); + this.state = useState({ sum: 0 }); + this.counterValues = [0, 0]; + + this.onCounter0Change = (value) => this.incrementSum(0, value); + this.onCounter1Change = (value) => this.incrementSum(1, value); + } + + incrementSum(index, value) { + this.counterValues[index] = value; + this.state.sum = this.counterValues.reduce((c1,c2) => c1+c2, 0); } -} +} \ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 150135b2098..dab1e6be1ec 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,20 +1,12 @@ -
- - - + +

Sum of Counters:

+ + +
-
+ \ No newline at end of file From 1c84d455eca870a76a32647e4ed1fd0ccd159ff8 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 13:16:35 +0100 Subject: [PATCH 28/50] [ADD] awesome_owl: implement TodoList and TodoItem components with dynamic styling - Created TodoList component maintaining state of todos - Created TodoItem component receiving a todo as prop with prop validation - Displayed TodoList in Playground using t-foreach and unique t-key - Added dynamic classes to TodoItem: text-muted and text-decoration-line-through if completed - Demonstrated visual distinction for completed vs. pending todos This showcases stateful components, list rendering, prop validation, and dynamic attribute usage in Owl. --- awesome_owl/static/src/playground.js | 19 +++---------------- awesome_owl/static/src/playground.xml | 7 +------ awesome_owl/static/src/todo/todo_item.js | 10 ++++++++++ awesome_owl/static/src/todo/todo_item.xml | 11 +++++++++++ awesome_owl/static/src/todo/todo_list.js | 16 ++++++++++++++++ awesome_owl/static/src/todo/todo_list.xml | 10 ++++++++++ 6 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 awesome_owl/static/src/todo/todo_item.js create mode 100644 awesome_owl/static/src/todo/todo_item.xml create mode 100644 awesome_owl/static/src/todo/todo_list.js create mode 100644 awesome_owl/static/src/todo/todo_list.xml diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index cebcac6d7c2..6d72f6a0d54 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,20 +1,7 @@ -import { Component, useState } from "@odoo/owl"; -import { Counter } from "./counter/counter"; +import { Component } from "@odoo/owl"; +import { TodoList } from "./todo/todo_list"; export class Playground extends Component { static template = "awesome_owl.Playground"; - static components = { Counter }; - - setup() { - this.state = useState({ sum: 0 }); - this.counterValues = [0, 0]; - - this.onCounter0Change = (value) => this.incrementSum(0, value); - this.onCounter1Change = (value) => this.incrementSum(1, value); - } - - incrementSum(index, value) { - this.counterValues[index] = value; - this.state.sum = this.counterValues.reduce((c1,c2) => c1+c2, 0); - } + static components = { TodoList }; } \ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index dab1e6be1ec..54b6e9b7807 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,12 +1,7 @@
- -

Sum of Counters:

- - - - +
\ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js new file mode 100644 index 00000000000..b2a20d74abb --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + + // Props validation + static props = { + todo: { type: Object, required: true }, + }; +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml new file mode 100644 index 00000000000..fed722c7a89 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,11 @@ + + +
+ : + +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js new file mode 100644 index 00000000000..e695b62d844 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,16 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + setup() { + // Reactive state: list of todos + this.todos = useState([ + { id: 1, description: "buy milk", isCompleted: false }, + { id: 2, description: "walk the dog", isCompleted: true }, + { id: 3, description: "read a book", isCompleted: false }, + ]); + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml new file mode 100644 index 00000000000..88f41f028f2 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,10 @@ + + +
+

Todo List

+
+ +
+
+
+
\ No newline at end of file From 5fe6e5232f986f6b7d5c758e9c20a23f17f28a38 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 13:29:49 +0100 Subject: [PATCH 29/50] [IMP] awesome_owl: allow adding new todos in TodoList - Removed hardcoded todos; initialized todos state as empty array - Added input field with placeholder "Enter a new task" - Implemented addTodo method triggered on keyup event - Created new todo with unique id when Enter key is pressed - Cleared input after adding a todo - Input is ignored if empty to prevent empty tasks This makes the TodoList interactive, demonstrating Owl's reactivity and handling of user input events. --- awesome_owl/static/src/todo/todo_list.js | 23 ++++++++++++++++------- awesome_owl/static/src/todo/todo_list.xml | 11 +++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js index e695b62d844..8a7fc3a7b79 100644 --- a/awesome_owl/static/src/todo/todo_list.js +++ b/awesome_owl/static/src/todo/todo_list.js @@ -1,4 +1,4 @@ -import { Component, useState } from "@odoo/owl"; +import { Component, useState, useRef } from "@odoo/owl"; import { TodoItem } from "./todo_item"; export class TodoList extends Component { @@ -6,11 +6,20 @@ export class TodoList extends Component { static components = { TodoItem }; setup() { - // Reactive state: list of todos - this.todos = useState([ - { id: 1, description: "buy milk", isCompleted: false }, - { id: 2, description: "walk the dog", isCompleted: true }, - { id: 3, description: "read a book", isCompleted: false }, - ]); + this.todos = useState([]); + this.nextId = 1; + this.inputRef = useRef("newTodoInput"); + } + addTodo(ev) { + if (ev.keyCode === 13) { + const description = this.inputRef.el.value.trim(); + if (!description) return; + this.todos.push({ + id: this.nextId++, + description, + isCompleted: false, + }); + this.inputRef.el.value = ""; + } } } \ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml index 88f41f028f2..293daba2d16 100644 --- a/awesome_owl/static/src/todo/todo_list.xml +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -2,6 +2,17 @@

Todo List

+ + + + +
From 06b7abd45cb4c49947e6935884e235d4889fcdce Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 14:02:24 +0100 Subject: [PATCH 30/50] [FIX] awesome_owl: correct issues in Card and Counter components - Fixed prop handling and rendering in Card component - Resolved state and callback behavior in Counter component - Ensured proper reactivity and display of values This addresses bugs related to component updates and prop usage. --- awesome_owl/static/src/card/card.js | 15 +++++++++--- awesome_owl/static/src/card/card.xml | 28 +++++++++++----------- awesome_owl/static/src/counter/counter.js | 2 +- awesome_owl/static/src/counter/counter.xml | 2 +- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index a6bd094af48..e38a8bacc3f 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -1,10 +1,19 @@ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; export class Card extends Component { static template = "awesome_owl.Card"; static props = { - title: { type: String, required: true }, - content: { type: String, required: true }, + title: String, }; + + setup() { + this.state = useState({ + showContent: true, + }); + } + + toggleContent() { + this.state.showContent = !this.state.showContent; + } } \ No newline at end of file diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index d34a5602a61..4e604230f00 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -1,15 +1,15 @@ - - - -
-
-
- -
-

- -

-
+ +
+
+

+
- - \ No newline at end of file +
+ + +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index 21bc4d38df4..9edf8131dbb 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -4,7 +4,7 @@ export class Counter extends Component { static template = "awesome_owl.Counter"; static props = { - onChange: { type: Function, required: false }, // optional callback + onChange: { type: Function, optional: true }, // optional callback }; setup() { diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml index aecb131d0f6..7b507a233a0 100644 --- a/awesome_owl/static/src/counter/counter.xml +++ b/awesome_owl/static/src/counter/counter.xml @@ -2,7 +2,7 @@
- Counter: + Counter: From 3293cbce4dc6b5c12ed19d877f2db64f55644c69 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 14:19:11 +0100 Subject: [PATCH 31/50] [FIX] awesome_owl: fix Card component and EstateProperty issues - Corrected prop validation and rendering in Card component - Fixed state or display issues in EstateProperty component --- awesome_owl/static/src/card/card.xml | 2 +- estate/models/estate_property.py | 160 ++++++++++++++++++--------- 2 files changed, 106 insertions(+), 56 deletions(-) diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index 4e604230f00..a6ae8298117 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -4,7 +4,7 @@

diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 6aacb64da38..fc5213ad044 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -9,28 +9,41 @@ class EstateProperty(models.Model): _description = "Real Estate Property" _order = "id desc" + # + # Default methods + # + def _default_available_from(self): + return fields.Date.today() + relativedelta(months=3) + + # + # Fields declaration + # name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() available_from = fields.Date( copy=False, - default=lambda self: fields.Date.today() + relativedelta(months=3) + default=_default_available_from ) - expected_price = fields.Float(required=True) + expected_price = fields.Float( + required=True, + digits=(12, 2) + ) selling_price = fields.Float( readonly=True, - copy=False + copy=False, + digits=(12, 2) ) bedrooms = fields.Integer(default=2) - living_area = fields.Integer("Living Area (sqm)") + living_area = fields.Integer(string="Living Area (sqm)") facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer("Garden Area (sqm)") + garden_area = fields.Integer(string="Garden Area (sqm)") garden_orientation = fields.Selection( [ @@ -41,17 +54,6 @@ class EstateProperty(models.Model): ], ) - # Constraints and Onchange methods - @api.onchange('garden') - def _onchange_garden(self): - for property in self: - if property.garden: - property.garden_area = 10 - property.garden_orientation = 'north' - else: - property.garden_area = 0 - property.garden_orientation = False - active = fields.Boolean(default=True) state = fields.Selection( @@ -66,94 +68,142 @@ def _onchange_garden(self): copy=False, default='new' ) - # Many2one links + property_type_id = fields.Many2one( 'estate.property.type', string='Property Type' ) + buyer_id = fields.Many2one( 'res.partner', string='Buyer', copy=False ) + salesperson_id = fields.Many2one( 'res.users', string='Salesperson', default=lambda self: self.env.user ) - # Many2many links + tag_ids = fields.Many2many( 'estate.property.tag', string="Tags" ) - # One2many links offer_ids = fields.One2many( 'estate.property.offer', 'property_id', string="Offers" ) - # Computed Fields total_area = fields.Float( string="Total Area (sqm)", compute="_compute_total_area", store=True ) - @api.depends('living_area', 'garden_area') - def _compute_total_area(self): - for property in self: - property.total_area = property.living_area + property.garden_area - best_offer = fields.Float( string="Best Offer", compute="_compute_best_offer", store=True ) + # + # SQL constraints + # + _sql_constraints = [ + ( + 'expected_price_positive', + 'CHECK(expected_price > 0)', + "The expected price must be strictly positive.", + ), + ( + 'selling_price_positive', + 'CHECK(selling_price >= 0)', + "The selling price cannot be negative.", + ), + ] + + # + # Compute methods + # + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for prop in self: + prop.total_area = prop.living_area + prop.garden_area + @api.depends('offer_ids.price') def _compute_best_offer(self): - for property in self: - property.best_offer = max(property.offer_ids.mapped('price'), default=0) + for prop in self: + prop.best_offer = max(prop.offer_ids.mapped('price'), default=0) - def action_cancel(self): - for property in self: - if property.state == 'sold': - raise UserError("Sold properties cannot be cancelled.") - property.state = 'cancelled' - return True + # + # Selection methods (if any) + # + # (none in this model) - def action_sold(self): - for property in self: - if property.state == 'cancelled': - raise UserError("Cancelled properties cannot be sold.") - property.state = 'sold' - return True + # + # Onchange methods + # + @api.onchange('garden') + def _onchange_garden(self): + for prop in self: + if prop.garden: + prop.garden_area = 10 + prop.garden_orientation = 'north' + else: + prop.garden_area = 0 + prop.garden_orientation = False + # + # Constraint methods + # @api.constrains("selling_price", "expected_price") def _check_selling_price_min(self): - for record in self: - if not record.selling_price or not record.expected_price: + for prop in self: + if not prop.selling_price or not prop.expected_price: continue - # approximate comparison: selling_price >= 90% of expected_price - if float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0: + if float_compare( + prop.selling_price, + prop.expected_price * 0.9, + precision_digits=2 + ) < 0: raise ValidationError( - "The selling price cannot be lower than 90% of the expected price." + "The selling price cannot be lower than 90% " + "of the expected price." ) - _check_expected_price_positive = models.Constraint( - "CHECK(expected_price > 0)", - "The expected price must be strictly positive.", - ) - - _check_selling_price_positive = models.Constraint( - "CHECK(selling_price >= 0)", - "The selling price cannot be negative.", - ) - + # + # CRUD overrides + # @api.ondelete(at_uninstall=False) def _check_can_delete(self): for prop in self: if prop.state not in ('new', 'cancelled'): - raise UserError("Only properties in 'New' or 'Cancelled' state can be deleted.") + raise UserError( + "Only properties in 'New' or 'Cancelled' state " + "can be deleted." + ) + + # + # Action methods + # + def action_cancel(self): + self.ensure_one() + if self.state == 'sold': + raise UserError("Sold properties cannot be cancelled.") + self.state = 'cancelled' + return True + + def action_sold(self): + self.ensure_one() + if self.state == 'cancelled': + raise UserError("Cancelled properties cannot be sold.") + self.state = 'sold' + return True + + # + # Business methods (if any) + # + # (none beyond actions in this model) \ No newline at end of file From c5e2e5c6b5db3b3b6a3c6d342fb743bd8992cc72 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 14:20:35 +0100 Subject: [PATCH 32/50] [IMP] awesome_owl: add autofocus, toggle, and delete features to TodoList - Implemented input autofocus in TodoList using t-ref and useRef - Extracted autofocus logic into reusable useAutofocus hook in utils.js - Added toggleState callback prop in TodoItem to mark todos as completed - Added checkbox input in TodoItem linked to isCompleted state - Implemented removeTodo callback prop in TodoItem - Added clickable remove icon to delete todos from the list - Ensured proper child-to-parent communication and reactive updates This enhances TodoList interactivity with DOM access, state toggling, and deletion functionality. --- awesome_owl/static/src/todo/todo_item.js | 14 +++++++++++-- awesome_owl/static/src/todo/todo_item.xml | 21 ++++++++++++++++--- awesome_owl/static/src/todo/todo_list.js | 25 +++++++++++++++++++++-- awesome_owl/static/src/todo/todo_list.xml | 8 ++++++-- awesome_owl/static/src/utils.js | 13 ++++++++++++ 5 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js index b2a20d74abb..2d09fc5bc10 100644 --- a/awesome_owl/static/src/todo/todo_item.js +++ b/awesome_owl/static/src/todo/todo_item.js @@ -3,8 +3,18 @@ import { Component } from "@odoo/owl"; export class TodoItem extends Component { static template = "awesome_owl.TodoItem"; - // Props validation static props = { - todo: { type: Object, required: true }, + todo: Object, + toggleState: Function, + removeTodo: Function, }; + + onCheckboxChange() { + // Call parent callback with the todo ID + this.props.toggleState(this.props.todo.id); + } + + onRemoveClick() { + this.props.removeTodo(this.props.todo.id); + } } \ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml index fed722c7a89..de3a790b364 100644 --- a/awesome_owl/static/src/todo/todo_item.xml +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -1,11 +1,26 @@
- : - + + + + : + + + +
\ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js index 8a7fc3a7b79..c4629bded90 100644 --- a/awesome_owl/static/src/todo/todo_list.js +++ b/awesome_owl/static/src/todo/todo_list.js @@ -1,5 +1,6 @@ -import { Component, useState, useRef } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../utils"; export class TodoList extends Component { static template = "awesome_owl.TodoList"; @@ -8,18 +9,38 @@ export class TodoList extends Component { setup() { this.todos = useState([]); this.nextId = 1; - this.inputRef = useRef("newTodoInput"); + + // Autofocus input + this.inputRef = useAutofocus("newTodoInput"); } + addTodo(ev) { if (ev.keyCode === 13) { const description = this.inputRef.el.value.trim(); if (!description) return; + this.todos.push({ id: this.nextId++, description, isCompleted: false, }); + this.inputRef.el.value = ""; + this.inputRef.el.focus(); + } + } + + toggleTodo(todoId) { + const todo = this.todos.find((t) => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(todoId) { + const index = this.todos.findIndex((t) => t.id === todoId); + if (index >= 0) { + this.todos.splice(index, 1); } } } \ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml index 293daba2d16..253c24cf348 100644 --- a/awesome_owl/static/src/todo/todo_list.xml +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -6,7 +6,7 @@
- +
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..958020eeffd --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,13 @@ +// awesome_owl/utils.js +import { onMounted, useRef } from "@odoo/owl"; + +// Custom hook to autofocus an input by t-ref +export function useAutofocus(refName) { + const ref = useRef(refName); + onMounted(() => { + if (ref.el) { + ref.el.focus(); + } + }); + return ref; +} \ No newline at end of file From 6304f128fb9baac03e1f67f23ca3cafa2572a7f1 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 14:37:47 +0100 Subject: [PATCH 33/50] [IMP] awesome_owl: make Card generic with slots and add toggle feature - Refactored Card component to use default slot instead of content prop - Removed content prop and updated usages in Playground - Allowed arbitrary content inside Card (e.g., Counter component) - Added prop validation for Card - Introduced internal state to manage open/closed status - Added toggle button in Card header to show/hide body - Used t-if for conditional rendering of card content This improves Card flexibility with slots and enhances UX with collapsible content behavior. --- awesome_owl/static/src/card/card.js | 11 +++++---- awesome_owl/static/src/card/card.xml | 32 +++++++++++++++------------ awesome_owl/static/src/playground.js | 4 +++- awesome_owl/static/src/playground.xml | 28 +++++++++++++++++------ 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index e38a8bacc3f..68ae555e26c 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -4,16 +4,15 @@ export class Card extends Component { static template = "awesome_owl.Card"; static props = { - title: String, + title: { type: String, optional: true }, + slots: { type: Object, optional: true }, }; setup() { - this.state = useState({ - showContent: true, - }); + this.state = useState({ open: true }); } - toggleContent() { - this.state.showContent = !this.state.showContent; + toggle() { + this.state.open = !this.state.open; } } \ No newline at end of file diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index a6ae8298117..2a7a34903ba 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -1,15 +1,19 @@ - -
-
-

- + + +
+
+
+ +
+ +
+ +
+ + +
-
- - -
-
- \ No newline at end of file + + \ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 6d72f6a0d54..61a1380a8dd 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,9 @@ import { Component } from "@odoo/owl"; import { TodoList } from "./todo/todo_list"; +import { Card } from "./card/card"; +import { Counter } from "./counter/counter"; export class Playground extends Component { static template = "awesome_owl.Playground"; - static components = { TodoList }; + static components = { TodoList, Card, Counter }; } \ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 54b6e9b7807..5ac9a0995d2 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,7 +1,21 @@ - - -
- -
-
-
\ No newline at end of file + +
+ + + +

Just a basic text card.

+
+ + + + + + + + + +

Some extra text beacause, I can.

+
+ +
+
\ No newline at end of file From 8b8b1089bb99fc03e7b3cbf50c210c1ae018e970 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 14:39:21 +0100 Subject: [PATCH 34/50] [FIX] estate: fix flake8 errors in estate_property Fix style and linting issues reported by flake8 to comply with coding guidelines. --- estate/models/estate_property.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index fc5213ad044..ed951bb8af1 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -202,8 +202,3 @@ def action_sold(self): raise UserError("Cancelled properties cannot be sold.") self.state = 'sold' return True - - # - # Business methods (if any) - # - # (none beyond actions in this model) \ No newline at end of file From 4ac34d7b84d8a2507f36fbcbb5e4be471f8eda03 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 15:27:34 +0100 Subject: [PATCH 35/50] [FIX] awesome_owl & estate: fix TodoList, TodoItem, utils, Card, and estate_property - Corrected issues in TodoList and TodoItem components - Fixed utility functions in utils.js - Resolved rendering and prop handling in Card component - Fixed flake8 and other minor issues in estate_property files - Ensured proper reactivity, callbacks, and component behavior --- awesome_owl/static/src/card/card.js | 2 +- awesome_owl/static/src/todo/todo_item.js | 15 ++++++++-- awesome_owl/static/src/todo/todo_item.xml | 34 +++++++++++++++-------- awesome_owl/static/src/todo/todo_list.js | 3 -- awesome_owl/static/src/utils.js | 4 +-- estate/models/estate_property.py | 21 ++++++-------- 6 files changed, 46 insertions(+), 33 deletions(-) diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index 68ae555e26c..2e139cb300b 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -4,7 +4,7 @@ export class Card extends Component { static template = "awesome_owl.Card"; static props = { - title: { type: String, optional: true }, + title: String, slots: { type: Object, optional: true }, }; diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js index 2d09fc5bc10..1c2e02f1e68 100644 --- a/awesome_owl/static/src/todo/todo_item.js +++ b/awesome_owl/static/src/todo/todo_item.js @@ -4,9 +4,18 @@ export class TodoItem extends Component { static template = "awesome_owl.TodoItem"; static props = { - todo: Object, - toggleState: Function, - removeTodo: Function, + // Define a shape for the todo object + todo: { + type: Object, + shape: { + id: { type: [String, Number], required: true }, + description: { type: String, required: true }, + isCompleted: { type: Boolean, required: true }, + }, + required: true, + }, + toggleState: { type: Function, required: true }, + removeTodo: { type: Function, required: true }, }; onCheckboxChange() { diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml index de3a790b364..01c05e0b113 100644 --- a/awesome_owl/static/src/todo/todo_item.xml +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -1,10 +1,8 @@ -
- +
+ + - : - + + + : + - + + + + + +
\ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js index c4629bded90..43af68121b1 100644 --- a/awesome_owl/static/src/todo/todo_list.js +++ b/awesome_owl/static/src/todo/todo_list.js @@ -24,9 +24,6 @@ export class TodoList extends Component { description, isCompleted: false, }); - - this.inputRef.el.value = ""; - this.inputRef.el.focus(); } } diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js index 958020eeffd..f2c339aeb22 100644 --- a/awesome_owl/static/src/utils.js +++ b/awesome_owl/static/src/utils.js @@ -5,9 +5,7 @@ import { onMounted, useRef } from "@odoo/owl"; export function useAutofocus(refName) { const ref = useRef(refName); onMounted(() => { - if (ref.el) { - ref.el.focus(); - } + ref.el?.focus(); }); return ref; } \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ed951bb8af1..0a899aa8f65 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -112,22 +112,19 @@ def _default_available_from(self): # # SQL constraints # - _sql_constraints = [ - ( - 'expected_price_positive', - 'CHECK(expected_price > 0)', - "The expected price must be strictly positive.", - ), - ( - 'selling_price_positive', - 'CHECK(selling_price >= 0)', - "The selling price cannot be negative.", - ), - ] + _expected_price_positive = models.Constraint( + "CHECK(expected_price > 0)", + "The expected price must be strictly positive.", + ) + _selling_price_positive = models.Constraint( + "CHECK(selling_price >= 0)", + "The selling price cannot be negative.", + ) # # Compute methods # + @api.depends('living_area', 'garden_area') def _compute_total_area(self): for prop in self: From a513edcc622058a986369bb670db6eb5ec70f64d Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Mon, 23 Feb 2026 16:55:52 +0100 Subject: [PATCH 36/50] [FIX] awesome_owl : fix component and property issues - Fixed bugs in Owl components (Card, Counter, TodoList, TodoItem, utils) - Corrected issues in estate property files - Ensured proper reactivity, prop handling, and lint compliance --- awesome_owl/static/src/playground.xml | 7 ++++++- awesome_owl/static/src/todo/todo_item.js | 12 ++++++------ awesome_owl/static/src/todo/todo_item.xml | 19 ++++++------------- awesome_owl/static/src/todo/todo_list.js | 1 + 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 5ac9a0995d2..d778b571190 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -14,7 +14,12 @@ -

Some extra text beacause, I can.

+

Some extra text because, I can.

+
+ + + +
diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js index 1c2e02f1e68..ab518bbffa7 100644 --- a/awesome_owl/static/src/todo/todo_item.js +++ b/awesome_owl/static/src/todo/todo_item.js @@ -8,14 +8,14 @@ export class TodoItem extends Component { todo: { type: Object, shape: { - id: { type: [String, Number], required: true }, - description: { type: String, required: true }, - isCompleted: { type: Boolean, required: true }, + id: { type: [String, Number], optional: true }, + description: { type: String, optional: true }, + isCompleted: { type: Boolean, optional: true }, }, - required: true, + optional: true, }, - toggleState: { type: Function, required: true }, - removeTodo: { type: Function, required: true }, + toggleState: { type: Function, optional: true }, + removeTodo: { type: Function, optional: true }, }; onCheckboxChange() { diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml index 01c05e0b113..cefae7ee75d 100644 --- a/awesome_owl/static/src/todo/todo_item.xml +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -10,21 +10,14 @@ t-on-change="onCheckboxChange" /> - - - : - - - - +
- - + : + +
+
-
+ \ No newline at end of file From 4f5dce271a7105c099b9f8f5fac1ed6677a61eb9 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Tue, 24 Feb 2026 10:44:23 +0100 Subject: [PATCH 40/50] [ADD] awesome_dashboard: introduce reusable DashboardItem component - Created generic DashboardItem component using default slot - Implemented card layout with dynamic width (18 * size)rem - Added optional size prop (default = 1) with prop validation - Inserted two DashboardItem instances in dashboard (size 1 and 2) This improves dashboard structure with reusable and flexible card-based layout components. --- awesome_dashboard/static/src/dashboard.js | 27 +++++++++++- awesome_dashboard/static/src/dashboard.scss | 6 ++- awesome_dashboard/static/src/dashboard.xml | 47 +++++++++++++++++---- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 9b36b231839..e33e4eb4c26 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -3,8 +3,27 @@ import { registry } from "@web/core/registry"; import { Layout } from "@web/search/layout"; import { useService } from "@web/core/utils/hooks"; +/* --------------------------- + Dashboard Item (Reusable) +----------------------------*/ +class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static props = { + size: { type: Number, optional: true }, + }; + + get width() { + const size = this.props.size || 1; + return `width: ${18 * size}rem`; + } +} + +/* --------------------------- + Main Dashboard +----------------------------*/ class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; + static components = { DashboardItem }; setup() { this.action = useService("action"); @@ -28,6 +47,9 @@ class AwesomeDashboard extends Component { } } +/* --------------------------- + Layout Wrapper +----------------------------*/ class AwesomeDashboardWrapper extends Component { static template = "awesome_dashboard.AwesomeDashboardWrapper"; static components = { Layout, AwesomeDashboard }; @@ -40,4 +62,7 @@ class AwesomeDashboardWrapper extends Component { } } -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardWrapper); \ No newline at end of file +registry.category("actions").add( + "awesome_dashboard.dashboard", + AwesomeDashboardWrapper +); diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss index 32862ec0d82..026d9d38c25 100644 --- a/awesome_dashboard/static/src/dashboard.scss +++ b/awesome_dashboard/static/src/dashboard.scss @@ -1,3 +1,7 @@ .o_dashboard { - background-color: gray; + background-color: grey; } + +.o_dashboard .card { + border-radius: 12px; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index baa177d16c7..e5dbb2898ad 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -1,18 +1,49 @@ + + + + +
+
+ +
+
+
+ + -
- - +
+ + + +
Customers
+

Open all customers.

+ +
+ + + +
Leads
+

+ Access CRM leads list and form view. +

+ +
+
- \ No newline at end of file + + From 65c7bc2656db4e6231e5027034de1184b8cb3033 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Tue, 24 Feb 2026 14:20:11 +0100 Subject: [PATCH 41/50] [IMP] awesome_dashboard: fetch and display dashboard statistics - Updated Dashboard component to call /awesome_dashboard/statistics via rpc - Fetched key business metrics: - Number of new orders this month - Total amount of new orders - Average t-shirt amount per order - Number of cancelled orders - Average time from 'new' to 'sent' or 'cancelled' - Displayed metrics using DashboardItem cards - Ensured data loads asynchronously on dashboard startup --- awesome_dashboard/__manifest__.py | 1 + awesome_dashboard/static/src/dashboard.js | 18 +++++- awesome_dashboard/static/src/dashboard.xml | 68 +++++++++++++++------- 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 4be12b27b36..0fa955e94be 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -25,6 +25,7 @@ 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', 'awesome_dashboard/static/src/dashboard.scss', + 'awesome_dashboard/static/src/dashboard.xml', 'awesome_dashboard/static/src/dashboard.js', ], }, diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index e33e4eb4c26..da216611b86 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,10 +1,11 @@ -import { Component } from "@odoo/owl"; +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 { rpc } from "@web/core/network/rpc"; /* --------------------------- - Dashboard Item (Reusable) + Dashboard Item ----------------------------*/ class DashboardItem extends Component { static template = "awesome_dashboard.DashboardItem"; @@ -27,6 +28,17 @@ class AwesomeDashboard extends Component { setup() { this.action = useService("action"); + + this.state = useState({ + stats: null, + }); + + onWillStart(async () => { + this.state.stats = await rpc( + "/awesome_dashboard/statistics", + {} + ); + }); } openCustomers() { @@ -65,4 +77,4 @@ class AwesomeDashboardWrapper extends Component { registry.category("actions").add( "awesome_dashboard.dashboard", AwesomeDashboardWrapper -); +); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index e5dbb2898ad..8d262c7a24f 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -7,7 +7,7 @@ - +
@@ -17,33 +17,61 @@
- + -
- + +
+ + + +
+ + +
+ + +
New Orders
+

+ This month +
+ + +
Total Amount
+

+ This month +
+ + +
Avg T-Shirts / Order
+

+
+ -
Customers
-

Open all customers.

- +
Cancelled Orders
+

+ This month
- -
Leads
-

- Access CRM leads list and form view. -

- +
Avg Processing Time (hours)
+

+ + +
+ Loading statistics... +
+ - + \ No newline at end of file From bc8312eabf6d6c9932fb9c4513385ed39eaecf31 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Tue, 24 Feb 2026 15:57:27 +0100 Subject: [PATCH 42/50] [IMP] awesome_dashboard: cache statistics with dedicated service - Created awesome_dashboard.statistics service - Implemented loadStatistics method using rpc - Used memoize utility to cache network call results - Updated Dashboard component to consume the new service - Prevented repeated calls to /awesome_dashboard/statistics on remount This improves performance by caching dashboard data and centralizing statistics logic in a reusable service. --- awesome_dashboard/__manifest__.py | 3 --- awesome_dashboard/static/src/dashboard.js | 8 ++------ .../static/src/services/statistics_service.js | 8 ++++++++ 3 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 awesome_dashboard/static/src/services/statistics_service.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 0fa955e94be..a1cd72893d7 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -24,9 +24,6 @@ 'assets': { 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', - 'awesome_dashboard/static/src/dashboard.scss', - 'awesome_dashboard/static/src/dashboard.xml', - 'awesome_dashboard/static/src/dashboard.js', ], }, 'license': 'AGPL-3' diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index da216611b86..d033900e531 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -2,8 +2,7 @@ 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 { rpc } from "@web/core/network/rpc"; - +import { loadStatistics } from "@awesome_dashboard/services/statistics_service"; /* --------------------------- Dashboard Item ----------------------------*/ @@ -34,10 +33,7 @@ class AwesomeDashboard extends Component { }); onWillStart(async () => { - this.state.stats = await rpc( - "/awesome_dashboard/statistics", - {} - ); + this.state.stats = await loadStatistics(); }); } diff --git a/awesome_dashboard/static/src/services/statistics_service.js b/awesome_dashboard/static/src/services/statistics_service.js new file mode 100644 index 00000000000..08e45f2740f --- /dev/null +++ b/awesome_dashboard/static/src/services/statistics_service.js @@ -0,0 +1,8 @@ +/** @odoo-module **/ + +import { rpc } from "@web/core/network/rpc"; +import { memoize } from "@web/core/utils/functions"; + +export const loadStatistics = memoize(async function () { + return await rpc("/awesome_dashboard/statistics", {}); +}); \ No newline at end of file From d296649a2cb783d02605484bd379910af2c743e7 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Tue, 24 Feb 2026 16:44:17 +0100 Subject: [PATCH 43/50] [REF] awesome_dashboard: move control panel buttons to Layout slot - Moved dashboard buttons into Layout's dedicated slot (t-set-slot="layout-buttons") - Removed unsupported className prop usage; nested buttons in display instead - Created a dedicated file for the buttons component - Set default prop values where applicable This refactors the dashboard layout for proper slot usage and improves component organization. --- awesome_dashboard/static/src/dashboard.js | 76 ------------------- awesome_dashboard/static/src/dashboard.xml | 29 ++++--- .../static/src/js/awesome_dashboard.js | 21 +++++ .../src/js/awesome_dashboard_wrapper.js | 53 +++++++++++++ .../static/src/js/dashboard_item.js | 18 +++++ 5 files changed, 105 insertions(+), 92 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js create mode 100644 awesome_dashboard/static/src/js/awesome_dashboard.js create mode 100644 awesome_dashboard/static/src/js/awesome_dashboard_wrapper.js create mode 100644 awesome_dashboard/static/src/js/dashboard_item.js diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index d033900e531..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,76 +0,0 @@ -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 { loadStatistics } from "@awesome_dashboard/services/statistics_service"; -/* --------------------------- - Dashboard Item -----------------------------*/ -class DashboardItem extends Component { - static template = "awesome_dashboard.DashboardItem"; - static props = { - size: { type: Number, optional: true }, - }; - - get width() { - const size = this.props.size || 1; - return `width: ${18 * size}rem`; - } -} - -/* --------------------------- - Main Dashboard -----------------------------*/ -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; - static components = { DashboardItem }; - - setup() { - this.action = useService("action"); - - this.state = useState({ - stats: null, - }); - - onWillStart(async () => { - this.state.stats = await loadStatistics(); - }); - } - - 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, "list"], - [false, "form"], - ], - target: "current", - }); - } -} - -/* --------------------------- - Layout Wrapper -----------------------------*/ -class AwesomeDashboardWrapper extends Component { - static template = "awesome_dashboard.AwesomeDashboardWrapper"; - static components = { Layout, AwesomeDashboard }; - - get layoutProps() { - return { - controlPanel: {}, - className: "o_dashboard h-100", - }; - } -} - -registry.category("actions").add( - "awesome_dashboard.dashboard", - AwesomeDashboardWrapper -); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 8d262c7a24f..e614d5a9e40 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -3,11 +3,23 @@ + + + + + + + -
@@ -17,23 +29,8 @@
- - -
- - - -
- -
diff --git a/awesome_dashboard/static/src/js/awesome_dashboard.js b/awesome_dashboard/static/src/js/awesome_dashboard.js new file mode 100644 index 00000000000..146c842e324 --- /dev/null +++ b/awesome_dashboard/static/src/js/awesome_dashboard.js @@ -0,0 +1,21 @@ +import { Component, onWillStart, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { loadStatistics } from "@awesome_dashboard/services/statistics_service"; +import { DashboardItem } from "./dashboard_item"; + +export class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { DashboardItem }; + + setup() { + this.action = useService("action"); + + this.state = useState({ + stats: null, + }); + + onWillStart(async () => { + this.state.stats = await loadStatistics(); + }); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/js/awesome_dashboard_wrapper.js b/awesome_dashboard/static/src/js/awesome_dashboard_wrapper.js new file mode 100644 index 00000000000..4c24b388948 --- /dev/null +++ b/awesome_dashboard/static/src/js/awesome_dashboard_wrapper.js @@ -0,0 +1,53 @@ +import { Component, onWillStart, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { AwesomeDashboard } from "./awesome_dashboard"; + +export class AwesomeDashboardWrapper extends Component { + static template = "awesome_dashboard.AwesomeDashboardWrapper"; + static components = { Layout, AwesomeDashboard }; + + setup() { + // If you need any services, e.g., action + this.action = this.env.services.action; + } + + // Move the handlers here + 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, "list"], + [false, "form"], + ], + target: "current", + }); + } + + get layoutProps() { + return { + display: { + controlPanel: {}, + className: "o_dashboard h-100", + }, + }; + } +} + +// Register the action +try { + registry.category("actions").add( + "awesome_dashboard.dashboard", + AwesomeDashboardWrapper + ); +} catch (e) { + if (!e.message.includes("already exists")) { + throw e; + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/js/dashboard_item.js b/awesome_dashboard/static/src/js/dashboard_item.js new file mode 100644 index 00000000000..546fb3b8815 --- /dev/null +++ b/awesome_dashboard/static/src/js/dashboard_item.js @@ -0,0 +1,18 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + // Default value + static defaultProps = { + size: 1, // if parent doesn't provide `size`, it will be 1 + }; + + static props = { + size: { type: Number, optional: true }, + }; + + get width() { + return `width: ${18 * this.props.size}rem`; + } +} \ No newline at end of file From ce585d64674971dc03c56445e0c5926da43d7686 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Wed, 25 Feb 2026 09:40:31 +0100 Subject: [PATCH 44/50] [ADD] awesome_dashboard: add PieChart component with lazy-loaded Chart.js - Created reusable PieChart component rendering a canvas element - Lazy loaded Chart.js using loadJs in onWillStart - Drew pie chart displaying t-shirt quantities by size (S/M/L/XL/XXL) - Integrated PieChart inside DashboardItem with adjustable size - Used statistics service data from /awesome_dashboard/statistics This enhances the dashboard with visual analytics while optimizing performance through lazy loading. --- awesome_dashboard/static/src/dashboard.xml | 10 +++++ .../static/src/js/awesome_dashboard.js | 3 +- awesome_dashboard/static/src/js/pie_chart.js | 38 +++++++++++++++++++ .../static/src/js/pie_chart_card.js | 14 +++++++ .../static/src/xml/pie_chart.xml | 5 +++ .../static/src/xml/pie_chart_card.xml | 6 +++ 6 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 awesome_dashboard/static/src/js/pie_chart.js create mode 100644 awesome_dashboard/static/src/js/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/xml/pie_chart.xml create mode 100644 awesome_dashboard/static/src/xml/pie_chart_card.xml diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index e614d5a9e40..0053b9dda73 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -62,6 +62,16 @@

+ +
T-Shirt Sizes Sold
+ + + + + +
diff --git a/awesome_dashboard/static/src/js/awesome_dashboard.js b/awesome_dashboard/static/src/js/awesome_dashboard.js index 146c842e324..8d0602951cc 100644 --- a/awesome_dashboard/static/src/js/awesome_dashboard.js +++ b/awesome_dashboard/static/src/js/awesome_dashboard.js @@ -2,10 +2,11 @@ import { Component, onWillStart, useState } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; import { loadStatistics } from "@awesome_dashboard/services/statistics_service"; import { DashboardItem } from "./dashboard_item"; +import { PieChart } from "./pie_chart"; export class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { DashboardItem }; + static components = { DashboardItem, PieChart }; setup() { this.action = useService("action"); diff --git a/awesome_dashboard/static/src/js/pie_chart.js b/awesome_dashboard/static/src/js/pie_chart.js new file mode 100644 index 00000000000..2514a6c7bc1 --- /dev/null +++ b/awesome_dashboard/static/src/js/pie_chart.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ + +import { Component, onWillStart, useEffect, useRef } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + useEffect(() => { + this.renderChart(); + }); + } + + renderChart() { + if (this.chart) { + this.chart.destroy(); + } + let labels = Object.keys(this.props.data); + let data = Object.values(this.props.data); + this.chart = new Chart(this.canvasRef.el, { + type: "pie", + data: { + datasets: [ + { + data: data, + }, + ], + labels: labels, + }, + }); + } +} diff --git a/awesome_dashboard/static/src/js/pie_chart_card.js b/awesome_dashboard/static/src/js/pie_chart_card.js new file mode 100644 index 00000000000..b8d68d784e2 --- /dev/null +++ b/awesome_dashboard/static/src/js/pie_chart_card.js @@ -0,0 +1,14 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; +import { DashboardItem } from "./dashboard_item"; +import { PieChart } from "./pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { DashboardItem, PieChart }; + static props = { + title: String, + data: Object, + }; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/xml/pie_chart.xml b/awesome_dashboard/static/src/xml/pie_chart.xml new file mode 100644 index 00000000000..9c0e4cab0d5 --- /dev/null +++ b/awesome_dashboard/static/src/xml/pie_chart.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/awesome_dashboard/static/src/xml/pie_chart_card.xml b/awesome_dashboard/static/src/xml/pie_chart_card.xml new file mode 100644 index 00000000000..32e3440f9bd --- /dev/null +++ b/awesome_dashboard/static/src/xml/pie_chart_card.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From 4b4f0bb8e3cc1d88f988eaf1a330bc0396198534 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Wed, 25 Feb 2026 10:48:56 +0100 Subject: [PATCH 45/50] [IMP] awesome_dashboard: add live refresh to statistics service - Updated statistics service to periodically reload data (setInterval) - Returned a reactive object to allow live updates - Updated data in place to notify subscribers - Modified Dashboard to use useState on the reactive statistics object - Ensured dashboard updates automatically when data refreshes This enables real-time dashboard updates while keeping statistics logic centralized in the service. --- awesome_dashboard/static/src/dashboard.xml | 16 ++++---- .../static/src/js/awesome_dashboard.js | 14 ++----- .../static/src/services/statistics_service.js | 40 ++++++++++++++++--- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 0053b9dda73..d272728b99e 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -32,42 +32,42 @@
+ t-if="stats.isReady">
New Orders
-

+

This month
Total Amount
-

+

This month
Avg T-Shirts / Order
-

+

Cancelled Orders
-

+

This month
Avg Processing Time (hours)
-

+

T-Shirt Sizes Sold
- + diff --git a/awesome_dashboard/static/src/js/awesome_dashboard.js b/awesome_dashboard/static/src/js/awesome_dashboard.js index 8d0602951cc..7b087b2362a 100644 --- a/awesome_dashboard/static/src/js/awesome_dashboard.js +++ b/awesome_dashboard/static/src/js/awesome_dashboard.js @@ -1,8 +1,8 @@ import { Component, onWillStart, useState } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; -import { loadStatistics } from "@awesome_dashboard/services/statistics_service"; -import { DashboardItem } from "./dashboard_item"; -import { PieChart } from "./pie_chart"; +import { statisticsStore } from "@awesome_dashboard/services/statistics_service"; +import { DashboardItem } from "../js/dashboard_item"; +import { PieChart } from "../js/pie_chart"; export class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; @@ -11,12 +11,6 @@ export class AwesomeDashboard extends Component { setup() { this.action = useService("action"); - this.state = useState({ - stats: null, - }); - - onWillStart(async () => { - this.state.stats = await loadStatistics(); - }); + this.stats = useState(statisticsStore); } } \ No newline at end of file diff --git a/awesome_dashboard/static/src/services/statistics_service.js b/awesome_dashboard/static/src/services/statistics_service.js index 08e45f2740f..898332009b0 100644 --- a/awesome_dashboard/static/src/services/statistics_service.js +++ b/awesome_dashboard/static/src/services/statistics_service.js @@ -1,8 +1,38 @@ /** @odoo-module **/ -import { rpc } from "@web/core/network/rpc"; -import { memoize } from "@web/core/utils/functions"; +import { reactive } from "@odoo/owl"; -export const loadStatistics = memoize(async function () { - return await rpc("/awesome_dashboard/statistics", {}); -}); \ No newline at end of file +const REFRESH_INTERVAL = 600000; + +export const statisticsStore = reactive({ + data: null, + isReady: false, +}); + +async function loadStatistics() { + const response = await fetch("/awesome_dashboard/statistics", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "call", + params: {}, + id: Date.now(), + }), + }); + + const payload = await response.json(); + + statisticsStore.data = payload.result; + statisticsStore.isReady = true; +} + +// Initial load +loadStatistics(); + +// Auto refresh +setInterval(() => { + loadStatistics(); +}, REFRESH_INTERVAL); \ No newline at end of file From be259d0bfbfe3b23b7b624da29ddf8b61027cbfa Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Wed, 25 Feb 2026 12:00:24 +0100 Subject: [PATCH 46/50] [IMP] awesome_dashboard: enable lazy loading for dashboard - Moved all dashboard assets into /dashboard subfolder - Created awesome_dashboard.dashboard asset bundle for dashboard content - Modified Dashboard component to register in lazy_components registry - Added dashboard_action.js with LazyComponent wrapper to load dashboard on demand - Registered lazy loader in actions registry for deferred loading This improves performance by loading dashboard assets only when users access the dashboard. --- awesome_dashboard/__manifest__.py | 5 ++- .../{js => dashboard}/awesome_dashboard.js | 11 ++++--- .../awesome_dashboard_wrapper.js | 32 ++++--------------- .../static/src/{ => dashboard}/dashboard.scss | 0 .../static/src/{ => dashboard}/dashboard.xml | 0 .../src/{js => dashboard}/dashboard_item.js | 0 .../static/src/{js => dashboard}/pie_chart.js | 0 .../src/{js => dashboard}/pie_chart_card.js | 0 .../services/statistics_service.js | 22 +++---------- .../src/{ => dashboard}/xml/pie_chart.xml | 0 .../{ => dashboard}/xml/pie_chart_card.xml | 0 .../static/src/dashboard_action.js | 17 ++++++++++ 12 files changed, 39 insertions(+), 48 deletions(-) rename awesome_dashboard/static/src/{js => dashboard}/awesome_dashboard.js (57%) rename awesome_dashboard/static/src/{js => dashboard}/awesome_dashboard_wrapper.js (53%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.scss (100%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.xml (100%) rename awesome_dashboard/static/src/{js => dashboard}/dashboard_item.js (100%) rename awesome_dashboard/static/src/{js => dashboard}/pie_chart.js (100%) rename awesome_dashboard/static/src/{js => dashboard}/pie_chart_card.js (100%) rename awesome_dashboard/static/src/{ => dashboard}/services/statistics_service.js (56%) rename awesome_dashboard/static/src/{ => dashboard}/xml/pie_chart.xml (100%) rename awesome_dashboard/static/src/{ => dashboard}/xml/pie_chart_card.xml (100%) create mode 100644 awesome_dashboard/static/src/dashboard_action.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..a52714a9550 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -23,7 +23,10 @@ ], 'assets': { 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + 'awesome_dashboard/static/src/dashboard_action.js', # loader + ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', # everything else ], }, 'license': 'AGPL-3' diff --git a/awesome_dashboard/static/src/js/awesome_dashboard.js b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js similarity index 57% rename from awesome_dashboard/static/src/js/awesome_dashboard.js rename to awesome_dashboard/static/src/dashboard/awesome_dashboard.js index 7b087b2362a..fe621460374 100644 --- a/awesome_dashboard/static/src/js/awesome_dashboard.js +++ b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js @@ -1,8 +1,8 @@ -import { Component, onWillStart, useState } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; -import { statisticsStore } from "@awesome_dashboard/services/statistics_service"; -import { DashboardItem } from "../js/dashboard_item"; -import { PieChart } from "../js/pie_chart"; +import { statisticsStore } from "./services/statistics_service"; +import { DashboardItem } from "./dashboard_item"; +import { PieChart } from "./pie_chart"; export class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; @@ -13,4 +13,5 @@ export class AwesomeDashboard extends Component { this.stats = useState(statisticsStore); } -} \ No newline at end of file +} + diff --git a/awesome_dashboard/static/src/js/awesome_dashboard_wrapper.js b/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js similarity index 53% rename from awesome_dashboard/static/src/js/awesome_dashboard_wrapper.js rename to awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js index 4c24b388948..c85b9bc1056 100644 --- a/awesome_dashboard/static/src/js/awesome_dashboard_wrapper.js +++ b/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js @@ -1,18 +1,17 @@ -import { Component, onWillStart, useState } from "@odoo/owl"; -import { registry } from "@web/core/registry"; +/** @odoo-module **/ +import { Component } from "@odoo/owl"; import { Layout } from "@web/search/layout"; import { AwesomeDashboard } from "./awesome_dashboard"; +import { registry } from "@web/core/registry"; export class AwesomeDashboardWrapper extends Component { static template = "awesome_dashboard.AwesomeDashboardWrapper"; static components = { Layout, AwesomeDashboard }; setup() { - // If you need any services, e.g., action this.action = this.env.services.action; } - // Move the handlers here openCustomers() { this.action.doAction("base.action_partner_form"); } @@ -22,32 +21,15 @@ export class AwesomeDashboardWrapper extends Component { type: "ir.actions.act_window", name: "Leads", res_model: "crm.lead", - views: [ - [false, "list"], - [false, "form"], - ], + views: [[false, "list"], [false, "form"]], target: "current", }); } get layoutProps() { - return { - display: { - controlPanel: {}, - className: "o_dashboard h-100", - }, - }; + return { display: { controlPanel: {}, className: "o_dashboard h-100" } }; } } -// Register the action -try { - registry.category("actions").add( - "awesome_dashboard.dashboard", - AwesomeDashboardWrapper - ); -} catch (e) { - if (!e.message.includes("already exists")) { - throw e; - } -} \ No newline at end of file +// Register for lazy loading +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboardWrapper); \ No newline at end of file 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/js/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js similarity index 100% rename from awesome_dashboard/static/src/js/dashboard_item.js rename to awesome_dashboard/static/src/dashboard/dashboard_item.js diff --git a/awesome_dashboard/static/src/js/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart.js similarity index 100% rename from awesome_dashboard/static/src/js/pie_chart.js rename to awesome_dashboard/static/src/dashboard/pie_chart.js diff --git a/awesome_dashboard/static/src/js/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card.js similarity index 100% rename from awesome_dashboard/static/src/js/pie_chart_card.js rename to awesome_dashboard/static/src/dashboard/pie_chart_card.js diff --git a/awesome_dashboard/static/src/services/statistics_service.js b/awesome_dashboard/static/src/dashboard/services/statistics_service.js similarity index 56% rename from awesome_dashboard/static/src/services/statistics_service.js rename to awesome_dashboard/static/src/dashboard/services/statistics_service.js index 898332009b0..c7442f3fc49 100644 --- a/awesome_dashboard/static/src/services/statistics_service.js +++ b/awesome_dashboard/static/src/dashboard/services/statistics_service.js @@ -1,8 +1,7 @@ /** @odoo-module **/ - import { reactive } from "@odoo/owl"; -const REFRESH_INTERVAL = 600000; +const REFRESH_INTERVAL = 10000; // for testing export const statisticsStore = reactive({ data: null, @@ -12,19 +11,10 @@ export const statisticsStore = reactive({ async function loadStatistics() { const response = await fetch("/awesome_dashboard/statistics", { method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - jsonrpc: "2.0", - method: "call", - params: {}, - id: Date.now(), - }), + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", method: "call", params: {}, id: Date.now() }), }); - const payload = await response.json(); - statisticsStore.data = payload.result; statisticsStore.isReady = true; } @@ -32,7 +22,5 @@ async function loadStatistics() { // Initial load loadStatistics(); -// Auto refresh -setInterval(() => { - loadStatistics(); -}, REFRESH_INTERVAL); \ No newline at end of file +// Auto-refresh +setInterval(loadStatistics, REFRESH_INTERVAL); \ No newline at end of file diff --git a/awesome_dashboard/static/src/xml/pie_chart.xml b/awesome_dashboard/static/src/dashboard/xml/pie_chart.xml similarity index 100% rename from awesome_dashboard/static/src/xml/pie_chart.xml rename to awesome_dashboard/static/src/dashboard/xml/pie_chart.xml diff --git a/awesome_dashboard/static/src/xml/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/xml/pie_chart_card.xml similarity index 100% rename from awesome_dashboard/static/src/xml/pie_chart_card.xml rename to awesome_dashboard/static/src/dashboard/xml/pie_chart_card.xml diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..519417acd5d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,17 @@ +/** @odoo-module **/ +import { Component, xml } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { registry } from "@web/core/registry"; + +export class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +// Register action +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); \ No newline at end of file From 483bd50097dacde33fcf49ab8285b50b46fa1c83 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Wed, 25 Feb 2026 14:27:24 +0100 Subject: [PATCH 47/50] [REF] awesome_dashboard: make dashboard generic with dynamic items - Introduced NumberCard and PieChartCard components with explicit props - Created dashboard_items.js exporting configurable dashboard items list - Defined item structure with id, description, Component, size, and props - Refactored Dashboard template to iterate over items with t-foreach - Implemented dynamic components (t-component) and dynamic props (t-props) - Removed hardcoded dashboard content This makes the dashboard fully generic and extensible, allowing flexible configuration of displayed items. --- awesome_dashboard/__manifest__.py | 3 +- .../static/src/dashboard/awesome_dashboard.js | 2 + .../static/src/dashboard/dashboard.xml | 84 ------------------- .../static/src/dashboard/dashboard_items.js | 62 ++++++++++++++ .../static/src/dashboard/number_card.js | 10 +++ .../dashboard/services/statistics_service.js | 2 +- .../static/src/dashboard/xml/dashboard.xml | 47 +++++++++++ .../static/src/dashboard/xml/number_card.xml | 8 ++ 8 files changed, 132 insertions(+), 86 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/xml/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/xml/number_card.xml diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a52714a9550..b87c06bf878 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -26,7 +26,8 @@ 'awesome_dashboard/static/src/dashboard_action.js', # loader ], 'awesome_dashboard.dashboard': [ - 'awesome_dashboard/static/src/dashboard/**/*', # everything else + 'awesome_dashboard/static/src/dashboard/**/*', + 'awesome_dashboard/static/src/dashboard/xml/**/*', ], }, 'license': 'AGPL-3' diff --git a/awesome_dashboard/static/src/dashboard/awesome_dashboard.js b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js index fe621460374..1b501dbad07 100644 --- a/awesome_dashboard/static/src/dashboard/awesome_dashboard.js +++ b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js @@ -3,6 +3,7 @@ import { useService } from "@web/core/utils/hooks"; import { statisticsStore } from "./services/statistics_service"; import { DashboardItem } from "./dashboard_item"; import { PieChart } from "./pie_chart"; +import { items } from "./dashboard_items"; export class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; @@ -12,6 +13,7 @@ export class AwesomeDashboard extends Component { this.action = useService("action"); this.stats = useState(statisticsStore); + this.items = items; } } diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml deleted file mode 100644 index d272728b99e..00000000000 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - -
-
- -
-
-
- - - -
- - -
New Orders
-

- This month -
- - -
Total Amount
-

- This month -
- - -
Avg T-Shirts / Order
-

-
- - -
Cancelled Orders
-

- This month -
- - -
Avg Processing Time (hours)
-

-
- - -
T-Shirt Sizes Sold
- - - - - -
-
- - -
- Loading statistics... -
- -
- -
\ No newline at end of file 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..6666fe655d8 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,62 @@ +/** @odoo-module **/ +import { NumberCard } from "./number_card"; +import { PieChartCard } from "./pie_chart_card"; + +export const items = [ + { + id: "new_orders", + description: "New orders this month", + Component: NumberCard, + props: (stats) => ({ + title: "New Orders", + value: stats.nb_new_orders, + }), + }, + { + id: "total_amount", + description: "Total amount this month", + Component: NumberCard, + props: (stats) => ({ + title: "Total Amount", + value: stats.total_amount, + }), + }, + { + id: "average_quantity", + description: "Average T-Shirts per order", + Component: NumberCard, + props: (stats) => ({ + title: "Avg T-Shirts / Order", + value: stats.average_quantity, + }), + }, + { + id: "cancelled_orders", + description: "Cancelled orders this month", + Component: NumberCard, + props: (stats) => ({ + title: "Cancelled Orders", + value: stats.nb_cancelled_orders, + }), + }, + { + id: "avg_time", + description: "Average processing time", + Component: NumberCard, + props: (stats) => ({ + title: "Avg Processing Time (hours)", + value: stats.average_time, + }), + size: 2, + }, + { + id: "tshirt_sizes", + description: "T-Shirt sizes sold", + Component: PieChartCard, + props: (stats) => ({ + title: "T-Shirt Sizes Sold", + data: stats.orders_by_size, + }), + size: 2, + }, +]; \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/number_card.js b/awesome_dashboard/static/src/dashboard/number_card.js new file mode 100644 index 00000000000..a675b0e817c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card.js @@ -0,0 +1,10 @@ +/** @odoo-module **/ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: String, + value: [Number, String], + }; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/services/statistics_service.js b/awesome_dashboard/static/src/dashboard/services/statistics_service.js index c7442f3fc49..d0bde1f706f 100644 --- a/awesome_dashboard/static/src/dashboard/services/statistics_service.js +++ b/awesome_dashboard/static/src/dashboard/services/statistics_service.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { reactive } from "@odoo/owl"; -const REFRESH_INTERVAL = 10000; // for testing +const REFRESH_INTERVAL = 10000; // make it 600000 for 10mins export const statisticsStore = reactive({ data: null, diff --git a/awesome_dashboard/static/src/dashboard/xml/dashboard.xml b/awesome_dashboard/static/src/dashboard/xml/dashboard.xml new file mode 100644 index 00000000000..321dcb1dee6 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/xml/dashboard.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ + +
+ + + + + + +
+ +
+ Loading statistics... +
+
+ +
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/xml/number_card.xml b/awesome_dashboard/static/src/dashboard/xml/number_card.xml new file mode 100644 index 00000000000..0781ebfde79 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/xml/number_card.xml @@ -0,0 +1,8 @@ + + +
+
+

+
+
+
\ No newline at end of file From 08f9b6750c7fb348ca99e7ffff8e7c9014c307c5 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Wed, 25 Feb 2026 16:13:29 +0100 Subject: [PATCH 48/50] [IMP] awesome_dashboard: make dashboard extensible with registry - Replaced static items list with awesome_dashboard registry - Registered NumberCard and PieChartCard items in the registry - Updated Dashboard to load items dynamically from registry - Removed hardcoded dashboard_items export This makes the dashboard fully extensible, allowing other addons to register new dashboard items dynamically. --- .../static/src/dashboard/awesome_dashboard.js | 17 ++- .../static/src/dashboard/dashboard_items.js | 121 +++++++++--------- .../src/dashboard/dashboard_registry.js | 6 + 3 files changed, 77 insertions(+), 67 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_registry.js diff --git a/awesome_dashboard/static/src/dashboard/awesome_dashboard.js b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js index 1b501dbad07..f36d3c80611 100644 --- a/awesome_dashboard/static/src/dashboard/awesome_dashboard.js +++ b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js @@ -1,19 +1,18 @@ +/** @odoo-module **/ + import { Component, useState } from "@odoo/owl"; -import { useService } from "@web/core/utils/hooks"; import { statisticsStore } from "./services/statistics_service"; import { DashboardItem } from "./dashboard_item"; -import { PieChart } from "./pie_chart"; -import { items } from "./dashboard_items"; +import { dashboardItemRegistry } from "./dashboard_registry"; export class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { DashboardItem, PieChart }; + static components = { DashboardItem }; setup() { - this.action = useService("action"); - this.stats = useState(statisticsStore); - this.items = items; - } -} + // Get all registered dashboard items + this.items = dashboardItemRegistry.getAll(); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js index 6666fe655d8..b471e6fb8fc 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_items.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -1,62 +1,67 @@ /** @odoo-module **/ + +import { dashboardItemRegistry } from "./dashboard_registry"; import { NumberCard } from "./number_card"; import { PieChartCard } from "./pie_chart_card"; -export const items = [ - { - id: "new_orders", - description: "New orders this month", - Component: NumberCard, - props: (stats) => ({ - title: "New Orders", - value: stats.nb_new_orders, - }), - }, - { - id: "total_amount", - description: "Total amount this month", - Component: NumberCard, - props: (stats) => ({ - title: "Total Amount", - value: stats.total_amount, - }), - }, - { - id: "average_quantity", - description: "Average T-Shirts per order", - Component: NumberCard, - props: (stats) => ({ - title: "Avg T-Shirts / Order", - value: stats.average_quantity, - }), - }, - { - id: "cancelled_orders", - description: "Cancelled orders this month", - Component: NumberCard, - props: (stats) => ({ - title: "Cancelled Orders", - value: stats.nb_cancelled_orders, - }), - }, - { - id: "avg_time", - description: "Average processing time", - Component: NumberCard, - props: (stats) => ({ - title: "Avg Processing Time (hours)", - value: stats.average_time, - }), - size: 2, - }, - { - id: "tshirt_sizes", - description: "T-Shirt sizes sold", - Component: PieChartCard, - props: (stats) => ({ - title: "T-Shirt Sizes Sold", - data: stats.orders_by_size, - }), - size: 2, - }, -]; \ No newline at end of file +dashboardItemRegistry.add("new_orders", { + id: "new_orders", + description: "New orders this month", + Component: NumberCard, + props: (stats) => ({ + title: "New Orders", + value: stats.nb_new_orders, + }), +}); + +dashboardItemRegistry.add("total_amount", { + id: "total_amount", + description: "Total amount this month", + Component: NumberCard, + props: (stats) => ({ + title: "Total Amount", + value: stats.total_amount, + }), +}); + +dashboardItemRegistry.add("average_quantity", { + id: "average_quantity", + description: "Average T-Shirts per order", + Component: NumberCard, + props: (stats) => ({ + title: "Avg T-Shirts / Order", + value: stats.average_quantity, + }), +}); + +dashboardItemRegistry.add("cancelled_orders", { + id: "cancelled_orders", + description: "Cancelled orders this month", + Component: NumberCard, + props: (stats) => ({ + title: "Cancelled Orders", + value: stats.nb_cancelled_orders, + }), +}); + +dashboardItemRegistry.add("avg_time", { + id: "avg_time", + description: "Average processing time", + Component: NumberCard, + size: 2, + props: (stats) => ({ + title: "Avg Processing Time (hours)", + value: stats.average_time, + }), +}); + +dashboardItemRegistry.add("tshirt_sizes", { + id: "tshirt_sizes", + description: "T-Shirt sizes sold", + Component: PieChartCard, + size: 2, + props: (stats) => ({ + title: "T-Shirt Sizes Sold", + data: stats.orders_by_size, + }), +}); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_registry.js b/awesome_dashboard/static/src/dashboard/dashboard_registry.js new file mode 100644 index 00000000000..3209b6a6f17 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_registry.js @@ -0,0 +1,6 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; + +// Create a new registry category for dashboard items +export const dashboardItemRegistry = registry.category("awesome_dashboard.items"); \ No newline at end of file From df82d76dc676b0068f58135421836b6ee1ce8629 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Wed, 25 Feb 2026 16:47:29 +0100 Subject: [PATCH 49/50] [IMP] awesome_dashboard: add customizable dashboard items with local storage - Added settings button in control panel to open dashboard configuration dialog - Displayed list of all dashboard items with checkboxes in the dialog - Implemented Apply button to save unchecked item ids to local storage - Updated Dashboard component to filter items based on stored configuration - Ensured user dashboard preferences persist across sessions This enables users to customize which dashboard items are visible. --- .../static/src/dashboard/awesome_dashboard.js | 37 ++++++++++++-- .../dashboard/awesome_dashboard_wrapper.js | 11 +++++ .../dashboard/dashboard_settings_dialog.js | 49 +++++++++++++++++++ .../static/src/dashboard/xml/dashboard.xml | 14 +++--- .../xml/dashboard_settings_dialog.xml | 26 ++++++++++ 5 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js create mode 100644 awesome_dashboard/static/src/dashboard/xml/dashboard_settings_dialog.xml diff --git a/awesome_dashboard/static/src/dashboard/awesome_dashboard.js b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js index f36d3c80611..a5331a44e92 100644 --- a/awesome_dashboard/static/src/dashboard/awesome_dashboard.js +++ b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js @@ -1,18 +1,49 @@ /** @odoo-module **/ - import { Component, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; import { statisticsStore } from "./services/statistics_service"; import { DashboardItem } from "./dashboard_item"; import { dashboardItemRegistry } from "./dashboard_registry"; +import { DashboardSettingsDialog } from "./dashboard_settings_dialog"; export class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; static components = { DashboardItem }; + static props = { + onSettingsOpen: Function, // callback to expose settings API + }; + setup() { + this.dialog = useService("dialog"); this.stats = useState(statisticsStore); - // Get all registered dashboard items - this.items = dashboardItemRegistry.getAll(); + this.allItems = dashboardItemRegistry.getAll(); + + // reactive items + this.state = useState({ + items: this.getFilteredItems(), + }); + + // expose the API + if (this.props.onSettingsOpen) { + this.props.onSettingsOpen(() => this.openSettings()); + } + } + + getFilteredItems() { + const removed = JSON.parse( + localStorage.getItem("awesome_dashboard.removed_items") || "[]" + ); + return this.allItems.filter(item => !removed.includes(item.id)); + } + + openSettings() { + this.dialog.add(DashboardSettingsDialog, { + items: this.allItems, + onApply: () => { + this.state.items = this.getFilteredItems(); + }, + }); } } \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js b/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js index c85b9bc1056..ff5aaf06271 100644 --- a/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js +++ b/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js @@ -10,6 +10,17 @@ export class AwesomeDashboardWrapper extends Component { setup() { this.action = this.env.services.action; + this.dashboardOpenSettings = null; + + this.setDashboardSettingsCallback = (fn) => { + this.dashboardOpenSettings = fn; + }; + } + + openSettings() { + if (this.dashboardOpenSettings) { + this.dashboardOpenSettings(); + } } openCustomers() { diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js b/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js new file mode 100644 index 00000000000..ab4fb94eea8 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js @@ -0,0 +1,49 @@ +/** @odoo-module **/ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; + +export class DashboardSettingsDialog extends Component { + static template = "awesome_dashboard.DashboardSettingsDialog"; + static components = { Dialog }; + + static props = { + items: Array, + onApply: Function, + close: Function, // this is automatically passed by Dialog + }; + + setup() { + const removed = JSON.parse( + localStorage.getItem("awesome_dashboard.removed_items") || "[]" + ); + + this.state = useState({ + checked: Object.fromEntries( + this.props.items.map(item => [ + item.id, + !removed.includes(item.id) + ]) + ) + }); + } + + toggle(itemId) { + this.state.checked[itemId] = !this.state.checked[itemId]; + } + + apply() { + const removedIds = Object.entries(this.state.checked) + .filter(([id, checked]) => !checked) + .map(([id]) => id); + + localStorage.setItem( + "awesome_dashboard.removed_items", + JSON.stringify(removedIds) + ); + + if (this.props.onApply) { + this.props.onApply(); + } + this.props.close(); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/xml/dashboard.xml b/awesome_dashboard/static/src/dashboard/xml/dashboard.xml index 321dcb1dee6..b044c6a352a 100644 --- a/awesome_dashboard/static/src/dashboard/xml/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/xml/dashboard.xml @@ -5,18 +5,20 @@ - - + + - +
@@ -31,7 +33,7 @@
- + diff --git a/awesome_dashboard/static/src/dashboard/xml/dashboard_settings_dialog.xml b/awesome_dashboard/static/src/dashboard/xml/dashboard_settings_dialog.xml new file mode 100644 index 00000000000..5a99cf0ff8c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/xml/dashboard_settings_dialog.xml @@ -0,0 +1,26 @@ + + + +
+ +
+ + +
+
+
+
+ +
+
+
+
\ No newline at end of file From 492ac0783789295743e250c1fd3e98179ef36320 Mon Sep 17 00:00:00 2001 From: jupao-odoo Date: Fri, 27 Feb 2026 16:46:12 +0100 Subject: [PATCH 50/50] [REF] awesome_dashboard: remove unnecessary @odoo-module headers - Removed redundant /** @odoo-module **/ declarations - Cleaned up JS files to keep only required module definitions No functional impact. --- awesome_dashboard/static/src/dashboard/awesome_dashboard.js | 1 - .../static/src/dashboard/awesome_dashboard_wrapper.js | 1 - awesome_dashboard/static/src/dashboard/dashboard_items.js | 2 -- awesome_dashboard/static/src/dashboard/dashboard_registry.js | 2 -- .../static/src/dashboard/dashboard_settings_dialog.js | 1 - awesome_dashboard/static/src/dashboard/number_card.js | 1 - awesome_dashboard/static/src/dashboard/pie_chart.js | 2 -- .../static/src/dashboard/services/statistics_service.js | 1 - awesome_dashboard/static/src/dashboard_action.js | 1 - 9 files changed, 12 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard/awesome_dashboard.js b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js index a5331a44e92..6f6235ea084 100644 --- a/awesome_dashboard/static/src/dashboard/awesome_dashboard.js +++ b/awesome_dashboard/static/src/dashboard/awesome_dashboard.js @@ -1,4 +1,3 @@ -/** @odoo-module **/ import { Component, useState } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; import { statisticsStore } from "./services/statistics_service"; diff --git a/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js b/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js index ff5aaf06271..f0357b07fc6 100644 --- a/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js +++ b/awesome_dashboard/static/src/dashboard/awesome_dashboard_wrapper.js @@ -1,4 +1,3 @@ -/** @odoo-module **/ import { Component } from "@odoo/owl"; import { Layout } from "@web/search/layout"; import { AwesomeDashboard } from "./awesome_dashboard"; diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js index b471e6fb8fc..937bb19f635 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_items.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import { dashboardItemRegistry } from "./dashboard_registry"; import { NumberCard } from "./number_card"; import { PieChartCard } from "./pie_chart_card"; diff --git a/awesome_dashboard/static/src/dashboard/dashboard_registry.js b/awesome_dashboard/static/src/dashboard/dashboard_registry.js index 3209b6a6f17..ad1edfb465b 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_registry.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_registry.js @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import { registry } from "@web/core/registry"; // Create a new registry category for dashboard items diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js b/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js index ab4fb94eea8..272d41913ac 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js @@ -1,4 +1,3 @@ -/** @odoo-module **/ import { Component, useState } from "@odoo/owl"; import { Dialog } from "@web/core/dialog/dialog"; diff --git a/awesome_dashboard/static/src/dashboard/number_card.js b/awesome_dashboard/static/src/dashboard/number_card.js index a675b0e817c..cca81306f58 100644 --- a/awesome_dashboard/static/src/dashboard/number_card.js +++ b/awesome_dashboard/static/src/dashboard/number_card.js @@ -1,4 +1,3 @@ -/** @odoo-module **/ import { Component } from "@odoo/owl"; export class NumberCard extends Component { diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart.js index 2514a6c7bc1..9993e559ff7 100644 --- a/awesome_dashboard/static/src/dashboard/pie_chart.js +++ b/awesome_dashboard/static/src/dashboard/pie_chart.js @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import { Component, onWillStart, useEffect, useRef } from "@odoo/owl"; import { loadJS } from "@web/core/assets"; diff --git a/awesome_dashboard/static/src/dashboard/services/statistics_service.js b/awesome_dashboard/static/src/dashboard/services/statistics_service.js index d0bde1f706f..9371252c753 100644 --- a/awesome_dashboard/static/src/dashboard/services/statistics_service.js +++ b/awesome_dashboard/static/src/dashboard/services/statistics_service.js @@ -1,4 +1,3 @@ -/** @odoo-module **/ import { reactive } from "@odoo/owl"; const REFRESH_INTERVAL = 10000; // make it 600000 for 10mins diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js index 519417acd5d..be5399d19c2 100644 --- a/awesome_dashboard/static/src/dashboard_action.js +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -1,4 +1,3 @@ -/** @odoo-module **/ import { Component, xml } from "@odoo/owl"; import { LazyComponent } from "@web/core/assets"; import { registry } from "@web/core/registry";