From 13375d4921d00dab0de7469ee804f4679edfab71 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Mon, 17 Feb 2025 15:50:29 +0100 Subject: [PATCH 01/35] [ADD] estate: create a basic setup for the module - chapter-2 apply the instructions by which I declare a new app. add the necessary configurations in manifest to let it show up in the apps list without further filtering. --- estate/__init__.py | 0 estate/__manifest__.py | 12 ++++++++++++ 2 files changed, 12 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..51342b17a2f --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,12 @@ +{ + 'name': 'Real Estate', + 'version': '18.0', + 'description': 'anything', + 'summary': 'anything again', + 'data': [], + 'depends': [ + 'base', + ], + 'application': True, + 'installable': True, +} From 4da6e085fc7df3b2a071c7245bb06ec8808ece3c Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 18 Feb 2025 10:32:26 +0100 Subject: [PATCH 02/35] [IMP] estate: correct module version and add a license to manifest - chapter-2 change the version to refer to mdoule's version instead of Odoo's version. add a license as it was missing. --- estate/__manifest__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 51342b17a2f..bab1a4186af 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Real Estate', - 'version': '18.0', + 'version': '1.0', 'description': 'anything', 'summary': 'anything again', 'data': [], @@ -9,4 +9,5 @@ ], 'application': True, 'installable': True, + 'license': 'AGPL-3' } From e1eb0f4558983fb79cf108ce94fde5ef694c9f33 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 18 Feb 2025 14:34:10 +0100 Subject: [PATCH 03/35] [IMP] estate: define property model and essentials added the estate.property model to manage properties in the estate module. it includes all the essential fields like name, price, bedrooms, .. etc, with some options for the user to choose the garden orientation. chapter-3 --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py index e69de29bb2d..9a7e03eded3 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..2adec0c2254 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,24 @@ +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'model for the properties in our app' + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string="Garden Orientation", + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + help='garden orientation is used to choose the orientation of the garden attached to the property' + ) From 2f634ec6d17f8a2432c1555fa38c1cc5bb5e9313 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 18 Feb 2025 15:38:36 +0100 Subject: [PATCH 04/35] [IMP] estate: add and link module access rules add access security access rules under the security directory to make the module accessible. add the idea config files to gitignore not to be included in the upcoming commits. chapter-4 --- .gitignore | 3 +++ estate/__manifest__.py | 2 +- estate/security/ir.model.access.csv | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/.gitignore b/.gitignore index b6e47617de1..675a67e5764 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# idea config files +.idea \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py index bab1a4186af..c9f1aa3e685 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,7 +3,7 @@ 'version': '1.0', 'description': 'anything', 'summary': 'anything again', - 'data': [], + 'data': ['security/ir.model.access.csv'], 'depends': [ 'base', ], diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..fe21e56c6d2 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From 15456970754987e64b4c3e8025b0ac8b839adaec Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 18 Feb 2025 18:57:08 +0100 Subject: [PATCH 05/35] [IMP] estate: create basic UI, enhance prop model create action to add the basic functionality of viewing and adding estate properties. implement menus to better visualize and access the properties. Add attributes to previously set fields to prevent copying, set default values, and prevent modifications. Add more predefined reserved fields to enable more functionalities (state, active). chapter-5 --- estate/__manifest__.py | 6 +++++- estate/models/estate_property.py | 30 ++++++++++++++++++++++---- estate/views/estate_menus.xml | 12 +++++++++++ estate/views/estate_property_views.xml | 8 +++++++ 4 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c9f1aa3e685..ccece092c30 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,7 +3,11 @@ 'version': '1.0', 'description': 'anything', 'summary': 'anything again', - 'data': ['security/ir.model.access.csv'], + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', + ], 'depends': [ 'base', ], diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2adec0c2254..5ef9f8410b2 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,10 +8,13 @@ class EstateProperty(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date( + copy=False, + default=fields.Date.add(fields.Date.today(), months=3) + ) expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() @@ -19,6 +22,25 @@ class EstateProperty(models.Model): garden_area = fields.Integer() garden_orientation = fields.Selection( string="Garden Orientation", - selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ], help='garden orientation is used to choose the orientation of the garden attached to the property' ) + active = fields.Boolean(default=True) + state = fields.Selection( + string="State", + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + default='new', + required=True, + copy=False, + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..09dbb95d7e5 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..42b9f769ee5 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + New Record + estate.property + list,form + + \ No newline at end of file From 5edf1b6d30a48a480169518abb30ef8145cef5f8 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Wed, 19 Feb 2025 10:52:56 +0100 Subject: [PATCH 06/35] [IMP] estate: make code style changes add file end empty line in __init__ files to align with coding style conventions. add a comma after the last record in lists for better extensibility options. remove .idea ignore from local .gitignore and move it to global ignore file. fix alignment in estate_menus. --- .gitignore | 3 -- estate/__init__.py | 2 +- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 52 ++++++++++++++++---------------- estate/views/estate_menus.xml | 6 ++-- 5 files changed, 31 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 675a67e5764..b6e47617de1 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,3 @@ dmypy.json # Pyre type checker .pyre/ - -# idea config files -.idea \ No newline at end of file diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 5ef9f8410b2..85f536a67d1 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,42 +5,42 @@ class EstateProperty(models.Model): _name = 'estate.property' _description = 'model for the properties in our app' - name = fields.Char(required=True) + name = fields.Char(required = True) description = fields.Text() postcode = fields.Char() date_availability = fields.Date( - copy=False, - default=fields.Date.add(fields.Date.today(), months=3) + copy = False, + default = fields.Date.add(fields.Date.today(), months = 3) ) - expected_price = fields.Float(required=True) - selling_price = fields.Float(readonly=True, copy=False) - bedrooms = fields.Integer(default=2) + expected_price = fields.Float(required = True) + selling_price = fields.Float(readonly = True, copy = False) + bedrooms = fields.Integer(default = 2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection( - string="Garden Orientation", - selection=[ - ('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West') - ], - help='garden orientation is used to choose the orientation of the garden attached to the property' + string = "Garden Orientation", + selection = [ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], + help = 'garden orientation is used to choose the orientation of the garden attached to the property' ) - active = fields.Boolean(default=True) + active = fields.Boolean(default = True) state = fields.Selection( - string="State", - selection=[ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('cancelled', 'Cancelled'), - ], - default='new', - required=True, - copy=False, + string = "State", + selection = [ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + default = 'new', + required = True, + copy = False, ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 09dbb95d7e5..1a7ab92cba5 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -3,9 +3,9 @@ From 4ea025cf585cb3f7f49e8f4c9ccbf181c51b6e5e Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Wed, 19 Feb 2025 11:07:47 +0100 Subject: [PATCH 07/35] [IMP] estate: add empty line at the end of files add empty missing empty lines in xml and csv files to align with coding style conventions. --- estate/security/ir.model.access.csv | 2 +- estate/views/estate_menus.xml | 2 +- estate/views/estate_property_views.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index fe21e56c6d2..85de405deb2 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 1a7ab92cba5..8db6e153022 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -9,4 +9,4 @@ /> - \ No newline at end of file + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 42b9f769ee5..ada0d17cf77 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,4 @@ estate.property list,form - \ No newline at end of file + From 173f26e3afd20b743aec1e0b5f3cfb6e2e0f0def Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Wed, 19 Feb 2025 15:55:24 +0100 Subject: [PATCH 08/35] [IMP] estate: create custom views for properties create a custom form for properties for better visuals and to suit business needs. create a notebook to be able to separate related fields together in one page. add a custom search bar to better search in relevant fields related to properties, group properties by postcode, or filter them depending on availability or active status. set a new suitable title for the action. chapter-6 --- estate/views/estate_property_views.xml | 76 +++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index ada0d17cf77..ed27a9bc6ed 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,7 +1,81 @@ + + estate.property.form + estate.property + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + - New Record + Properties estate.property list,form From bb0d205069e6a7938fc49f2e9deba65962f58ef4 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Thu, 20 Feb 2025 09:30:36 +0100 Subject: [PATCH 09/35] [IMP] estate: create estate property type logic add a new model enabling users to specify a tag for each property. implement menus to simplify accessing property tags represented in settings and property tags. create access rules for the new model. add a views file describing the related action, and list and form views. link the model to the __init__ file for the module. add views file to __manifest__ to be recognized and loaded. add two new fields for the estate.property model to be able to access the buyer and seller in the form, and search. chapter-7 --- estate/__manifest__.py | 1 + estate/models/__init__.py | 2 +- estate/models/estate_property.py | 3 ++ estate/models/estate_property_type.py | 8 ++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_menus.xml | 9 +++++- estate/views/estate_property_type_views.xml | 32 +++++++++++++++++++++ estate/views/estate_property_views.xml | 10 ++++++- 8 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index ccece092c30..f72f104de72 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,6 +6,7 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', 'views/estate_menus.xml', ], 'depends': [ diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..76e779e73b0 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property +from . import estate_property, estate_property_type diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 85f536a67d1..20aae35192b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -44,3 +44,6 @@ class EstateProperty(models.Model): required = True, copy = False, ) + 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) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..6477d67f9cd --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'the type of the property being sold' + + name = fields.Char(required = True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 85de405deb2..db554fd92fd 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 8db6e153022..e64ffb88e94 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,12 +1,19 @@ - + + + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..2599619deaf --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,32 @@ + + + + estate.property.type.form + estate.property.type + +
+ +

+ +

+
+
+
+
+ + + estate.property.type.list + estate.property.type + + + + + + + + + Property Types + estate.property.type + list,form + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index ed27a9bc6ed..1436f2e9da5 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,7 +1,7 @@ - estate.property.form + estate.property.search estate.property @@ -11,6 +11,7 @@ + @@ -31,6 +32,7 @@ + @@ -52,6 +54,12 @@ + + + + + + From 8b85b0cb8ca454589ace0e62565887cf04f8f172 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Thu, 20 Feb 2025 09:57:40 +0100 Subject: [PATCH 10/35] [IMP] estate: create tags to estate properties create an estate property tag model to be added to properties specifying more context about them. create a new menu to simplify accessing tags. add a field to estate.property to show and add related tags to the property. create some views and an action to suite viewing, creating and accessing tags. add new views file to __manifest__ to be loaded when the server starts. chapter-7 --- estate/__manifest__.py | 1 + estate/models/__init__.py | 2 +- estate/models/estate_property.py | 1 + estate/models/estate_property_tag.py | 7 ++++++ estate/security/ir.model.access.csv | 3 ++- estate/views/estate_menus.xml | 5 ++++ estate/views/estate_property_tag.xml | 32 ++++++++++++++++++++++++++ estate/views/estate_property_views.xml | 1 + 8 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/views/estate_property_tag.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index f72f104de72..3b927ecccc0 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,6 +7,7 @@ 'security/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_property_type_views.xml', + 'views/estate_property_tag.xml', 'views/estate_menus.xml', ], 'depends': [ diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 76e779e73b0..6c1ae061713 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property, estate_property_type +from . import estate_property, estate_property_type, estate_property_tag diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 20aae35192b..88a87ff852b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -47,3 +47,4 @@ class EstateProperty(models.Model): 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) + property_tag_ids = fields.Many2many("estate.property.tag", string = "Property Tags") \ No newline at end of file diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..68e8fd35f48 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,7 @@ +from odoo import fields, models + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'the tag of the property being sold' + + name = fields.Char(required = True) \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index db554fd92fd..c706368c638 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 -estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 \ No newline at end of file +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index e64ffb88e94..a982d2a89c7 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -14,6 +14,11 @@ action="estate_property_type_action" name="Property Types" /> + diff --git a/estate/views/estate_property_tag.xml b/estate/views/estate_property_tag.xml new file mode 100644 index 00000000000..ae4fc6984e5 --- /dev/null +++ b/estate/views/estate_property_tag.xml @@ -0,0 +1,32 @@ + + + + estate.property.tag.form + estate.property.tag + +
+ +

+ +

+
+
+
+
+ + + estate.property.tag.list + estate.property.tag + + + + + + + + + Property Tags + estate.property.tag + list,form + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1436f2e9da5..4ab1b5f2421 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -32,6 +32,7 @@ + From 3433acdf5f39256a1904dd6893e6fcaaf9c643d6 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Thu, 20 Feb 2025 10:35:47 +0100 Subject: [PATCH 11/35] [IMP] estate: create property offer model create a new estate.property.offer model to manage offers for specific properties. create list, and form views for the offer to manage how it looks. add offer_ids field to estate.property model to be able to view offers from inside the property form. create a new page in the notebook form view for the estate.property for simplifying accessing the offers. link views file to manifest to be loaded when starting the server. add access rules for the new model. rename estate_property_tag file to estate_property_tag_views to meet the convention. chapter-7 --- estate/__manifest__.py | 3 +- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 3 +- estate/models/estate_property_offer.py | 11 ++++++ estate/models/estate_property_tag.py | 3 +- estate/security/ir.model.access.csv | 3 +- estate/views/estate_property_offer_views.xml | 36 +++++++++++++++++++ ..._tag.xml => estate_property_tag_views.xml} | 0 estate/views/estate_property_views.xml | 5 +++ 9 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/views/estate_property_offer_views.xml rename estate/views/{estate_property_tag.xml => estate_property_tag_views.xml} (100%) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 3b927ecccc0..f69ff4d26f0 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,7 +7,8 @@ 'security/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_property_type_views.xml', - 'views/estate_property_tag.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ], 'depends': [ diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 6c1ae061713..a3ed256a7ed 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property, estate_property_type, estate_property_tag +from . import estate_property, estate_property_offer, estate_property_tag, estate_property_type diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 88a87ff852b..eb71ae29d0a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -47,4 +47,5 @@ class EstateProperty(models.Model): 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) - property_tag_ids = fields.Many2many("estate.property.tag", string = "Property Tags") \ No newline at end of file + property_tag_ids = fields.Many2many("estate.property.tag", string = "Property Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string = "Offers") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..26b70ddf76d --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'the offer of the property being sold' + + price = fields.Float() + status = fields.Selection(selection = [('accepted', 'Accepted'), ('refused', 'Refused')], copy = False) + partner_id = fields.Many2one('res.partner', string = 'Partner', required = True) + property_id = fields.Many2one('estate.property', string = 'Property', required = True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 68e8fd35f48..9e8f5df2005 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -1,7 +1,8 @@ from odoo import fields, models + class EstatePropertyTag(models.Model): _name = 'estate.property.tag' _description = 'the tag of the property being sold' - name = fields.Char(required = True) \ No newline at end of file + name = fields.Char(required = True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index c706368c638..78458c1cb61 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,4 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 -estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 \ No newline at end of file +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..36264c8cd48 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,36 @@ + + + + estate.property.offer.form + estate.property.offer + +
+ + +

+ + + +

+ + + + +
+
+
+
+
+ + + estate.property.offer.list + estate.property.offer + + + + + + + + +
diff --git a/estate/views/estate_property_tag.xml b/estate/views/estate_property_tag_views.xml similarity index 100% rename from estate/views/estate_property_tag.xml rename to estate/views/estate_property_tag_views.xml diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4ab1b5f2421..0fb975b1f4d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -55,6 +55,11 @@
+ + + + + From 059ad0fb7b338a95cd17839a627799b07599799f Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Thu, 20 Feb 2025 12:02:45 +0100 Subject: [PATCH 12/35] [IMP] estate: add computed and onchange fields create computed fields to estate.property to compute the total area automatically when the user changes one of the two summed values. add a onchange behavior for the garden toggling that changes garden_rea and garden_orientation's values either by resetting them when toggled off and putting a default value when toggled on. implement a computed field and inverse method in property offer model to be able to see the validity days when changing the deadline while updating the validity when changing the deadline as well. change the form and list views for offers to reflect the changes. chapter-8 --- estate/models/estate_property.py | 23 +++++++++++++++++++- estate/models/estate_property_offer.py | 17 ++++++++++++++- estate/views/estate_property_offer_views.xml | 4 ++++ estate/views/estate_property_views.xml | 2 ++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index eb71ae29d0a..0c7765a8b7a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -49,3 +49,24 @@ class EstateProperty(models.Model): salesperson_id = fields.Many2one("res.users", string = "Salesperson", default = lambda self: self.env.user) property_tag_ids = fields.Many2many("estate.property.tag", string = "Property Tags") offer_ids = fields.One2many("estate.property.offer", "property_id", string = "Offers") + total_area = fields.Float(compute = "_compute_total_area") + best_offer = fields.Float(compute = "_compute_best_offer") + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends('offer_ids.price') + def _compute_best_offer(self): + for record in self: + record.best_offer = max(record.offer_ids.mapped('price')) + + @api.onchange('garden') + def _onchange_garden(self): + if not self.garden: + self.garden_area = 0 + self.garden_orientation = '' + else: + self.garden_orientation = 'north' + self.garden_area = 10 \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 26b70ddf76d..978696f6091 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -9,3 +9,18 @@ class EstatePropertyOffer(models.Model): status = fields.Selection(selection = [('accepted', 'Accepted'), ('refused', 'Refused')], copy = False) partner_id = fields.Many2one('res.partner', string = 'Partner', required = True) property_id = fields.Many2one('estate.property', string = 'Property', required = True) + validity = fields.Integer(default = 7) + date_deadline = fields.Date(compute = '_compute_date_deadline', inverse = '_inverse_date_deadline') + + @api.depends('validity') + def _compute_date_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = fields.Date.add(record.create_date, days = record.validity) + + def _inverse_date_deadline(self): + for record in self: + if record.create_date: + create_date = fields.Date.from_string(record.create_date) + deadline_date = fields.Date.from_string(record.date_deadline) + record.validity = (deadline_date - create_date).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 36264c8cd48..1de0c74c3cd 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -14,6 +14,8 @@ + + @@ -29,6 +31,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 0fb975b1f4d..855ebb3f336 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -39,6 +39,7 @@ +
@@ -53,6 +54,7 @@ + From 6d7c899d28aa0fa88fc231424de5ecc9814a244b Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Thu, 20 Feb 2025 15:47:54 +0100 Subject: [PATCH 13/35] [IMP] estate: add actions to deal with offers create two buttons for selling and canceling a property and automtaing the process of changing the state. handle the logic of not selling a cancelled property and not canceling a sold property. add two buttons for offers to accept and refuse them, handle setting the buyer and selling price for the property upon acceptance. handle the logic of not accepting more than one offer at the same time for the same property. implement logic for clearing buyer and selling price data upon refusing a previously accepted offer to enable accepting another offer. chapter-9 --- estate/models/estate_property.py | 17 ++++++++++++++++- estate/models/estate_property_offer.py | 20 +++++++++++++++++++- estate/views/estate_property_offer_views.xml | 6 ++++++ estate/views/estate_property_views.xml | 5 +++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0c7765a8b7a..d3f54c1665e 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class EstateProperty(models.Model): @@ -69,4 +70,18 @@ def _onchange_garden(self): self.garden_orientation = '' else: self.garden_orientation = 'north' - self.garden_area = 10 \ No newline at end of file + self.garden_area = 10 + + def action_cancel_property(self): + for record in self: + if record.state == 'sold': + raise UserError('You cannot cancel a sold property') + record.state = 'cancelled' + return True + + def action_sell_property(self): + for record in self: + if record.state == 'cancelled': + raise UserError('You cannot sell a cancelled property') + record.state = 'sold' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 978696f6091..2c280d43caf 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): @@ -20,7 +21,24 @@ def _compute_date_deadline(self): def _inverse_date_deadline(self): for record in self: - if record.create_date: + if record.create_date and record.date_deadline: create_date = fields.Date.from_string(record.create_date) deadline_date = fields.Date.from_string(record.date_deadline) record.validity = (deadline_date - create_date).days + + def action_accept_offer(self): + if self.property_id.buyer_id: + raise UserError('This property already has an accepted offer') + self.property_id.selling_price = self.price + self.property_id.state = 'offer_accepted' + self.status = 'accepted' + self.property_id.buyer_id = self.partner_id + return True + + def action_refuse_offer(self): + if self.property_id.buyer_id == self.partner_id: + self.property_id.buyer_id = False + self.property_id.state = 'offer_received' + self.property_id.selling_price = 0 + self.status = 'refused' + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 1de0c74c3cd..be99c0b4016 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -18,6 +18,10 @@ + + +

+ + + + + + + + + + + + +
@@ -19,6 +39,7 @@ estate.property.type + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 14874a98f72..3329335da57 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -9,7 +9,8 @@ - + @@ -27,17 +28,23 @@
-
-

- +

+

- - + + @@ -45,7 +52,6 @@ - @@ -57,14 +63,16 @@ - - + + - + @@ -83,14 +91,18 @@ estate.property.list estate.property - + - + @@ -99,5 +111,6 @@ Properties estate.property list,form + {'search_default_available': True} From 2421236be21bcdf0e1dd39e354adf34a69389b79 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Mon, 24 Feb 2025 07:06:47 +0100 Subject: [PATCH 17/35] [IMP] estate: apply changes using inheretance apply ondelete method to prevent deletion of a property if its state is not 'new' or 'cancelled'. override create method in offers to handle preventing creating an offer with a lowr price than the maximum price offered. extend user form view to add a new page for estate properties related to that user using view inheritance. chapter-12 --- estate/__manifest__.py | 1 + estate/models/__init__.py | 6 +++++- estate/models/estate_property.py | 5 +++++ estate/models/estate_property_offer.py | 9 +++++++++ estate/models/estate_property_user.py | 9 +++++++++ estate/views/estate_res_users_views.xml | 17 +++++++++++++++++ 6 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 estate/models/estate_property_user.py create mode 100644 estate/views/estate_res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index acabc439599..e23e965ffd2 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -9,6 +9,7 @@ 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', + 'views/estate_res_users_views.xml', 'views/estate_menus.xml', ], 'depends': [ diff --git a/estate/models/__init__.py b/estate/models/__init__.py index a3ed256a7ed..7b3fc844702 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,5 @@ -from . import estate_property, estate_property_offer, estate_property_tag, estate_property_type +from . import estate_property +from . import estate_property_offer +from . import estate_property_tag +from . import estate_property_type +from . import estate_property_user diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 161df93aad7..c73d2fee214 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -112,6 +112,11 @@ def _check_selling_price(self): ) < 0: raise UserError('The selling price must be at least 90% of the the expected price') + @api.ondelete(at_uninstall = False) + def _ondelete_property(self): + if self.filtered(lambda record: record.state not in ('new', 'cancelled')): + raise UserError('Only new or canceled properties can be deleted') + _sql_constraints = [ ('positive_expected_price', 'CHECK(expected_price > 0)', 'Expected price must be positive'), ('positive_selling_price', 'CHECK(selling_price >= 0)', 'Selling price must be positive'), diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index a3cdff8f0ed..159f627d446 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -47,6 +47,15 @@ def action_refuse_offer(self): self.status = 'refused' return True + @api.model + def create(self, vals): + property_id_number = vals.get('property_id') + property_id = self.env['estate.property'].browse(property_id_number) + offers = property_id.offer_ids + if offers.filtered(lambda offer: offer.price > vals.get('price')): + raise UserError('The offer price must be at least %s' % max(offers.mapped('price'), default = 0.0)) + return super().create(vals) + _sql_constraints = [ ('price_positive', 'CHECK(price > 0)', 'Price must be positive') ] diff --git a/estate/models/estate_property_user.py b/estate/models/estate_property_user.py new file mode 100644 index 00000000000..492668a5895 --- /dev/null +++ b/estate/models/estate_property_user.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class EstatePropertyUser(models.Model): + _inherit = 'res.users' + + property_ids = fields.Many2many( + 'estate.property', string = 'Properties', domain = "[('state', 'in', ['new', 'offer_received'])]" + ) diff --git a/estate/views/estate_res_users_views.xml b/estate/views/estate_res_users_views.xml new file mode 100644 index 00000000000..9f08fbd9027 --- /dev/null +++ b/estate/views/estate_res_users_views.xml @@ -0,0 +1,17 @@ + + + + res.users.view.inherit.show.properties + res.users + + + + + + + + + + + + \ No newline at end of file From 09df5a64698435c19e2b3d49882452f4301a267d Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Mon, 24 Feb 2025 08:10:04 +0100 Subject: [PATCH 18/35] [ADD] estate_account: add invoices functionality create a new module to handle creating invoices when a property is sold. implement the inheritance for extending the action_sell_action and then create a new invoice contains two lines linked to it upon creation. chapter-13 --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 14 ++++++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 28 ++++++++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..1c346467585 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Real Estate Account', + 'version': '1.0', + 'description': 'a model to enable creation of invoices for sold properties', + 'data': [], + 'depends': [ + 'base', + 'estate', + 'account', + ], + 'application': True, + 'installable': True, + 'license': 'LGPL-3', +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ No newline at end of file diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..14b4f33081d --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,28 @@ +from odoo import fields, models, Command + + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + def action_sell_property(self): + for record in self: + record.env["account.move"].create( + { + "partner_id": record.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create({ + "name": "6% of the selling price", + "price_unit": record.selling_price * 0.06, + "quantity": 1, + }), + Command.create({ + "name": "Administration Fee", + "price_unit": 20, + "quantity": 1, + }) + ] + }, + ) + + return super().action_sell_property() From 52690e176a48d94448b780275d9da4115d46d4c2 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Mon, 24 Feb 2025 08:32:20 +0100 Subject: [PATCH 19/35] [IMP] estate: add kanban view for a properties create a kanban view using QWeb templating for properties to achieve better viewing experience. chapter-14 --- estate/views/estate_property_views.xml | 31 +++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3329335da57..6270b020d1d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -87,6 +87,35 @@ + + estate.property.kanban + estate.property + + + + + + + + estate.property.list estate.property @@ -110,7 +139,7 @@ Properties estate.property - list,form + list,form,kanban {'search_default_available': True} From e36a9a8287511115e41e7f5010a475e3bed663bc Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Mon, 24 Feb 2025 15:11:07 +0100 Subject: [PATCH 20/35] [REF] estate: refactor methods for better process refactor create overridden method to process handling batches of records instead of single records. modify kanban view for the estate property to appear. use card instead of kanban-box in kanban view as it is no longer used. move the logic of changing the state of the property to 'offer_received' to the create in offer model instead of onchange in estate_property model for better separation of concerns applying SRP. move the constants for the offer price threshold should be met and the epsilon for floats comparison to a separate file making it central to be used all over the module instead of redefining it locally at many places. --- estate/constants.py | 5 +++++ estate/models/estate_property.py | 21 +++++---------------- estate/models/estate_property_offer.py | 26 ++++++++++++++++---------- estate/views/estate_property_views.xml | 8 ++++---- 4 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 estate/constants.py diff --git a/estate/constants.py b/estate/constants.py new file mode 100644 index 00000000000..abc18e1560a --- /dev/null +++ b/estate/constants.py @@ -0,0 +1,5 @@ +# selling prices for the properties should be at least 90% of the expected price +PROPERTY_SELLING_PRICE_THRESHOLD = 0.9 + +# epsilon to define the rounding precision when comparing floats +PROPERTY_PRICE_PRECISION_EPSILON = 1e-6 diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index c73d2fee214..7c47b2af0dd 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -2,18 +2,14 @@ from odoo.exceptions import UserError from odoo.tools.float_utils import float_compare, float_is_zero +from ..constants import PROPERTY_PRICE_PRECISION_EPSILON, PROPERTY_SELLING_PRICE_THRESHOLD + class EstateProperty(models.Model): _name = 'estate.property' _description = 'model for the properties in our app' _order = 'id desc' - # selling prices for the properties should be at least 90% of the expected price - PROPERTY_SELLING_PRICE_THRESHOLD = 0.9 - - # epsilon to define the rounding precision when comparing floats - PROPERTY_PRICE_PRECISION_EPSILON = 1e-6 - name = fields.Char(required = True) description = fields.Text() postcode = fields.Char() @@ -80,13 +76,6 @@ def _onchange_garden(self): self.garden_orientation = 'north' self.garden_area = 10 - @api.onchange('offer_ids') - def _onchange_offer_ids(self): - if self.offer_ids and self.state == 'new': - self.state = 'offer_received' - elif not self.offer_ids and self.state == 'offer_received': - self.state = 'new' - def action_cancel_property(self): if self.filtered(lambda record: record.state == 'sold'): raise UserError('Sold properties cannot be cancelled') @@ -104,11 +93,11 @@ def action_sell_property(self): @api.constrains('selling_price') def _check_selling_price(self): for record in self: - if float_is_zero(record.selling_price, precision_rounding = self.PROPERTY_PRICE_PRECISION_EPSILON): + if float_is_zero(record.selling_price, precision_rounding = PROPERTY_PRICE_PRECISION_EPSILON): continue if float_compare( - record.selling_price, record.expected_price * self.PROPERTY_SELLING_PRICE_THRESHOLD, - precision_rounding = self.PROPERTY_PRICE_PRECISION_EPSILON + record.selling_price, record.expected_price * PROPERTY_SELLING_PRICE_THRESHOLD, + precision_rounding = PROPERTY_PRICE_PRECISION_EPSILON ) < 0: raise UserError('The selling price must be at least 90% of the the expected price') diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 159f627d446..4f90a7f0d50 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,8 @@ from odoo import api, fields, models from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare, float_is_zero + +from ..constants import PROPERTY_PRICE_PRECISION_EPSILON class EstatePropertyOffer(models.Model): @@ -25,8 +28,7 @@ def _inverse_date_deadline(self): for record in self: if record.create_date and record.date_deadline: create_date = fields.Date.from_string(record.create_date) - deadline_date = fields.Date.from_string(record.date_deadline) - record.validity = (deadline_date - create_date).days + record.validity = (record.date_deadline - create_date).days def action_accept_offer(self): self.ensure_one() @@ -47,14 +49,18 @@ def action_refuse_offer(self): self.status = 'refused' return True - @api.model - def create(self, vals): - property_id_number = vals.get('property_id') - property_id = self.env['estate.property'].browse(property_id_number) - offers = property_id.offer_ids - if offers.filtered(lambda offer: offer.price > vals.get('price')): - raise UserError('The offer price must be at least %s' % max(offers.mapped('price'), default = 0.0)) - return super().create(vals) + @api.model_create_multi + def create(self, vals_list): + property_offers = [vals for vals in vals_list if vals.get('property_id')] + for offer_vals in property_offers: + property_id = self.env['estate.property'].browse(offer_vals.get('property_id')) + max_price = max(property_id.offer_ids.mapped('price'), default = 0.0) + if float_compare( + offer_vals.get('price'), max_price, precision_rounding = PROPERTY_PRICE_PRECISION_EPSILON + ) < 0: + raise UserError('The offer must be at least %s' % max_price) + property_id.state = 'offer_received' + return super().create(vals_list) _sql_constraints = [ ('price_positive', 'CHECK(price > 0)', 'Price must be positive') diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 6270b020d1d..b45464284c3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -93,8 +93,8 @@ - + @@ -139,7 +139,7 @@ Properties estate.property - list,form,kanban + kanban,list,form {'search_default_available': True} From 655c35b93e80b4fa83e5db20ad883d59df78385f Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Mon, 24 Feb 2025 16:05:41 +0100 Subject: [PATCH 21/35] [IMP] awesome_owl: create Counters and Cards create a reusable Counter component to be able to create as many separate counters as needed. create a Card component and use props to pass values through the parent component Playground. use t-out and markup to show the difference between escaped content and non escaped content. chapter-1-part-4 --- awesome_owl/static/src/card/card.js | 9 +++++++++ awesome_owl/static/src/card/card.xml | 9 +++++++++ awesome_owl/static/src/counter/counter.js | 14 ++++++++++++++ awesome_owl/static/src/counter/counter.xml | 9 +++++++++ awesome_owl/static/src/playground.js | 13 +++++++++++-- awesome_owl/static/src/playground.xml | 15 ++++++++++----- 6 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..28bdff7cbaf --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,9 @@ +import { Component } from '@odoo/owl'; + +export class Card extends Component { + static template = 'awesome_owl.Card'; + static props = { + "title": String, + "content": String, + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..d79974cb646 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,9 @@ + + + +
+
+

+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..b0a26b1e390 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,14 @@ +import { Component, useState } from '@odoo/owl'; + +export class Counter extends Component { + static template = 'awesome_owl.Counter'; + static props = {}; + + setup() { + this.state = useState({ value: 0 }); + } + + increment() { + this.state.value++; + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..16bb489fd21 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,9 @@ + + + +

+ Counter: + +

+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..a428a07f135 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,16 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, markup } from '@odoo/owl'; +import { Counter } from './counter/counter'; +import { Card } from './card/card'; export class Playground extends Component { - static template = "awesome_owl.playground"; + static template = 'awesome_owl.playground'; + static components = { Counter, Card }; + static props = {}; + + setup() { + this.escapedValue = "Hello, world!"; + this.notEscapedValue = markup("Hello, world!"); + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..e363a01b282 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,15 @@ - + - -
+
hello world
+
+ + +
+ + +
- - + \ No newline at end of file From 0f9dfa62d0d213eceaa00f4f3582540bcbc0cad4 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Mon, 24 Feb 2025 16:57:33 +0100 Subject: [PATCH 22/35] [IMP] awesome_owl: use callback to add counters define a new state in the playground to hold the summation of the two children counters. bind the onChange function to increase the sum by one to define the context for the function. define the optional prop as an onChange function that is called when the child's state is changed to update the parent's sum. chapter-1-part-6 --- awesome_owl/static/src/counter/counter.js | 8 +++++++- awesome_owl/static/src/playground.js | 10 +++++++++- awesome_owl/static/src/playground.xml | 7 +++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index b0a26b1e390..692c532a89e 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -2,7 +2,12 @@ import { Component, useState } from '@odoo/owl'; export class Counter extends Component { static template = 'awesome_owl.Counter'; - static props = {}; + static props = { + onChange: { + type: Function, + optional: true, + }, + }; setup() { this.state = useState({ value: 0 }); @@ -10,5 +15,6 @@ export class Counter extends Component { increment() { this.state.value++; + this.props.onChange && this.props.onChange(); } } diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index a428a07f135..bd6338e19db 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { Component, markup } from '@odoo/owl'; +import { Component, markup, useState } from '@odoo/owl'; import { Counter } from './counter/counter'; import { Card } from './card/card'; @@ -12,5 +12,13 @@ export class Playground extends Component { setup() { this.escapedValue = "Hello, world!"; this.notEscapedValue = markup("Hello, world!"); + this.countersSum = useState({ + value: 0, + }); + this.increaseSum = this.increaseSum.bind(this); + } + + increaseSum() { + this.countersSum.value++; } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index e363a01b282..05700cacc16 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -5,8 +5,11 @@ hello world
- - +
+ + +

Counters Sum:

+

From dee1da828c82797433e5cecb6ddad43574076a54 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 25 Feb 2025 11:55:03 +0100 Subject: [PATCH 23/35] [IMP] awesome_owl: add a todo list to playground create a todo item component that takes a prop todo and shows its id and description in a card style. add a todolist component that accepts an optional prop todos and keeps a state for the available todos, iterates over them and displays a list of TodoItem. append the todolist to the playground. chapter-1-part-7 --- awesome_owl/static/src/playground.js | 7 +++--- awesome_owl/static/src/playground.xml | 2 ++ awesome_owl/static/src/todolist/todo_item.js | 15 ++++++++++++ awesome_owl/static/src/todolist/todo_item.xml | 11 +++++++++ awesome_owl/static/src/todolist/todo_list.js | 24 +++++++++++++++++++ awesome_owl/static/src/todolist/todo_list.xml | 12 ++++++++++ 6 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 awesome_owl/static/src/todolist/todo_item.js create mode 100644 awesome_owl/static/src/todolist/todo_item.xml create mode 100644 awesome_owl/static/src/todolist/todo_list.js create mode 100644 awesome_owl/static/src/todolist/todo_list.xml diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index bd6338e19db..e752b15dea4 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -3,15 +3,16 @@ import { Component, markup, useState } from '@odoo/owl'; import { Counter } from './counter/counter'; import { Card } from './card/card'; +import { TodoList } from './todolist/todo_list'; export class Playground extends Component { static template = 'awesome_owl.playground'; - static components = { Counter, Card }; + static components = { Counter, Card, TodoList }; static props = {}; setup() { - this.escapedValue = "Hello, world!"; - this.notEscapedValue = markup("Hello, world!"); + this.escapedValue = 'Hello, world!'; + this.notEscapedValue = markup('Hello, world!'); this.countersSum = useState({ value: 0, }); diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 05700cacc16..2f294727753 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -13,6 +13,8 @@
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_item.js b/awesome_owl/static/src/todolist/todo_item.js new file mode 100644 index 00000000000..e1c8468937d --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_item.js @@ -0,0 +1,15 @@ +import { Component } from '@odoo/owl'; + +export class TodoItem extends Component { + static template = 'awesome_owl.TodoItem'; + static props = { + todo: { + type: { + id: Number, + description: String, + isCompleted: Boolean, + }, + optional: false, + }, + }; +} \ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_item.xml b/awesome_owl/static/src/todolist/todo_item.xml new file mode 100644 index 00000000000..10fb247d85d --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_item.xml @@ -0,0 +1,11 @@ + + + +
+
ID:
+
+
Description:
+
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_list.js b/awesome_owl/static/src/todolist/todo_list.js new file mode 100644 index 00000000000..3134495862d --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_list.js @@ -0,0 +1,24 @@ +import { Component, useState } from '@odoo/owl'; +import { TodoItem } from './todo_item'; + +export class TodoList extends Component { + static template = 'awesome_owl.TodoList'; + static components = { TodoItem }; + static props = { + todos: { + type: Array, + optional: true, + }, + }; + + setup() { + this.todos = useState([ + { id: 1, description: 'Learn JavaScript', isCompleted: true }, + { id: 2, description: 'Learn Odoo', isCompleted: false }, + { id: 3, description: 'Learn Owl', isCompleted: true }, + { id: 4, description: 'Learn React', isCompleted: false }, + { id: 5, description: 'Learn Vue', isCompleted: true }, + { id: 6, description: 'Learn Python', isCompleted: false }, + ]); + } +} diff --git a/awesome_owl/static/src/todolist/todo_list.xml b/awesome_owl/static/src/todolist/todo_list.xml new file mode 100644 index 00000000000..7c5e0048464 --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_list.xml @@ -0,0 +1,12 @@ + + + +
+
+
+ +
+
+
+
+
\ No newline at end of file From 7844bfa9704c24e7965eae0a7b7f166c95157b8e Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 25 Feb 2025 14:53:50 +0100 Subject: [PATCH 24/35] [IMP] awesome_owl: enable users to add tasks add an input field that handles the event of key pressing and reacts to them by adding a new task to the todos if they're suitable. implement addTask method in todo_list that handles the process of adding tasks and updating the state. maintain a global variable in the TodoList to handle generating new ids for tasks. chapter-1-part-9 --- awesome_owl/static/src/todolist/todo_item.xml | 10 ++++++- awesome_owl/static/src/todolist/todo_list.js | 29 ++++++++++++++----- awesome_owl/static/src/todolist/todo_list.xml | 1 + 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/awesome_owl/static/src/todolist/todo_item.xml b/awesome_owl/static/src/todolist/todo_item.xml index 10fb247d85d..2a30fdb1412 100644 --- a/awesome_owl/static/src/todolist/todo_item.xml +++ b/awesome_owl/static/src/todolist/todo_item.xml @@ -1,7 +1,15 @@ -
+ +
ID:
Description:
diff --git a/awesome_owl/static/src/todolist/todo_list.js b/awesome_owl/static/src/todolist/todo_list.js index 3134495862d..00471db6d5d 100644 --- a/awesome_owl/static/src/todolist/todo_list.js +++ b/awesome_owl/static/src/todolist/todo_list.js @@ -12,13 +12,26 @@ export class TodoList extends Component { }; setup() { - this.todos = useState([ - { id: 1, description: 'Learn JavaScript', isCompleted: true }, - { id: 2, description: 'Learn Odoo', isCompleted: false }, - { id: 3, description: 'Learn Owl', isCompleted: true }, - { id: 4, description: 'Learn React', isCompleted: false }, - { id: 5, description: 'Learn Vue', isCompleted: true }, - { id: 6, description: 'Learn Python', isCompleted: false }, - ]); + this.todos = useState([]); + this.addTask = this.addTask.bind(this); + this.nextTodoId = this.todos.length + 1; + } + + addTask(ev) { + ev.preventDefault(); + if (ev.key !== 'Enter') { + return; + } + const input = ev.target; + const description = input.value.trim(); + if (!description) { + return; + } + this.todos.push({ + id: this.nextTodoId++, + description, + isCompleted: false, + }); + input.value = ''; } } diff --git a/awesome_owl/static/src/todolist/todo_list.xml b/awesome_owl/static/src/todolist/todo_list.xml index 7c5e0048464..794e24d580e 100644 --- a/awesome_owl/static/src/todolist/todo_list.xml +++ b/awesome_owl/static/src/todolist/todo_list.xml @@ -1,6 +1,7 @@ +
From cbe834f7e49585cde2d1c27f505c53368c3d9b99 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 25 Feb 2025 15:21:08 +0100 Subject: [PATCH 25/35] [IMP] awesome_owl: add autofocus to task input use reference to access the input field for the tasks and implement an autofocus feature when on mounting the component. extract the autofocus feature into a separate custom hook under src/utils.js. chapter-1-part-10 --- awesome_owl/static/src/todolist/todo_list.js | 2 ++ awesome_owl/static/src/todolist/todo_list.xml | 6 +++++- awesome_owl/static/src/utils.js | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/todolist/todo_list.js b/awesome_owl/static/src/todolist/todo_list.js index 00471db6d5d..053dd4b2de0 100644 --- a/awesome_owl/static/src/todolist/todo_list.js +++ b/awesome_owl/static/src/todolist/todo_list.js @@ -1,5 +1,6 @@ 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'; @@ -15,6 +16,7 @@ export class TodoList extends Component { this.todos = useState([]); this.addTask = this.addTask.bind(this); this.nextTodoId = this.todos.length + 1; + useAutoFocus('add_task_input'); } addTask(ev) { diff --git a/awesome_owl/static/src/todolist/todo_list.xml b/awesome_owl/static/src/todolist/todo_list.xml index 794e24d580e..637c692c812 100644 --- a/awesome_owl/static/src/todolist/todo_list.xml +++ b/awesome_owl/static/src/todolist/todo_list.xml @@ -1,7 +1,11 @@ - +
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..d88b6f1a6cd --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,8 @@ +import { onMounted, useRef } from '@odoo/owl'; + +export const useAutoFocus = (inputFieldRefName) => { + const addTaskInputRef = useRef(inputFieldRefName); + onMounted(() => { + addTaskInputRef.el.focus(); + }); +} \ No newline at end of file From eaf7030037890a939452cde01eb37621bdeba443 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 25 Feb 2025 15:49:07 +0100 Subject: [PATCH 26/35] [IMP] awesome_owl: add task toggling functionality add the functionality of toggling a task and marking it as completed. use a callback prop to implement the communication between the child TodoItem and parent TodoList chapter-1-part-11 --- awesome_owl/static/src/todolist/todo_item.js | 4 ++++ awesome_owl/static/src/todolist/todo_item.xml | 15 ++++++++++++--- awesome_owl/static/src/todolist/todo_list.js | 6 ++++++ awesome_owl/static/src/todolist/todo_list.xml | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/awesome_owl/static/src/todolist/todo_item.js b/awesome_owl/static/src/todolist/todo_item.js index e1c8468937d..12c70f384ce 100644 --- a/awesome_owl/static/src/todolist/todo_item.js +++ b/awesome_owl/static/src/todolist/todo_item.js @@ -11,5 +11,9 @@ export class TodoItem extends Component { }, optional: false, }, + toggleTodo: { + type: Function, + optional: false, + }, }; } \ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_item.xml b/awesome_owl/static/src/todolist/todo_item.xml index 2a30fdb1412..a06ec50df03 100644 --- a/awesome_owl/static/src/todolist/todo_item.xml +++ b/awesome_owl/static/src/todolist/todo_item.xml @@ -10,9 +10,18 @@ }" style="max-width: 18rem;" > -
ID:
-
-
Description:
+
+ +
+ ID: +
+
+
+
diff --git a/awesome_owl/static/src/todolist/todo_list.js b/awesome_owl/static/src/todolist/todo_list.js index 053dd4b2de0..547fa509406 100644 --- a/awesome_owl/static/src/todolist/todo_list.js +++ b/awesome_owl/static/src/todolist/todo_list.js @@ -17,6 +17,7 @@ export class TodoList extends Component { this.addTask = this.addTask.bind(this); this.nextTodoId = this.todos.length + 1; useAutoFocus('add_task_input'); + this.toggleTodo = this.toggleTodo.bind(this); } addTask(ev) { @@ -36,4 +37,9 @@ export class TodoList extends Component { }); input.value = ''; } + + toggleTodo(todoId) { + const toBeToggled = this.todos.find(todo => todo.id === todoId); + toBeToggled.isCompleted = !toBeToggled.isCompleted; + } } diff --git a/awesome_owl/static/src/todolist/todo_list.xml b/awesome_owl/static/src/todolist/todo_list.xml index 637c692c812..bf0c482c21d 100644 --- a/awesome_owl/static/src/todolist/todo_list.xml +++ b/awesome_owl/static/src/todolist/todo_list.xml @@ -9,7 +9,7 @@
- +
From 63e37933dcd7bf4db4938805ad020001dc751b1c Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 25 Feb 2025 16:17:35 +0100 Subject: [PATCH 27/35] [IMP] awesome_owl: enable removing todos add callback prop to invoke re-renders on clicking the remove icon. change the conditional decoration to be applied only on the body of the card instead of applying it globally on the card. add the remove icon to the card header. change the header display to flex for better alignment for the elements. chapter-1-part-12 --- awesome_owl/static/src/todolist/todo_item.js | 4 ++++ awesome_owl/static/src/todolist/todo_item.xml | 19 +++++++++++-------- awesome_owl/static/src/todolist/todo_list.js | 6 ++++++ awesome_owl/static/src/todolist/todo_list.xml | 2 +- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/awesome_owl/static/src/todolist/todo_item.js b/awesome_owl/static/src/todolist/todo_item.js index 12c70f384ce..a8f3fc6a611 100644 --- a/awesome_owl/static/src/todolist/todo_item.js +++ b/awesome_owl/static/src/todolist/todo_item.js @@ -15,5 +15,9 @@ export class TodoItem extends Component { type: Function, optional: false, }, + removeTodo: { + type: Function, + optional: false, + }, }; } \ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_item.xml b/awesome_owl/static/src/todolist/todo_item.xml index a06ec50df03..566952279b9 100644 --- a/awesome_owl/static/src/todolist/todo_item.xml +++ b/awesome_owl/static/src/todolist/todo_item.xml @@ -1,26 +1,29 @@ -
-
+
+ -
+
ID:
-
+
diff --git a/awesome_owl/static/src/todolist/todo_list.js b/awesome_owl/static/src/todolist/todo_list.js index 547fa509406..1949bd8dee9 100644 --- a/awesome_owl/static/src/todolist/todo_list.js +++ b/awesome_owl/static/src/todolist/todo_list.js @@ -18,6 +18,7 @@ export class TodoList extends Component { this.nextTodoId = this.todos.length + 1; useAutoFocus('add_task_input'); this.toggleTodo = this.toggleTodo.bind(this); + this.removeTodo = this.removeTodo.bind(this); } addTask(ev) { @@ -42,4 +43,9 @@ export class TodoList extends Component { const toBeToggled = this.todos.find(todo => todo.id === todoId); toBeToggled.isCompleted = !toBeToggled.isCompleted; } + + removeTodo(todoId) { + const toBeRemovedIndex = this.todos.findIndex(todo => todo.id === todoId); + this.todos.splice(toBeRemovedIndex, 1); + } } diff --git a/awesome_owl/static/src/todolist/todo_list.xml b/awesome_owl/static/src/todolist/todo_list.xml index bf0c482c21d..d0ba75abe28 100644 --- a/awesome_owl/static/src/todolist/todo_list.xml +++ b/awesome_owl/static/src/todolist/todo_list.xml @@ -9,7 +9,7 @@
- +
From fe7df59d05c61f05689bdaeaacca09842d825cec Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 25 Feb 2025 16:56:09 +0100 Subject: [PATCH 28/35] [IMP] awesome_owl: enable dynamic card content change the Card component to be able to accept dynamic content as default slot props. add prop validation to the Card. chapter-1-part-13 --- awesome_owl/static/src/card/card.js | 10 ++++++++-- awesome_owl/static/src/card/card.xml | 2 +- awesome_owl/static/src/playground.xml | 8 ++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index 28bdff7cbaf..d0e5551210f 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -3,7 +3,13 @@ import { Component } from '@odoo/owl'; export class Card extends Component { static template = 'awesome_owl.Card'; static props = { - "title": String, - "content": String, + title: { + type: String, + optional: false, + }, + slots: { + type: Object, + optional: true, + } } } \ 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 d79974cb646..e914a71223d 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -3,7 +3,7 @@
-

+
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 2f294727753..5ed6ca8516b 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -11,8 +11,12 @@

Counters Sum:


- - + +

Card content 1

+
+ + +
From 7562113c5aeb83b0bdfe199c92fcc247a53772f5 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Tue, 25 Feb 2025 17:13:45 +0100 Subject: [PATCH 29/35] [IMP] awesome_owl: add a fold toggle to card enable folding and unfolding the card depending on the state of the button 'toggle'. redefine the structure for better styling support. group the cards together to better align them. chapter-1-part-14 --- awesome_owl/static/src/card/card.js | 13 ++++++++++++- awesome_owl/static/src/card/card.xml | 11 ++++++++--- awesome_owl/static/src/playground.xml | 14 ++++++++------ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index d0e5551210f..460c13a3583 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -1,4 +1,4 @@ -import { Component } from '@odoo/owl'; +import { Component, useState } from '@odoo/owl'; export class Card extends Component { static template = 'awesome_owl.Card'; @@ -12,4 +12,15 @@ export class Card extends Component { optional: true, } } + + setup() { + this.isBodyOpen = useState({ + value: true, + }); + this.toggleBodyOpen = this.toggleBodyOpen.bind(this); + } + + toggleBodyOpen() { + this.isBodyOpen.value = !this.isBodyOpen.value; + } } \ 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 e914a71223d..4a861f67ff9 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -1,9 +1,14 @@ -
-
- +
+
+

+ +
+
+ +
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 5ed6ca8516b..ce41f52afca 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -11,12 +11,14 @@

Counters Sum:


- -

Card content 1

-
- - - +
+ +

Card content 1

+
+ + + +

From b74064f3ea6f8219b3624eb9136832578c9cbfa0 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Wed, 26 Feb 2025 15:44:58 +0100 Subject: [PATCH 30/35] [IMP] awesome_dashboard: create dashboard stats create dashboard and dashboard item components. use service and rpc to show customers, leads, and call the server to get stats data. use slots to enable defining the dashboard item dynamically. chapter-2-part-4 --- awesome_dashboard/static/src/dashboard.js | 44 ++++++++++++++++-- awesome_dashboard/static/src/dashboard.scss | 5 ++ awesome_dashboard/static/src/dashboard.xml | 46 +++++++++++++++++-- .../src/dashboard_item/dashboard_item.js | 14 ++++++ .../src/dashboard_item/dashboard_item.xml | 16 +++++++ 5 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard_item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard_item/dashboard_item.xml diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 637fa4bb972..f2211fa493a 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,10 +1,46 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; +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 { DashboardItem } from './dashboard_item/dashboard_item'; +import { rpc } from '@web/core/network/rpc'; class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; + static template = 'awesome_dashboard.AwesomeDashboard'; + static components = { Layout, DashboardItem }; + + setup() { + this.showCustomers = this.showCustomers.bind(this); + this.showLeads = this.showLeads.bind(this); + this.action = useService("action"); + this.stats = useState({}); + onWillStart(async () => { + const result = await rpc('/awesome_dashboard/statistics', {}); + this.stats = { ...result }; + console.log(this.stats); + }); + } + + showCustomers() { + this.action.doAction('base.action_partner_form'); + } + + showLeads() { + this.action.doAction({ + type: 'ir.actions.act_window', + name: 'Leads', + target: 'current', + res_model: 'crm.lead', + views: [ + [false, 'list'], + [false, 'form'], + ], + }); + } } -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); +registry + .category('actions') + .add('awesome_dashboard.dashboard', AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss new file mode 100644 index 00000000000..2e0c3d6162d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.scss @@ -0,0 +1,5 @@ +.o_dashboard { + background-color: gray; + font-weight: bold; + font-size: 1.5rem; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 1a2ac9a2fed..0afb27d414a 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -1,8 +1,44 @@ - + - - hello dashboard + +

some content

+ + + + +
+ + Number of new orders this month + + + + + + Total amount of new orders this month + + + + + + Average amount of t-shirt by order this month + + + + + + Number of cancelled orders this month + + + + + + Average time for an order to go from 'new' to 'sent' or 'cancelled' + + + + +
+
- -
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..a1cbca1a4a8 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js @@ -0,0 +1,14 @@ +import { Component } from '@odoo/owl'; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static props = { + size: { + type: Number, + optional: true, + } + }; + static defaultProps = { + size: 1, + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..fd7fb9c4343 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml @@ -0,0 +1,16 @@ + + + +
+
+ +
+
+ +
+
+
+
\ No newline at end of file From d53317f3e6f530827ef72c18d6eea39ed90dee34 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Wed, 26 Feb 2025 17:02:33 +0100 Subject: [PATCH 31/35] [IMP] awesome_dashboard: cache network calls create a new service to load statistics instead of doing it in the dashboard component. use memoize to cache the results of the function. chapter-2-part-5 --- awesome_dashboard/static/src/dashboard.js | 5 ++--- .../static/src/dashboard_service.js | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard_service.js diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index f2211fa493a..af2344134d2 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -5,7 +5,6 @@ import { registry } from '@web/core/registry'; import { Layout } from '@web/search/layout'; import { useService } from '@web/core/utils/hooks'; import { DashboardItem } from './dashboard_item/dashboard_item'; -import { rpc } from '@web/core/network/rpc'; class AwesomeDashboard extends Component { static template = 'awesome_dashboard.AwesomeDashboard'; @@ -16,9 +15,9 @@ class AwesomeDashboard extends Component { this.showLeads = this.showLeads.bind(this); this.action = useService("action"); this.stats = useState({}); + this.service = useService('awesome_dashboard.statistics'); onWillStart(async () => { - const result = await rpc('/awesome_dashboard/statistics', {}); - this.stats = { ...result }; + this.stats = await this.service.loadStatisitcs(); console.log(this.stats); }); } diff --git a/awesome_dashboard/static/src/dashboard_service.js b/awesome_dashboard/static/src/dashboard_service.js new file mode 100644 index 00000000000..97674f986ff --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_service.js @@ -0,0 +1,16 @@ +import { registry } from '@web/core/registry'; +import { rpc } from '@web/core/network/rpc'; +import { memoize } from '@web/core/utils/functions'; + +const dashboardService = { + start() { + return { + loadStatisitcs: memoize(async () => { + const result = await rpc('/awesome_dashboard/statistics', {}); + return { ...result }; + }), + } + } +} + +registry.category('services').add('awesome_dashboard.statistics', dashboardService); \ No newline at end of file From 86fcb08953c0276f21bb06a83dc61a95a6fc278a Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Thu, 27 Feb 2025 12:51:34 +0100 Subject: [PATCH 32/35] [IMP] awesome_dashboard: create tshirt sizes chart create a PieChart to view sales of t-shirts organized by size. use loadJS to lazy load the chart library in order to enhance performance. chapter-2-part-6 --- awesome_dashboard/static/src/dashboard.js | 3 +- awesome_dashboard/static/src/dashboard.xml | 6 ++ .../static/src/pie_chart/pie_chart.js | 65 +++++++++++++++++++ .../static/src/pie_chart/pie_chart.xml | 10 +++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 awesome_dashboard/static/src/pie_chart/pie_chart.js create mode 100644 awesome_dashboard/static/src/pie_chart/pie_chart.xml diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index af2344134d2..7e326d18711 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -5,10 +5,11 @@ import { registry } from '@web/core/registry'; import { Layout } from '@web/search/layout'; import { useService } from '@web/core/utils/hooks'; import { DashboardItem } from './dashboard_item/dashboard_item'; +import { PieChart } from './pie_chart/pie_chart'; class AwesomeDashboard extends Component { static template = 'awesome_dashboard.AwesomeDashboard'; - static components = { Layout, DashboardItem }; + static components = { Layout, DashboardItem, PieChart }; setup() { this.showCustomers = this.showCustomers.bind(this); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 0afb27d414a..a216cfdd451 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -38,6 +38,12 @@ + + T-shirt sales by size + + + +
diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.js b/awesome_dashboard/static/src/pie_chart/pie_chart.js new file mode 100644 index 00000000000..a1d9e6f1898 --- /dev/null +++ b/awesome_dashboard/static/src/pie_chart/pie_chart.js @@ -0,0 +1,65 @@ +import { + Component, + onWillStart, + useRef, + onMounted, + onWillUnmount, +} from '@odoo/owl'; +import { loadJS } from '@web/core/assets'; +import { getColor } from '@web/core/colors/colors'; + +export class PieChart extends Component { + static template = 'awesome_dashboard.PieChart'; + static props = { + label: { + type: String, + optional: true, + }, + data: { + type: Object, + optional: true, + }, + }; + + setup() { + this.canvasRef = useRef('canvas'); + this.chart = null; + this.renderChart = this.renderChart.bind(this); + this.getChartData = this.getChartData.bind(this); + onWillStart(() => loadJS('/web/static/lib/Chart/Chart.js')); + onWillUnmount(() => { + if (this.chart) { + this.chart.destroy(); + } + }); + onMounted(() => { + this.renderChart(); + }); + } + + getChartData() { + const labels = Object.keys(this.props.data); + const data = Object.values(this.props.data); + const colors = labels.map((_, index) => getColor(index * 2 + 1)); + const chartData = { + labels, + datasets: [ + { + label: this.props.label, + data, + backgroundColor: colors, + hoverOffset: 1, + }, + ], + }; + return chartData; + } + + renderChart() { + const config = { + type: 'pie', + data: this.getChartData(), + }; + this.chart = new Chart(this.canvasRef.el, config); + } +} diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..eda87fabf12 --- /dev/null +++ b/awesome_dashboard/static/src/pie_chart/pie_chart.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
\ No newline at end of file From e4703dbcb0db792edcec2fde45f9e39436ca2e73 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Thu, 27 Feb 2025 16:04:46 +0100 Subject: [PATCH 33/35] [IMP] awesome_dashboard: add live updates refactor dashboard serviece to add live updates for the dashboard using reactive components. use lazy components to lazy load the dashboard on demand only. chapter-2-part-8 --- awesome_dashboard/__manifest__.py | 4 +++ .../static/src/{ => dashboard}/dashboard.js | 15 ++++------- .../static/src/{ => dashboard}/dashboard.scss | 0 .../static/src/{ => dashboard}/dashboard.xml | 3 +-- .../dashboard_item/dashboard_item.js | 4 +++ .../dashboard_item/dashboard_item.xml | 0 .../static/src/dashboard/dashboard_service.js | 27 +++++++++++++++++++ .../static/src/dashboard_action.js | 14 ++++++++++ .../static/src/dashboard_service.js | 16 ----------- .../static/src/pie_chart/pie_chart.js | 15 +++++------ 10 files changed, 61 insertions(+), 37 deletions(-) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.js (70%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.scss (100%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.xml (96%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard_item/dashboard_item.js (78%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard_item/dashboard_item.xml (100%) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_service.js create mode 100644 awesome_dashboard/static/src/dashboard_action.js delete mode 100644 awesome_dashboard/static/src/dashboard_service.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..7d44af2a4ec 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -24,7 +24,11 @@ 'assets': { 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', + ('remove', 'awesome_dashboard/static/src/dashboard/**/*'), ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', + ] }, 'license': 'AGPL-3' } diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js similarity index 70% rename from awesome_dashboard/static/src/dashboard.js rename to awesome_dashboard/static/src/dashboard/dashboard.js index 7e326d18711..a6d04b70d42 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -5,7 +5,7 @@ import { registry } from '@web/core/registry'; import { Layout } from '@web/search/layout'; import { useService } from '@web/core/utils/hooks'; import { DashboardItem } from './dashboard_item/dashboard_item'; -import { PieChart } from './pie_chart/pie_chart'; +import { PieChart } from '../pie_chart/pie_chart'; class AwesomeDashboard extends Component { static template = 'awesome_dashboard.AwesomeDashboard'; @@ -14,13 +14,8 @@ class AwesomeDashboard extends Component { setup() { this.showCustomers = this.showCustomers.bind(this); this.showLeads = this.showLeads.bind(this); - this.action = useService("action"); - this.stats = useState({}); - this.service = useService('awesome_dashboard.statistics'); - onWillStart(async () => { - this.stats = await this.service.loadStatisitcs(); - console.log(this.stats); - }); + this.action = useService('action'); + this.stats = useState(useService('awesome_dashboard.statistics')); } showCustomers() { @@ -42,5 +37,5 @@ class AwesomeDashboard extends Component { } registry - .category('actions') - .add('awesome_dashboard.dashboard', AwesomeDashboard); + .category('lazy_components') + .add('AwesomeDashboard', AwesomeDashboard); 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 96% rename from awesome_dashboard/static/src/dashboard.xml rename to awesome_dashboard/static/src/dashboard/dashboard.xml index a216cfdd451..6561a453be2 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -2,12 +2,11 @@ -

some content

-
+
Number of new orders this month diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js similarity index 78% rename from awesome_dashboard/static/src/dashboard_item/dashboard_item.js rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js index a1cbca1a4a8..84e1db91491 100644 --- a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -6,6 +6,10 @@ export class DashboardItem extends Component { size: { type: Number, optional: true, + }, + slots: { + type: Object, + optional: true, } }; static defaultProps = { diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml similarity index 100% rename from awesome_dashboard/static/src/dashboard_item/dashboard_item.xml rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml diff --git a/awesome_dashboard/static/src/dashboard/dashboard_service.js b/awesome_dashboard/static/src/dashboard/dashboard_service.js new file mode 100644 index 00000000000..041b2dac15a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_service.js @@ -0,0 +1,27 @@ +import { registry } from '@web/core/registry'; +import { rpc } from '@web/core/network/rpc'; +import { reactive } from '@odoo/owl'; + +const dashboardService = { + start() { + let stats = reactive({ + isReady: false, + }); + + async function loadData() { + const result = await rpc('/awesome_dashboard/statistics'); + Object.assign(stats, result, { isReady: true }); + setTimeout(() => { + loadData(); + }, 1000 * 10); + } + + loadData(); + + return stats; + }, +}; + +registry + .category('services') + .add('awesome_dashboard.statistics', dashboardService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..e75bb2bb47e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +import { registry } from '@web/core/registry'; +import { LazyComponent } from '@web/core/assets'; +import { Component, xml } from '@odoo/owl'; + +class AwesomeDashboardLoader extends Component { + static template = xml` + + `; + static components = { LazyComponent }; +} + +registry + .category('actions') + .add('awesome_dashboard.dashboard', AwesomeDashboardLoader); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_service.js b/awesome_dashboard/static/src/dashboard_service.js deleted file mode 100644 index 97674f986ff..00000000000 --- a/awesome_dashboard/static/src/dashboard_service.js +++ /dev/null @@ -1,16 +0,0 @@ -import { registry } from '@web/core/registry'; -import { rpc } from '@web/core/network/rpc'; -import { memoize } from '@web/core/utils/functions'; - -const dashboardService = { - start() { - return { - loadStatisitcs: memoize(async () => { - const result = await rpc('/awesome_dashboard/statistics', {}); - return { ...result }; - }), - } - } -} - -registry.category('services').add('awesome_dashboard.statistics', dashboardService); \ No newline at end of file diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.js b/awesome_dashboard/static/src/pie_chart/pie_chart.js index a1d9e6f1898..a25bd35d918 100644 --- a/awesome_dashboard/static/src/pie_chart/pie_chart.js +++ b/awesome_dashboard/static/src/pie_chart/pie_chart.js @@ -11,14 +11,8 @@ import { getColor } from '@web/core/colors/colors'; export class PieChart extends Component { static template = 'awesome_dashboard.PieChart'; static props = { - label: { - type: String, - optional: true, - }, - data: { - type: Object, - optional: true, - }, + label: String, + data: Object, }; setup() { @@ -40,7 +34,7 @@ export class PieChart extends Component { getChartData() { const labels = Object.keys(this.props.data); const data = Object.values(this.props.data); - const colors = labels.map((_, index) => getColor(index * 2 + 1)); + const colors = labels.map((_, index) => getColor(2 ** (index + 2))); const chartData = { labels, datasets: [ @@ -56,6 +50,9 @@ export class PieChart extends Component { } renderChart() { + if(this.chart) { + this.chart.destroy(); + } const config = { type: 'pie', data: this.getChartData(), From 2d6a646a38b5e9d9d1724881a8bc5e0ac67d7088 Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Thu, 27 Feb 2025 17:08:51 +0100 Subject: [PATCH 34/35] [IMP] awesome_dashboard: make the dashboard generic add an items file containing the definition of each component of the dashboard items. genericly use a foreach to render eash of these items with the appropriate data in new dashboard items. create two card components to specify whether the item being renderd is a number card or a pie chart card. chapter-2-part-9 --- .../static/src/dashboard/dashboard.js | 7 ++- .../static/src/dashboard/dashboard.xml | 42 ++----------- .../dashboard_item/dashboard_item.xml | 2 +- .../static/src/dashboard/dashboard_items.js | 60 +++++++++++++++++++ .../src/dashboard/number_card/number_card.js | 13 ++++ .../src/dashboard/number_card/number_card.xml | 9 +++ .../pie_chart_card/pie_chart_card.js | 15 +++++ .../pie_chart_card/pie_chart_card.xml | 7 +++ 8 files changed, 115 insertions(+), 40 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index a6d04b70d42..bcca8311a72 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -1,21 +1,22 @@ /** @odoo-module **/ -import { Component, onWillStart, useState } from '@odoo/owl'; +import { Component, useState } from '@odoo/owl'; import { registry } from '@web/core/registry'; import { Layout } from '@web/search/layout'; import { useService } from '@web/core/utils/hooks'; import { DashboardItem } from './dashboard_item/dashboard_item'; -import { PieChart } from '../pie_chart/pie_chart'; +import { items } from './dashboard_items'; class AwesomeDashboard extends Component { static template = 'awesome_dashboard.AwesomeDashboard'; - static components = { Layout, DashboardItem, PieChart }; + static components = { Layout, DashboardItem }; setup() { this.showCustomers = this.showCustomers.bind(this); this.showLeads = this.showLeads.bind(this); this.action = useService('action'); this.stats = useState(useService('awesome_dashboard.statistics')); + this.items = items; } showCustomers() { diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml index 6561a453be2..e15bee71c51 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -7,42 +7,12 @@
- - Number of new orders this month - - - - - - Total amount of new orders this month - - - - - - Average amount of t-shirt by order this month - - - - - - Number of cancelled orders this month - - - - - - Average time for an order to go from 'new' to 'sent' or 'cancelled' - - - - - - T-shirt sales by size - - - - + + + + + +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml index fd7fb9c4343..e18b7cd916c 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -8,7 +8,7 @@
-
+
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..577658fc8ba --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,60 @@ +import { NumberCard } from './number_card/number_card'; +import { PieChartCard } from './pie_chart_card/pie_chart_card'; + +export const items = [ + { + id: 'average_quantity', + description: 'Average amount of t-shirt by order this month', + Component: NumberCard, + props: (data) => ({ + title: 'Average amount of t-shirt by order this month', + value: data.average_quantity, + }), + }, + { + id: 'average_time', + description: 'Average time for an order', + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time, + }), + }, + { + id: 'number_new_orders', + description: 'New orders this month', + Component: NumberCard, + props: (data) => ({ + title: 'Number of new orders this month', + value: data.nb_new_orders, + }), + }, + { + id: 'cancelled_orders', + description: 'Cancelled orders this month', + Component: NumberCard, + props: (data) => ({ + title: 'Number of cancelled orders this month', + value: data.nb_cancelled_orders, + }), + }, + { + id: 'amount_new_orders', + description: 'amount orders this month', + Component: NumberCard, + props: (data) => ({ + title: 'Total amount of new orders this month', + value: data.total_amount, + }), + }, + { + id: 'pie_chart', + description: 'Shirt orders by size', + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: 'Shirt orders by size', + values: data.orders_by_size, + }), + }, +]; diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..ff4a68acf06 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,13 @@ +import { Component } from '@odoo/owl'; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { + type: String, + }, + value: { + type: Number, + } + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..3b67b1a3a1c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,9 @@ + + + + +
+ +
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js new file mode 100644 index 00000000000..142416833be --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js @@ -0,0 +1,15 @@ +import { Component } from '@odoo/owl'; +import { PieChart } from '../../pie_chart/pie_chart'; + +export class PieChartCard extends Component { + static template = 'awesome_dashboard.PieChartCard'; + static components = { PieChart }; + static props = { + title: { + type: String, + }, + values: { + type: Object, + }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml new file mode 100644 index 00000000000..16f8b721330 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From fa6692ac8bbe3a351ae99bdb38fc931712b5aebf Mon Sep 17 00:00:00 2001 From: Ahmed Gamal Date: Thu, 27 Feb 2025 17:51:01 +0100 Subject: [PATCH 35/35] [IMP] awesome_dashboard: customize dashboard add a dialog and a settings option to enable editing what is visible in the dashboard by the user. use localStorage to store the settings of the user. create configurations to enable setting the view easily. chapter-2-part-11 --- .../static/src/dashboard/dashboard.js | 66 +++++++++++++++++-- .../static/src/dashboard/dashboard.xml | 25 ++++++- .../static/src/dashboard/dashboard_items.js | 7 +- 3 files changed, 90 insertions(+), 8 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index bcca8311a72..1bf2521fc7e 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -5,7 +5,9 @@ import { registry } from '@web/core/registry'; import { Layout } from '@web/search/layout'; import { useService } from '@web/core/utils/hooks'; import { DashboardItem } from './dashboard_item/dashboard_item'; -import { items } from './dashboard_items'; +import { Dialog } from '@web/core/dialog/dialog'; +import { CheckBox } from '@web/core/checkbox/checkbox'; +import { browser } from '@web/core/browser/browser'; class AwesomeDashboard extends Component { static template = 'awesome_dashboard.AwesomeDashboard'; @@ -16,7 +18,29 @@ class AwesomeDashboard extends Component { this.showLeads = this.showLeads.bind(this); this.action = useService('action'); this.stats = useState(useService('awesome_dashboard.statistics')); - this.items = items; + this.items = registry.category('awesome_dashboard').getAll(); + this.display = { + controlPanel: {}, + }; + this.dialog = useService('dialog'); + this.state = useState({ + disabledItems: + browser.localStorage + .getItem('disabledDashboardItems') + ?.split(',') || [], + }); + } + + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }); + } + + updateConfiguration(newDisabledItems) { + this.state.disabledItems = newDisabledItems; } showCustomers() { @@ -37,6 +61,38 @@ class AwesomeDashboard extends Component { } } -registry - .category('lazy_components') - .add('AwesomeDashboard', AwesomeDashboard); +class ConfigurationDialog extends Component { + static template = 'awesome_dashboard.ConfigurationDialog'; + static components = { Dialog, CheckBox }; + static props = ['close', 'items', 'disabledItems', 'onUpdateConfiguration']; + + setup() { + this.items = useState( + this.props.items.map((item) => { + return { + ...item, + checked: !this.props.disabledItems.includes(item.id), + }; + }) + ); + } + + done() { + this.props.close(); + } + + onChange(checked, changedItem) { + changedItem.enabled = checked; + const newDisabledItems = Object.values(this.items) + .filter((item) => !item.enabled) + .map((item) => item.id); + + browser.localStorage.setItem( + 'disabledDashboardItems', + JSON.stringify(newDisabledItems) + ); + this.props.onUpdateConfiguration(newDisabledItems); + } +} + +registry.category('lazy_components').add('AwesomeDashboard', AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml index e15bee71c51..b395fdd6427 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -1,14 +1,19 @@ - + + + +
- + @@ -16,4 +21,20 @@
+ + + + Which cards do you wish to see? + + + + + + + + + +
\ 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 577658fc8ba..00a8662f39e 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_items.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -1,7 +1,8 @@ import { NumberCard } from './number_card/number_card'; import { PieChartCard } from './pie_chart_card/pie_chart_card'; +import { registry } from '@web/core/registry'; -export const items = [ +const items = [ { id: 'average_quantity', description: 'Average amount of t-shirt by order this month', @@ -58,3 +59,7 @@ export const items = [ }), }, ]; + +items.forEach((item) => { + registry.category('awesome_dashboard').add(item.id, item); +});