From a6ef0c08a8ab0380b288d14ab582969b48de662f Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Tue, 10 Mar 2026 18:38:02 +0530 Subject: [PATCH 01/26] [ADD] Estate: Chapter 2 & 3 Initial setup completed for creating a new estate module. Created the base estate module and installed it. Initialized model and model fields which generates tables using odoo ORM. --- estate/__init__.py | 1 + estate/__manifest__.py | 6 ++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 37 ++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..6044f1059fb --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,6 @@ +{ + 'name': 'Real Estate', + 'depends': [ + 'base', + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..2d75caffdb3 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,37 @@ +from odoo import fields, models + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date(copy=False, default=lambda self: fields.Datetime.today()) + 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='Orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + help="Direction the garden faces" + ) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Canceled'), + ], + string='Status', + required=True, + copy=False, + default='new', + ) \ No newline at end of file From 09c816f73cb4f6c23a3d49c1e094d2f832b21c9f Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Wed, 11 Mar 2026 17:50:44 +0530 Subject: [PATCH 02/26] [IMP] Estate: Fixed issues and completed Chapter 4 Fixed issues and errors raised by the first push. Defined access rights by creating security/ir.model.access.csv file in estate. Defined the csv file in manifest. --- estate/__manifest__.py | 7 +++++++ estate/models/estate_property.py | 25 +++++++------------------ estate/security/ir.model.access.csv | 2 ++ 3 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6044f1059fb..73d780531bd 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,13 @@ { 'name': 'Real Estate', + 'author': 'Aditi (adpaw)', + 'license': 'LGPL-3', 'depends': [ 'base', ], + 'data': [ + 'security/ir.model.access.csv', + ], + 'installable': True, + 'application': True, } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2d75caffdb3..a7480d61211 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ from odoo import fields, models + class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property" @@ -7,10 +8,10 @@ class EstateProperty(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date(copy=False, default=lambda self: fields.Datetime.today()) + date_availability = fields.Date expected_price = fields.Float(required=True) - selling_price = fields.Float(readonly=True, copy=False) - bedrooms = fields.Integer(default=2) + selling_price = fields.Float + bedrooms = fields.Integer living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() @@ -18,20 +19,8 @@ class EstateProperty(models.Model): garden_area = fields.Integer() garden_orientation = fields.Selection( string='Orientation', - selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + selection=[ + ('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], help="Direction the garden faces" + ) - active = fields.Boolean(default=True) - state = fields.Selection( - selection=[ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('canceled', 'Canceled'), - ], - string='Status', - required=True, - copy=False, - default='new', - ) \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..976b61e8cb3 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From 85a7c032a38e10c84ac2e5012a2cfb51ae6a63a4 Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Thu, 12 Mar 2026 18:40:53 +0530 Subject: [PATCH 03/26] [IMP] Estate: Completed 2 sections in chapter-5 Added actions and menus in the module. Created estate_property_views.xml for the window action. Added estate_property_menus.xml to define the 3-level menu structure. Registered new XML files in the manifest data list. --- estate/__manifest__.py | 2 ++ estate/views/estate_property_menus.xml | 7 +++++++ estate/views/estate_property_views.xml | 7 +++++++ 3 files changed, 16 insertions(+) create mode 100644 estate/views/estate_property_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 73d780531bd..1f67a75e150 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,6 +7,8 @@ ], 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_menus.xml', ], 'installable': True, 'application': True, diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml new file mode 100644 index 00000000000..1c6d53ac7bd --- /dev/null +++ b/estate/views/estate_property_menus.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..4d544c18597 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,7 @@ + + + Properties + estate.property + list,form + + From 94a540ef9e8162c1456f1c8630ae2b65484bd433 Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Fri, 13 Mar 2026 19:11:46 +0530 Subject: [PATCH 04/26] [IMP] Estate: Completed chapter 5 Implemented fields, attributes and views. Configure field attributes: 'readonly' for selling_price and 'copy=False' where needed. set default value for bedrooms and dynamic default for availability date. Implement reserved fields: 'active' and 'state' --- awesome_gallery/models/ir_action.py | 2 +- estate/models/estate_property.py | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/awesome_gallery/models/ir_action.py b/awesome_gallery/models/ir_action.py index eae20acbf5c..8cf0277e0ff 100644 --- a/awesome_gallery/models/ir_action.py +++ b/awesome_gallery/models/ir_action.py @@ -5,6 +5,6 @@ class ActWindowView(models.Model): _inherit = 'ir.actions.act_window.view' - view_mode = fields.Selection(selection_add=[ + .Selection(selection_add=[ ('gallery', "Awesome Gallery") ], ondelete={'gallery': 'cascade'}) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a7480d61211..e5ca0ef2667 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,12 +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=lambda self: 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() + active = fields.Boolean(default=True) garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() @@ -24,3 +25,16 @@ class EstateProperty(models.Model): help="Direction the garden faces" ) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Cancelled'), + ], + string="Status", + required=True, + copy=False, + default='new', + ) From 8521ee9410485c5406f9c14d31182e451e543c43 Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Mon, 16 Mar 2026 09:59:19 +0530 Subject: [PATCH 05/26] [FIX] estate: fixed the issues Fixed issues in the previous commit. --- awesome_gallery/models/ir_action.py | 2 +- estate/models/estate_property.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/awesome_gallery/models/ir_action.py b/awesome_gallery/models/ir_action.py index 8cf0277e0ff..1be8a53222d 100644 --- a/awesome_gallery/models/ir_action.py +++ b/awesome_gallery/models/ir_action.py @@ -5,6 +5,6 @@ class ActWindowView(models.Model): _inherit = 'ir.actions.act_window.view' - .Selection(selection_add=[ +view_mode = fields.Selection(selection_add=[ ('gallery', "Awesome Gallery") ], ondelete={'gallery': 'cascade'}) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index e5ca0ef2667..f51ee74535a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -19,19 +19,19 @@ class EstateProperty(models.Model): garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection( - string='Orientation', + string="Orientation", selection=[ - ('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + ('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")], help="Direction the garden faces" ) state = fields.Selection( selection=[ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('canceled', 'Cancelled'), + ('new', "New"), + ('offer_received', "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ('sold', "Sold"), + ('canceled', "Cancelled"), ], string="Status", required=True, From 5b1412bd1212f8888472041eee814043d325ea54 Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Mon, 16 Mar 2026 22:57:03 +0530 Subject: [PATCH 06/26] [IMP] estate: Fixed issues and completed chapter 6 --- awesome_gallery/models/ir_action.py | 1 + estate/models/estate_property.py | 2 +- estate/views/estate_property_menus.xml | 4 +- estate/views/estate_property_views.xml | 79 ++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/awesome_gallery/models/ir_action.py b/awesome_gallery/models/ir_action.py index 1be8a53222d..12bf17e5013 100644 --- a/awesome_gallery/models/ir_action.py +++ b/awesome_gallery/models/ir_action.py @@ -5,6 +5,7 @@ class ActWindowView(models.Model): _inherit = 'ir.actions.act_window.view' + view_mode = fields.Selection(selection_add=[ ('gallery', "Awesome Gallery") ], ondelete={'gallery': 'cascade'}) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f51ee74535a..ae1a166eecc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -36,5 +36,5 @@ class EstateProperty(models.Model): string="Status", required=True, copy=False, - default='new', + default="new", ) diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml index 1c6d53ac7bd..5d240e46a1a 100644 --- a/estate/views/estate_property_menus.xml +++ b/estate/views/estate_property_menus.xml @@ -2,6 +2,6 @@ + action="estate_property_action" + parent="estate_first_level_menu"/> diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4d544c18597..4c917ec392f 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,4 +4,83 @@ estate.property list,form + + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + From a7fc97b54f9e754f3f2a8e6cf7b66bf3d53ed97d Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Tue, 17 Mar 2026 23:47:09 +0530 Subject: [PATCH 07/26] [IMP] Estate: Partially completed chapter 7 Created new model called estate_property_type.py. Added actions and menus. Implemented list, form and search views in estate_property_types.xml --- awesome_gallery/models/ir_action.py | 3 +- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 17 ++++++- estate/models/estate_property_types.py | 8 ++++ estate/security/ir.model.access.csv | 3 +- estate/views/estate_property_menus.xml | 15 ++++--- estate/views/estate_property_types_views.xml | 33 ++++++++++++++ estate/views/estate_property_views.xml | 47 +++++++++++--------- 9 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 estate/models/estate_property_types.py create mode 100644 estate/views/estate_property_types_views.xml diff --git a/awesome_gallery/models/ir_action.py b/awesome_gallery/models/ir_action.py index 12bf17e5013..eae20acbf5c 100644 --- a/awesome_gallery/models/ir_action.py +++ b/awesome_gallery/models/ir_action.py @@ -5,7 +5,6 @@ class ActWindowView(models.Model): _inherit = 'ir.actions.act_window.view' - -view_mode = fields.Selection(selection_add=[ + view_mode = fields.Selection(selection_add=[ ('gallery', "Awesome Gallery") ], ondelete={'gallery': 'cascade'}) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 1f67a75e150..3843eed2ba8 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,6 +8,7 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_types_views.xml', 'views/estate_property_menus.xml', ], 'installable': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..06c549bcf89 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,2 @@ from . import estate_property +from . import estate_property_types diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ae1a166eecc..3a0f727c74c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -2,7 +2,7 @@ class EstateProperty(models.Model): - _name = "estate.property" + _name = 'estate.property' _description = "Real Estate Property" name = fields.Char(required=True) @@ -23,7 +23,20 @@ class EstateProperty(models.Model): selection=[ ('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")], help="Direction the garden faces" - + ) + 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 ) state = fields.Selection( selection=[ diff --git a/estate/models/estate_property_types.py b/estate/models/estate_property_types.py new file mode 100644 index 00000000000..e51a916bd9b --- /dev/null +++ b/estate/models/estate_property_types.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = "Property Type" + + name = fields.Char(string="Name", required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 976b61e8cb3..1b712a3f909 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml index 5d240e46a1a..756844388ee 100644 --- a/estate/views/estate_property_menus.xml +++ b/estate/views/estate_property_menus.xml @@ -1,7 +1,12 @@ - - - + + + + + + diff --git a/estate/views/estate_property_types_views.xml b/estate/views/estate_property_types_views.xml new file mode 100644 index 00000000000..c4871c30561 --- /dev/null +++ b/estate/views/estate_property_types_views.xml @@ -0,0 +1,33 @@ + + + Property Types + estate.property.type + list,form + + + + estate.property.type.list + estate.property.type + + + + + + + + + estate.property.type.form + estate.property.type + +
+ +
+

+ +

+
+
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4c917ec392f..025410428e4 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -35,6 +35,7 @@ + @@ -55,32 +56,36 @@ - + + + + + + + + - estate.property.search - estate.property - - - - - - - - - - - - - - + estate.property.search + estate.property + + + + + + + + + + + - - - - + + + + From 68e69b03f04e59836aae620e5935707570faac20 Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Wed, 18 Mar 2026 18:48:45 +0530 Subject: [PATCH 08/26] [IMP] Estate: Completed Chapter-7 Implemented Many2many and One2many relations. Added estate.property.tag model with Many2many relation to estate.property Added offer_ids One2many field to display offers on property form Created corresponding views, menus, actions and access to above models and files --- estate/__manifest__.py | 2 ++ estate/models/__init__.py | 2 ++ estate/models/estate_property.py | 19 ++++---------- estate/models/estate_property_offer.py | 17 ++++++++++++ estate/models/estate_property_tags.py | 8 ++++++ estate/models/estate_property_types.py | 2 +- estate/security/ir.model.access.csv | 2 ++ estate/views/estate_property_menus.xml | 5 ++++ estate/views/estate_property_offer_views.xml | 27 ++++++++++++++++++++ estate/views/estate_property_tags_views.xml | 7 +++++ estate/views/estate_property_views.xml | 10 ++++++++ 11 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tags.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tags_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 3843eed2ba8..d0c3f1468c8 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -9,6 +9,8 @@ 'security/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_property_types_views.xml', + 'views/estate_property_tags_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_property_menus.xml', ], 'installable': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 06c549bcf89..b77e6f6703e 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,4 @@ from . import estate_property from . import estate_property_types +from . import estate_property_tags +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3a0f727c74c..f7042aa771d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -24,20 +24,10 @@ class EstateProperty(models.Model): ('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")], help="Direction the garden faces" ) - 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 - ) + tag_ids = fields.Many2many('estate.property.tag', string="Tags") + 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) state = fields.Selection( selection=[ ('new', "New"), @@ -51,3 +41,4 @@ class EstateProperty(models.Model): copy=False, default="new", ) + 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..2477b274061 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,17 @@ +from odoo import fields, models + + +class EstatepropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = "Property Offers" + + price = fields.Float() + status = fields.Selection( + selection=[ + ('accepted', "Accepted"), + ('refused', "Refused"), + ], + copy=False + ) + partner_id = fields.Many2one("res.partner", required=True) + property_id = fields.Many2one("estate.property", required=True) diff --git a/estate/models/estate_property_tags.py b/estate/models/estate_property_tags.py new file mode 100644 index 00000000000..8a5d64b48cf --- /dev/null +++ b/estate/models/estate_property_tags.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTags(models.Model): + _name = 'estate.property.tag' + _description = "Property Tags" + + name = fields.Char(required=True) diff --git a/estate/models/estate_property_types.py b/estate/models/estate_property_types.py index e51a916bd9b..3a56a9e478c 100644 --- a/estate/models/estate_property_types.py +++ b/estate/models/estate_property_types.py @@ -5,4 +5,4 @@ class EstatePropertyType(models.Model): _name = 'estate.property.type' _description = "Property Type" - name = fields.Char(string="Name", required=True) + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 1b712a3f909..49bca99cac8 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml index 756844388ee..65abf089eb4 100644 --- a/estate/views/estate_property_menus.xml +++ b/estate/views/estate_property_menus.xml @@ -9,4 +9,9 @@ + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..5b48a2ba294 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,27 @@ + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + +
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml new file mode 100644 index 00000000000..b3ab8fd9a1c --- /dev/null +++ b/estate/views/estate_property_tags_views.xml @@ -0,0 +1,7 @@ + + + Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 025410428e4..48a92e08e3b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -32,6 +32,7 @@ + @@ -63,6 +64,15 @@ + + + + + + + + + From 8e1e94263a0f79de1c4353b4e362f5ffd10db8ed Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Mon, 30 Mar 2026 19:01:17 +0530 Subject: [PATCH 09/26] [IMP] estate: Partially completed Chapter 8 Understood the concept of computed and relational fields. Imported api from odoo to supports depends decorator. Calculated total area by adding living area and garden area. And best price by selecting the highest offer price using mapped method. --- estate/models/estate_property.py | 20 +++++++++++++++++--- estate/views/estate_property_menus.xml | 1 - estate/views/estate_property_offer_views.xml | 2 +- estate/views/estate_property_views.xml | 8 ++++++-- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f7042aa771d..8c54a57c928 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): @@ -12,12 +12,12 @@ class EstateProperty(models.Model): expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) - living_area = fields.Integer() + living_area = fields.Integer(string="living area(sqm)") facades = fields.Integer() active = fields.Boolean(default=True) garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer() + garden_area = fields.Integer(string="Garden area(sqm)") garden_orientation = fields.Selection( string="Orientation", selection=[ @@ -25,9 +25,11 @@ class EstateProperty(models.Model): help="Direction the garden faces" ) tag_ids = fields.Many2many('estate.property.tag', string="Tags") + 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) + state = fields.Selection( selection=[ ('new', "New"), @@ -42,3 +44,15 @@ class EstateProperty(models.Model): default="new", ) offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + total_area = fields.Float(string="Total Area (sqm)", compute="_compute_total_area") + best_price = fields.Float(string="Best Offer", compute="_compute_best_price") + + @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_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped("price"), default=0.0) diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml index 65abf089eb4..2e3bf4a8d6d 100644 --- a/estate/views/estate_property_menus.xml +++ b/estate/views/estate_property_menus.xml @@ -14,4 +14,3 @@ action="estate_property_tag_action" parent="estate_second_level_menu"/> - diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 5b48a2ba294..81e6337b458 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -24,4 +24,4 @@ - \ No newline at end of file + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 48a92e08e3b..00d2e1d0c2d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -30,7 +30,7 @@

+ placeholder="Property Name"/>

@@ -43,6 +43,7 @@ + @@ -56,6 +57,9 @@ + + + @@ -91,7 +95,7 @@ - + From a6d7c9c91605eeb8cabe127f3071b8d0719eeb1d Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Fri, 3 Apr 2026 18:25:01 +0530 Subject: [PATCH 10/26] [IMP] estate: Completed Ch. 8 Implemented inverse function on validity_date to compute deadline from date_availability and vice versa. Added @api.onchange on garden field to auto-set area (10) and orientation (North) when enabled. When garden is disabled, garden area and orientation are cleared automatically. --- estate/models/estate_property.py | 20 ++++++++++--- estate/models/estate_property_offer.py | 31 +++++++++++++++++++- estate/models/estate_property_tags.py | 1 + estate/views/estate_property_menus.xml | 2 +- estate/views/estate_property_offer_views.xml | 5 ++++ estate/views/estate_property_tags_views.xml | 15 ++++++++++ estate/views/estate_property_views.xml | 10 +++---- 7 files changed, 72 insertions(+), 12 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 8c54a57c928..9e6f6d2985c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -25,11 +25,9 @@ class EstateProperty(models.Model): help="Direction the garden faces" ) tag_ids = fields.Many2many('estate.property.tag', string="Tags") - 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) - state = fields.Selection( selection=[ ('new', "New"), @@ -43,7 +41,7 @@ class EstateProperty(models.Model): copy=False, default="new", ) - offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers", copy=True) total_area = fields.Float(string="Total Area (sqm)", compute="_compute_total_area") best_price = fields.Float(string="Best Offer", compute="_compute_best_price") @@ -55,4 +53,18 @@ def _compute_total_area(self): @api.depends("offer_ids.price") def _compute_best_price(self): for record in self: - record.best_price = max(record.offer_ids.mapped("price"), default=0.0) + if record.offer_ids: + record.best_price = max(record.offer_ids.mapped("price")) + record.state = "offer_received" + else: + record.best_price = 0.0 + record.state = "new" + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = False diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 2477b274061..a233921b9b5 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): @@ -15,3 +15,32 @@ class EstatepropertyOffer(models.Model): ) partner_id = fields.Many2one("res.partner", required=True) property_id = fields.Many2one("estate.property", required=True) + validity = fields.Integer( + string="Validity (days)", + default=7 + ) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline" + ) + + @api.depends("validity", "create_date") + 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, + ) + else: + record.date_deadline = fields.Date.add( + fields.Date.today(), days=record.validity, + ) + + def _inverse_date_deadline(self): + for record in self: + if record.create_date and record.date_deadline: + record.validity = ( + record.date_deadline - + record.create_date.date() + ).days diff --git a/estate/models/estate_property_tags.py b/estate/models/estate_property_tags.py index 8a5d64b48cf..5ff83612dcc 100644 --- a/estate/models/estate_property_tags.py +++ b/estate/models/estate_property_tags.py @@ -6,3 +6,4 @@ class EstatePropertyTags(models.Model): _description = "Property Tags" name = fields.Char(required=True) + color = fields.Integer(string="Color") diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml index 2e3bf4a8d6d..683c6214e00 100644 --- a/estate/views/estate_property_menus.xml +++ b/estate/views/estate_property_menus.xml @@ -1,7 +1,7 @@ - diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 81e6337b458..69678c70e26 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -6,6 +6,8 @@ + + @@ -19,6 +21,9 @@ + + + diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml index b3ab8fd9a1c..25fe0b04d9c 100644 --- a/estate/views/estate_property_tags_views.xml +++ b/estate/views/estate_property_tags_views.xml @@ -4,4 +4,19 @@ estate.property.tag list,form + + + estate.property.tag.form + estate.property.tag + +
+ + + + + + +
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 00d2e1d0c2d..42e717a849e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,6 +3,7 @@ Properties estate.property list,form + {'search_default_available': 1, 'search_default_Bedroomss': 2} @@ -32,7 +33,7 @@ - + @@ -70,11 +71,6 @@
- - - - -
@@ -96,6 +92,8 @@ + + From 6d7d356eefc154705612cfb2e780f1bbccdde67c Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Sun, 5 Apr 2026 01:03:44 +0530 Subject: [PATCH 11/26] [IMP] estate: Completed Ch 9 Implement action_sold and action_cancel logic with UserError guards Add Accept/Refuse buttons to property offers using fa-icons Link offer acceptance to property selling_price and buyer_id Update UI with statusbar widget and button visibility rules --- estate/models/estate_property.py | 19 ++++++++++++++++--- estate/models/estate_property_offer.py | 18 ++++++++++++++++++ estate/views/estate_property_offer_views.xml | 4 +++- estate/views/estate_property_views.xml | 12 +++++++++++- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9e6f6d2985c..c59d1013498 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): @@ -34,7 +35,7 @@ class EstateProperty(models.Model): ('offer_received', "Offer Received"), ("offer_accepted", "Offer Accepted"), ('sold', "Sold"), - ('canceled', "Cancelled"), + ('cancelled', "Cancelled"), ], string="Status", required=True, @@ -55,10 +56,8 @@ def _compute_best_price(self): for record in self: if record.offer_ids: record.best_price = max(record.offer_ids.mapped("price")) - record.state = "offer_received" else: record.best_price = 0.0 - record.state = "new" @api.onchange('garden') def _onchange_garden(self): @@ -68,3 +67,17 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = False + + def action_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError("Cancelled properties cannot be sold!") + record.state = 'sold' + return True + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise UserError("Sold properties cannot be cancelled!") + record.state = 'cancelled' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index a233921b9b5..75756839556 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): @@ -44,3 +45,20 @@ def _inverse_date_deadline(self): record.date_deadline - record.create_date.date() ).days + + def action_accept(self): + for record in self: + if record.property_id.state == 'sold': + raise UserError("This property is already sold!") + if 'accepted' in record.property_id.offer_ids.mapped('status'): + raise UserError("An offer has already been accepted!") + record.status = 'accepted' + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + record.property_id.state = 'offer_accepted' + return True + + def action_refuse(self): + for record in self: + record.status = 'refused' + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 69678c70e26..12988d11a77 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -3,11 +3,13 @@ estate.property.offer.list estate.property.offer - + + +

diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index deff8b83059..38f4268e850 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -10,13 +10,15 @@ estate.property.list estate.property - + - + @@ -29,10 +31,12 @@

-
- + + @@ -55,23 +59,28 @@ + + + - - - - - + + + + + + + @@ -80,15 +89,22 @@ - - - + + + + + + -

diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 68e3cb3f0c3..4f678ba8125 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -42,6 +42,17 @@ statusbar_visible="new,offer_received,offer_accepted,sold"/> +
+ +

- + + @@ -130,5 +142,5 @@ - + diff --git a/estate/views/estate_property_visit_views.xml b/estate/views/estate_property_visit_views.xml new file mode 100644 index 00000000000..afa345efb95 --- /dev/null +++ b/estate/views/estate_property_visit_views.xml @@ -0,0 +1,42 @@ + + + estate.property.visit + estate.property.visit + + + + + + + + + + estate.property.visit.form + estate.property.visit + + +
+ +
+ + + + + + + + + +
+
+ + + Visits + estate.property.visit + list,form + [('property_id', '=', active_id)] + {'default_property_id': active_id} + +
From 39aea92faba46e6827654ed18018bc7f8a564e6e Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Thu, 16 Apr 2026 12:39:46 +0530 Subject: [PATCH 17/26] [IMP] estate : Completed Group Task Added estate_property_issues so we can report any issues in the property Added computed SLA deadline and overdue logic based on issue priority. Implemented restriction in action_sold to prevent selling properties with unresolved high-priority issues. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 4 + estate/models/estate_property_issues.py | 81 +++++++++++++++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_property_issues_views.xml | 62 ++++++++++++++ estate/views/estate_property_views.xml | 14 +++- 7 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 estate/models/estate_property_issues.py create mode 100644 estate/views/estate_property_issues_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 063c02d943c..e2bd0f7f8d9 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,6 +11,7 @@ 'views/estate_property_tags_views.xml', 'views/estate_property_offer_views.xml', 'views/estate_property_types_views.xml', + 'views/estate_property_issues_views.xml', 'views/res_user.xml', 'views/estate_property_views.xml', 'views/estate_property_menus.xml', diff --git a/estate/models/__init__.py b/estate/models/__init__.py index aed44be22f6..8bce8ee65fb 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -3,4 +3,5 @@ from . import estate_property_tags from . import estate_property_offer from . import estate_property_visit +from . import estate_property_issues from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 5f8b82f2b70..ba5c70ee40d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -47,6 +47,7 @@ class EstateProperty(models.Model): offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers", copy=True) total_area = fields.Float(string="Total Area (sqm)", compute="_compute_total_area") best_price = fields.Float(string="Best Offer", compute="_compute_best_price") + issue_ids = fields.One2many('estate.property.issues', 'property_id') @api.depends("living_area", "garden_area") def _compute_total_area(self): @@ -72,6 +73,9 @@ def _onchange_garden(self): def action_sold(self): for record in self: + for i in record.issue_ids: + if i.priority == 'high' and i.issue_state != 'resolved': + raise UserError("Cannot sell! Property has unresolved high-priority issues.") if record.state == 'cancelled': raise UserError("Cancelled properties cannot be sold!") record.state = 'sold' diff --git a/estate/models/estate_property_issues.py b/estate/models/estate_property_issues.py new file mode 100644 index 00000000000..0e6db3717dc --- /dev/null +++ b/estate/models/estate_property_issues.py @@ -0,0 +1,81 @@ +from odoo import api, fields, models +from datetime import timedelta +from odoo.exceptions import UserError + + +class EstatepropertyIssues(models.Model): + _name = 'estate.property.issues' + _description = 'Property Issues' + + name = fields.Char(required=True, string="Issue") + property_id = fields.Many2one('estate.property', string="Property") + reported_by = fields.Many2one('res.partner', string="Reported By") + assigned_to = fields.Many2one('res.users', string="Assigned To") + issue_type = fields.Selection( + selection=[ + ('plumbing', "Plumbing"), + ('electrical', "Electrical"), + ('structural', "Structural"), + ('other', "Other")], + required=True + ) + issue_state = fields.Selection( + selection=[ + ('new', "New"), + ('in progress', "In Progress"), + ('resolved', "Resolved"), + ('cancelled', "Cancelled"), + ('overdue', "Overdue")], + default='new', + string="Status" + ) + priority = fields.Selection( + selection=[ + ('low', "Low"), + ('medium', "Medium"), + ('high', "High")], + readonly=True, + compute='_compute_priority', + store=True + ) + reported_date = fields.Date(string="When reported", default=lambda self: fields.Date.today()) + resolved_date = fields.Date(readonly=True) + description = fields.Text() + is_overdue = fields.Boolean(compute='_compute_is_overdue') + + @api.depends('issue_type') + def _compute_priority(self): + if self.issue_type == 'other': + self.priority = 'low' + if self.issue_type == 'structural': + self.priority = 'high' + if self.issue_type in ('plumbing', 'electrical'): + self.priority = 'medium' + + @api.onchange('assigned_to') + def _onchange_assigned_to(self): + if self.assigned_to: + self.issue_state = 'in progress' + + def action_resolve(self): + for record in self: + if record.issue_state == 'cancelled': + raise UserError("Cancelled issues cannot be resolved!") + record.issue_state = "resolved" + record.resolved_date = fields.Date.today() + return True + + def action_cancel(self): + for record in self: + record.issue_state = 'cancelled' + return True + + @api.depends('reported_date', 'priority', 'issue_state') + def _compute_is_overdue(self): + for record in self: + if record.issue_state in ('resolved', 'cancelled'): + record.is_overdue = False + continue + days = {'high': 2, 'medium': 5, 'low': 10}.get(record.priority, 0) + deadline = record.reported_date + timedelta(days=days) + record.is_overdue = fields.Date.today() > deadline diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 8954b367ba8..5c477b276a0 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -4,3 +4,4 @@ access_estate_property_type,access_estate_property_type,model_estate_property_ty access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 access_estate_property_visit,access_estate_property_visit,model_estate_property_visit,base.group_user,1,1,1,1 +access_estate_property_issues,access_estate_property_issues,model_estate_property_issues,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_issues_views.xml b/estate/views/estate_property_issues_views.xml new file mode 100644 index 00000000000..6cd2d41af44 --- /dev/null +++ b/estate/views/estate_property_issues_views.xml @@ -0,0 +1,62 @@ + + + estate.property.issues + estate.property.issues + + + + + + + + + + + + + + + + estate.property.issues.form + estate.property.issues + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + + Issues + estate.property.issues + list,form + [('property_id', '=', active_id)] + {'default_property_id': active_id} + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4f678ba8125..b80518f22b1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -43,6 +43,15 @@
+ -
-
-

- -

-
- - - - - - - - - - - +
+ +
+
+

+ +

+
+ + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 7577796d9db..cb4e1151dc4 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,8 +6,8 @@ {'search_default_available': 1, 'search_default_Bedroomss': 2} - - estate.property.list + + estate.property.view.list estate.property - estate.property.form + estate.property.view.form estate.property
@@ -42,75 +42,60 @@ statusbar_visible="new,offer_received,offer_accepted,sold"/> -
- - -
+
+ + +

- +

- - - - - - -
- - This property has some suspicious offers -
-
- - - - - + + + + + +
+ + This property has some suspicious offers +
+
+ + + + +
-
- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + + @@ -133,7 +118,7 @@ - estate.property.search + estate.property.view.search estate.property diff --git a/estate/views/estate_property_visit_views.xml b/estate/views/estate_property_visit_views.xml index afa345efb95..c031e957e82 100644 --- a/estate/views/estate_property_visit_views.xml +++ b/estate/views/estate_property_visit_views.xml @@ -1,24 +1,22 @@ - - estate.property.visit + + estate.property.visits.view.list estate.property.visit - - - - + + + + - estate.property.visit.form + estate.property.visits.view.form estate.property.visit
- +
@@ -33,7 +31,7 @@
- Visits + Estate Property Stat Button estate.property.visit list,form [('property_id', '=', active_id)] From dc83a25783ff6b4446040e99e0bdc0e4cb983844 Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Thu, 7 May 2026 12:41:13 +0530 Subject: [PATCH 21/26] [ADD] estate_crm : added a new module Added a new bridge module called estate_crm which depends on both estate and crm Inherited crm.lead to borrow methods and fields from crm module Every offer made in the estate module will create a lead in crm Marked lead won/lost on offer accept/refuse by extending the action accept and refuse. Also added the 2 tasks from review i.e adding a chatter and mail template when the user clicks sold button --- estate/__manifest__.py | 3 +- estate/data/mail_template.xml | 20 ++++++++++++ estate/models/estate_property.py | 27 ++++++++++++++-- estate/views/estate_property_views.xml | 1 + estate_crm/__init__.py | 1 + estate_crm/__manifest__.py | 9 ++++++ estate_crm/models/__init__.py | 1 + estate_crm/models/estate_property_offer.py | 36 ++++++++++++++++++++++ 8 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 estate/data/mail_template.xml create mode 100644 estate_crm/__init__.py create mode 100644 estate_crm/__manifest__.py create mode 100644 estate_crm/models/__init__.py create mode 100644 estate_crm/models/estate_property_offer.py diff --git a/estate/__manifest__.py b/estate/__manifest__.py index e2bd0f7f8d9..c4a3b3756bd 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,10 +3,11 @@ 'author': 'Aditi (adpaw)', 'license': 'LGPL-3', 'depends': [ - 'base', 'calendar' + 'base', 'calendar', 'mail' ], 'data': [ 'security/ir.model.access.csv', + 'data/mail_template.xml', 'views/estate_property_visit_views.xml', 'views/estate_property_tags_views.xml', 'views/estate_property_offer_views.xml', diff --git a/estate/data/mail_template.xml b/estate/data/mail_template.xml new file mode 100644 index 00000000000..7e3963db60f --- /dev/null +++ b/estate/data/mail_template.xml @@ -0,0 +1,20 @@ + + + + Estate: Property sold Confirmation + + Property Sold: {{ object.name }} + {{ (object.salesperson_id.email_formatted or object.buyer_id.email_formatted)}} + {{ object.salesperson_id.partner_id.id }},{{ object.buyer_id.id }} + + +
+

Property: Property Name

+

Salesperson: Salesperson

+

Price: 0.00

+

Description: Description

+
+
+
+
+
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index c2d17d1f170..a92644c3681 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -6,6 +6,7 @@ class EstateProperty(models.Model): _name = 'estate.property' _description = "Real Estate Property" + _inherit = 'mail.thread' _order = 'id desc' name = fields.Char(required=True) @@ -42,7 +43,8 @@ class EstateProperty(models.Model): string="Status", required=True, copy=False, - default="new" + default="new", + tracking=2 ) total_area = fields.Float(string="Total Area (sqm)", compute="_compute_total_area") best_price = fields.Float(string="Best Offer", compute="_compute_best_price") @@ -160,7 +162,28 @@ def action_sold(self): raise UserError("Cancelled properties cannot be sold!") record.state = 'sold' record.sold_date = fields.Date.today() - return True + + template_id = self.env.ref('estate.mail_template_sold_confirmation', raise_if_not_found=False) + ctx = { + 'default_model': 'estate.property', + 'default_res_ids': self.ids, + 'default_composition_mode': 'comment', + 'default_email_layout_xmlid': 'mail.mail_notification_layout_with_responsible_signature', + 'force_email': True, + 'default_template_id': template_id.id, + } + + action = { + 'name': ('Sold'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'mail.compose.message', + 'views': [(False, 'form')], + 'view_id': False, + 'target': 'new', + 'context': ctx, + } + return action def action_cancel(self): for record in self: diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index cb4e1151dc4..8bfc3028072 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -113,6 +113,7 @@
+
diff --git a/estate_crm/__init__.py b/estate_crm/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_crm/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_crm/__manifest__.py b/estate_crm/__manifest__.py new file mode 100644 index 00000000000..4f696f19d5f --- /dev/null +++ b/estate_crm/__manifest__.py @@ -0,0 +1,9 @@ +{ + 'name': 'Real Estate Offer', + 'author': 'Aditi (adpaw)', + 'license': 'LGPL-3', + 'depends': [ + 'estate', 'crm' + ], + 'installable': True, +} diff --git a/estate_crm/models/__init__.py b/estate_crm/models/__init__.py new file mode 100644 index 00000000000..6b65f241225 --- /dev/null +++ b/estate_crm/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property_offer diff --git a/estate_crm/models/estate_property_offer.py b/estate_crm/models/estate_property_offer.py new file mode 100644 index 00000000000..846591d9987 --- /dev/null +++ b/estate_crm/models/estate_property_offer.py @@ -0,0 +1,36 @@ +from odoo import api, fields, models + + +class EstatePropertyOffer(models.Model): + _inherit = "estate.property.offer" + + crm_lead_id = fields.Many2one('crm.lead', string='CRM Lead', copy=False) + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + for record in records: + lead = self.env["crm.lead"].create({ + "name": record.property_id.name, + "expected_revenue": record.price, + "partner_id": record.partner_id.id, + }) + record.crm_lead_id = lead + return records + + def action_accept(self): + res = super().action_accept() + for offer in self: + if offer.crm_lead_id: + offer.crm_lead_id.action_set_won() + for offer in self: + other_offers = offer.property_id.offer_ids - offer + other_offers.action_refuse() + return res + + def action_refuse(self): + res = super().action_refuse() + for offer in self: + if offer.crm_lead_id: + offer.crm_lead_id.action_set_lost() + return res From 0b3c70c16a253b5e86bf057c461d5836fc5c6d92 Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Fri, 8 May 2026 17:06:19 +0530 Subject: [PATCH 22/26] [IMP] Awesome_owl : Completed Ch1 of owl framework. Successfully initialized the awesome_owl module by updating the Playground file Defined the component class in JS and linked it to a basic XML template Managed data flow by passing read-only Props from parents to children. Used usestate for reactivity, that re renders the ui whenever state changes. --- awesome_owl/static/src/card/card.js | 18 ++++++++ awesome_owl/static/src/card/card.xml | 18 ++++++++ awesome_owl/static/src/counter/counter.js | 20 +++++++++ awesome_owl/static/src/counter/counter.xml | 8 ++++ awesome_owl/static/src/playground.js | 20 +++++++-- awesome_owl/static/src/playground.xml | 24 +++++++--- awesome_owl/static/src/todo_list/todo_item.js | 16 +++++++ .../static/src/todo_list/todo_item.xml | 18 ++++++++ awesome_owl/static/src/todo_list/todo_list.js | 44 +++++++++++++++++++ .../static/src/todo_list/todo_list.xml | 20 +++++++++ awesome_owl/static/src/utils.js | 8 ++++ estate/data/mail_template.xml | 2 +- estate/models/estate_property.py | 4 -- 13 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml create mode 100644 awesome_owl/static/src/todo_list/todo_item.js create mode 100644 awesome_owl/static/src/todo_list/todo_item.xml create mode 100644 awesome_owl/static/src/todo_list/todo_list.js create mode 100644 awesome_owl/static/src/todo_list/todo_list.xml create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..cd36c0797ec --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,18 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + + static props = { + title: { type: String }, + slots: { type: Object, optional: true }, + }; + + setup() { + this.state = useState({ isOpen: true }); + } + + toggleContent() { + this.state.isOpen = !this.state.isOpen; + } +} \ 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..1f78da44fef --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,18 @@ + + +
+
+
+ +
+
+ +
+
+
+
\ 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..82e57af8ae9 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,20 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.Counter"; + + static props = { + onChange: { type: Function, optional: true }, + }; + + setup() { + this.state = useState({ value: 0 }); + } + + increment() { + this.state.value++; + if (this.props.onChange) { + this.props.onChange(this.state.value); + } + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..0700b087d70 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,8 @@ + + +
+ Counter: + +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..7fe30315da9 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,19 @@ -import { Component } from "@odoo/owl"; +import { Component, useState, markup } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo_list/todo_list"; export class Playground extends Component { - static template = "awesome_owl.playground"; -} + static template = "awesome_owl.Playground"; + static components = { Counter, Card, TodoList }; + + setup() { + this.state = useState({ sum: 0 }); + this.str1 = markup("
some content
"); + // this.str2 = "This is a card" + } + + incrementSum() { + this.state.sum++; + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..18d41871132 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,24 @@ - - - +
- hello world +

hello world

+ + + +

The sum is:

+ +
+ +
+ + + + +
+ +
+ +
-
diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js new file mode 100644 index 00000000000..1a4ec7f03c7 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,16 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + + static props = { + todo: { + type: Object, + shape: { + id: Number, description: String, isCompleted: Boolean, + }, + }, + toggleState: { type: Function }, + removeTodo: { type: Function }, + }; +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml new file mode 100644 index 00000000000..8c9c77c56e0 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,18 @@ + + +
+ + + + . + + + +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..4cfab5b092e --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,44 @@ +import { Component, useState, useRef, onMounted } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + setup() { + this.todos = useState([]); + this.nextId = 1; + this.inputRef = useRef("todoInput"); + + onMounted(() => { + this.inputRef.el.focus(); + }); + } + + addTodo(ev) { + if (ev.keyCode === 13) { + const description = ev.target.value.trim(); + if (!description) return; + this.todos.push({ + id: this.nextId++, + description: description, + isCompleted: false, + }); + ev.target.value = ""; + } + } + + toggleState(todoId) { + const todo = this.todos.find(t => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(todoId) { + const index = this.todos.findIndex(t => t.id === todoId); + if (index >= 0) { + this.todos.splice(index, 1); + } + } +} diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml new file mode 100644 index 00000000000..8befc365057 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,20 @@ + + +
+
+
Todo List
+ +
+ + + +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..6a5475b357e --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,8 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(refName) { + const ref = useRef(refName); + onMounted(() => { + ref.el.focus(); + }); +} \ No newline at end of file diff --git a/estate/data/mail_template.xml b/estate/data/mail_template.xml index 7e3963db60f..7dba1f6dd73 100644 --- a/estate/data/mail_template.xml +++ b/estate/data/mail_template.xml @@ -5,7 +5,7 @@ Property Sold: {{ object.name }} {{ (object.salesperson_id.email_formatted or object.buyer_id.email_formatted)}} - {{ object.salesperson_id.partner_id.id }},{{ object.buyer_id.id }} + {{ (object.salesperson_id.partner_id.id or object.buyer_id.id) }}
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a92644c3681..ecb61c4be3d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -167,8 +167,6 @@ def action_sold(self): ctx = { 'default_model': 'estate.property', 'default_res_ids': self.ids, - 'default_composition_mode': 'comment', - 'default_email_layout_xmlid': 'mail.mail_notification_layout_with_responsible_signature', 'force_email': True, 'default_template_id': template_id.id, } @@ -178,8 +176,6 @@ def action_sold(self): 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'mail.compose.message', - 'views': [(False, 'form')], - 'view_id': False, 'target': 'new', 'context': ctx, } From 509c7f45914ee26b85eb0ba49fd5140eef65c67d Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Fri, 8 May 2026 17:10:32 +0530 Subject: [PATCH 23/26] [IMP] Awesome_owl : Completed Ch1 of owl framework. Successfully initialized the awesome_owl module by updating the Playground file Defined the component class in JS and linked it to a basic XML template Managed data flow by passing read-only Props from parents to children. Used usestate for reactivity, that re renders the ui whenever state changes. --- awesome_owl/__init__.py | 2 +- awesome_owl/static/src/card/card.js | 2 +- awesome_owl/static/src/card/card.xml | 2 +- awesome_owl/static/src/counter/counter.js | 2 +- awesome_owl/static/src/counter/counter.xml | 2 +- awesome_owl/static/src/main.js | 1 - awesome_owl/static/src/playground.js | 2 +- awesome_owl/static/src/todo_list/todo_item.js | 2 +- awesome_owl/static/src/todo_list/todo_item.xml | 2 +- awesome_owl/static/src/todo_list/todo_list.xml | 2 +- awesome_owl/static/src/utils.js | 2 +- 11 files changed, 10 insertions(+), 11 deletions(-) diff --git a/awesome_owl/__init__.py b/awesome_owl/__init__.py index 457bae27e11..b0f26a9a602 100644 --- a/awesome_owl/__init__.py +++ b/awesome_owl/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import controllers \ No newline at end of file +from . import controllers diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index cd36c0797ec..5531a5f581c 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -15,4 +15,4 @@ export class Card extends Component { toggleContent() { this.state.isOpen = !this.state.isOpen; } -} \ 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 1f78da44fef..f62a7fcbaf9 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -15,4 +15,4 @@

- \ No newline at end of file + diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index 82e57af8ae9..1d6c0c5a0a8 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -17,4 +17,4 @@ export class Counter extends Component { this.props.onChange(this.state.value); } } -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml index 0700b087d70..6c58b742189 100644 --- a/awesome_owl/static/src/counter/counter.xml +++ b/awesome_owl/static/src/counter/counter.xml @@ -5,4 +5,4 @@

- \ No newline at end of file + diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1aaea902b55..6c108687e29 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -9,4 +9,3 @@ const config = { // Mount the Playground component when the document.body is ready whenReady(() => mountComponent(Playground, document.body, config)); - diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 7fe30315da9..77b0ae5f35a 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -16,4 +16,4 @@ export class Playground extends Component { incrementSum() { this.state.sum++; } -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js index 1a4ec7f03c7..a834d1cd87f 100644 --- a/awesome_owl/static/src/todo_list/todo_item.js +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -13,4 +13,4 @@ export class TodoItem extends Component { toggleState: { type: Function }, removeTodo: { type: Function }, }; -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml index 8c9c77c56e0..3a57a140e56 100644 --- a/awesome_owl/static/src/todo_list/todo_item.xml +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -15,4 +15,4 @@ t-on-click="() => props.removeTodo(props.todo.id)"/> - \ No newline at end of file + diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml index 8befc365057..045e7356aff 100644 --- a/awesome_owl/static/src/todo_list/todo_list.xml +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js index 6a5475b357e..f452f103aa0 100644 --- a/awesome_owl/static/src/utils.js +++ b/awesome_owl/static/src/utils.js @@ -5,4 +5,4 @@ export function useAutofocus(refName) { onMounted(() => { ref.el.focus(); }); -} \ No newline at end of file +} From 70cca2e2a8de78c7a765efa2c968380deeab504b Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Sat, 9 May 2026 17:15:50 +0530 Subject: [PATCH 24/26] [IMP] awesome_dashboard: OWL Chapter 2 completed Registered client action to link XML menu with Owl logic. Built custom service with rpc and memoize for data caching. Used useState and onWillStart for reactive data fetching. Created modular DashboardItem using generic slots. Integrated Chart.js to optimize asset loading. Used useRef and onMounted for imperative chart rendering. Used onWillUnmount to destroy charts and prevent memory leak --- awesome_dashboard/__manifest__.py | 3 + awesome_dashboard/static/src/dashboard.js | 8 -- awesome_dashboard/static/src/dashboard.xml | 8 -- .../static/src/dashboard/dashboard.js | 102 ++++++++++++++++++ .../static/src/dashboard/dashboard.scss | 3 + .../static/src/dashboard/dashboard.xml | 60 +++++++++++ .../dashboard_item/dashboard_item.js | 9 ++ .../dashboard_item/dashboard_item.xml | 8 ++ .../static/src/dashboard/dashboard_items.js | 70 ++++++++++++ .../src/dashboard/dashboard_registry.js | 3 + .../src/dashboard/number_card/number_card.js | 9 ++ .../src/dashboard/number_card/number_card.xml | 8 ++ .../src/dashboard/pie_chart/pie_chart.js | 34 ++++++ .../src/dashboard/pie_chart/pie_chart.xml | 6 ++ .../pie_chart_card/pie_chart_card.js | 11 ++ .../pie_chart_card/pie_chart_card.xml | 9 ++ .../src/dashboard/statistics_service.js | 24 +++++ .../static/src/dashboard_action.js | 10 ++ .../static/src/dashboard_action.xml | 9 ++ 19 files changed, 378 insertions(+), 16 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js delete mode 100644 awesome_dashboard/static/src/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_registry.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/pie_chart.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.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 create mode 100644 awesome_dashboard/static/src/dashboard/statistics_service.js create mode 100644 awesome_dashboard/static/src/dashboard_action.js create mode 100644 awesome_dashboard/static/src/dashboard_action.xml diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..f7c3c507aed 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -25,6 +25,9 @@ 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', ], + '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.js deleted file mode 100644 index c4fb245621b..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..f89821384c0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,102 @@ +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 { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; +import "./statistics_service"; // ← add this +import "./dashboard_items"; // ← add this ← THIS IS THE FIX + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem }; + + setup() { + this.action = useService("action"); + this.dialog = useService("dialog"); + this.display = { controlPanel: {} }; + this.stats = useService("awesome_dashboard.statistics"); + + this.state = useState({ + disabledItems: JSON.parse( + browser.localStorage.getItem("disabledDashboardItems") || "[]" + ) + }); + + this.allItems = registry + .category("awesome_dashboard.items") + .getAll(); + } + + get items() { + return this.allItems.filter( + item => !this.state.disabledItems.includes(item.id) + ); + } + + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.allItems, + disabledItems: this.state.disabledItems, + onUpdate: this.updateConfiguration.bind(this), + }); + } + + updateConfiguration(newDisabledItems) { + this.state.disabledItems = newDisabledItems; + browser.localStorage.setItem( + "disabledDashboardItems", + JSON.stringify(newDisabledItems) + ); + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [[false, "list"], [false, "form"]], + }); + } +} + +class ConfigurationDialog extends Component { + static template = "awesome_dashboard.ConfigurationDialog"; + static components = { Dialog, CheckBox }; + static props = ["close", "items", "disabledItems", "onUpdate"]; + + setup() { + const disabled = Array.isArray(this.props.disabledItems) + ? this.props.disabledItems + : []; + + this.items = useState( + this.props.items.map(item => ({ + ...item, + enabled: !disabled.includes(item.id), + })) + ); + } + + onChange(checked, item) { + item.enabled = checked; + const disabled = this.items + .filter(i => !i.enabled) + .map(i => i.id); + this.props.onUpdate(disabled); + } + + done() { + this.props.close(); + } +} + +registry + .category("lazy_components") + .add("awesome_dashboard.AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..ecdd85b5210 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: rgb(16, 18, 20); +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..b67ba1eecc0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..818c69c580f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static slots = { + default: {}, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..18367dea592 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,8 @@ + + + +
+ +
+
+
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..2edfb5b04b6 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,70 @@ +import { dashboardItemRegistry } from "./dashboard_registry"; +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; +import { _t } from "@web/core/l10n/translation"; + +dashboardItemRegistry.add("avg_quantity", { + id: "avg_quantity", + description: _t("Average amount of t-shirt"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Average amount of t-shirt by order this month"), + value: data?.average_quantity || 0, + }), +}); + +dashboardItemRegistry.add("average_time", { + id: "average_time", + description: _t("Average time new to sent/cancelled"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Average time for an order to go from 'new' to 'sent' or 'cancelled'"), + value: data?.average_time || 0, + }), +}); + +dashboardItemRegistry.add("new_orders", { + id: "new_orders", + description: _t("New orders this month"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Number of new orders this month"), + value: data?.nb_new_orders || 0, + }), +}); + +dashboardItemRegistry.add("nb_cancelled_orders", { + id: "nb_cancelled_orders", + description: _t("Cancelled orders this month"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Number of cancelled orders this month"), + value: data?.nb_cancelled_orders || 0, + }), +}); + +dashboardItemRegistry.add("total_amount", { + id: "total_amount", + description: _t("Total amount orders this month"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Total amount of new orders this month"), + value: data?.total_amount || 0, + }), +}); + +dashboardItemRegistry.add("pie_chart", { + id: "pie_chart", + description: _t("Shirt orders by size"), + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: _t("Shirt orders by size"), + data: data?.orders_by_size || {}, + }), +}); diff --git a/awesome_dashboard/static/src/dashboard/dashboard_registry.js b/awesome_dashboard/static/src/dashboard/dashboard_registry.js new file mode 100644 index 00000000000..e3fe596f7ad --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_registry.js @@ -0,0 +1,3 @@ +import { registry } from "@web/core/registry"; + +export const dashboardItemRegistry = registry.category("awesome_dashboard.items"); 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..95cc39c8b33 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: String, + value: { type: Number, optional: true }, + }; +} 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..6a3a97a9566 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,8 @@ + + +
+
+

+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js new file mode 100644 index 00000000000..4fb8c3bacaa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,34 @@ +import { Component, onWillStart, onMounted, useRef } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + + static props = { + data: Object, + }; + + setup() { + this.canvasRef = useRef("chart"); + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + onMounted(() => { + this.renderChart(); + }); + } + + renderChart() { + const ctx = this.canvasRef.el.getContext("2d"); + + new Chart(ctx, { + type: "pie", + data: { + labels: Object.keys(this.props.data), + datasets: [{ + data: Object.values(this.props.data), + }], + }, + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..e2f0b4e1ec8 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,6 @@ + + + + + + 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..23aa93ec2c3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js @@ -0,0 +1,11 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "../pie_chart/pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PieChart } + static props = { + title: String, + data: Object, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml new file mode 100644 index 00000000000..cd52b55e2bb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,9 @@ + + + +
+
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js new file mode 100644 index 00000000000..0b08c20b4ec --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,24 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +const statisticsService = { + name: "awesome_dashboard.statistics", + + start() { + const stats = reactive({}); + + async function loadStatistics() { + const data = await rpc("/awesome_dashboard/statistics"); + Object.assign(stats, data); + } + + loadStatistics(); + setInterval(loadStatistics, 10 * 60 * 1000); + return stats; + }, +}; + +registry + .category("services") + .add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..52decad3f86 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { registry } from "@web/core/registry"; + +export class DashboardAction extends Component { + static components = { LazyComponent }; + static template = "awesome_dashboard.DashboardLoader"; +} + +registry.category("actions").add("awesome_dashboard.dashboard", DashboardAction); diff --git a/awesome_dashboard/static/src/dashboard_action.xml b/awesome_dashboard/static/src/dashboard_action.xml new file mode 100644 index 00000000000..5beaede125b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.xml @@ -0,0 +1,9 @@ + + + + + + From c9e3259e7263c35eec8ef07d74538d8d1dec02b9 Mon Sep 17 00:00:00 2001 From: "Aditi Pawar(adpaw)" Date: Fri, 15 May 2026 11:35:07 +0530 Subject: [PATCH 25/26] [IMP] estate: Added multiple user groups Added three groups in admin i.e user, agent and manager Added this data via security.xml file in data folder and adding module category The user has all the basic access rights like creating, updating properties, offers but cannot delete anything. The agent can read, write the properties offers but cannot accept or refuse. The manager has full property rights but cannot add/delete. --- estate/__manifest__.py | 1 + estate/models/estate_property.py | 4 +-- estate/models/estate_property_issues.py | 7 +++-- estate/models/estate_property_offer.py | 5 +++ estate/models/estate_property_types.py | 2 +- estate/security/estate_security.xml | 32 ++++++++++++++++++++ estate/security/ir.model.access.csv | 11 +++++-- estate/views/estate_property_offer_views.xml | 6 ++-- 8 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 estate/security/estate_security.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c4a3b3756bd..92077c9d555 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,6 +6,7 @@ 'base', 'calendar', 'mail' ], 'data': [ + 'security/estate_security.xml', 'security/ir.model.access.csv', 'data/mail_template.xml', 'views/estate_property_visit_views.xml', diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ecb61c4be3d..9d715d144ae 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -28,7 +28,7 @@ class EstateProperty(models.Model): ('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")], help="Direction the garden faces" ) - tag_ids = fields.Many2many('estate.property.tag', compute='_dynamic_tags', string="Tags") + tag_ids = fields.Many2many('estate.property.tag', compute='_dynamic_tag_ids', string="Tags") 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) @@ -79,7 +79,7 @@ def _compute_best_price(self): record.best_price = 0.0 @api.depends('expected_price', 'offer_ids', 'sold_within') - def _dynamic_tags(self): + def _dynamic_tag_ids(self): all_tags = self.env['estate.property.tag'].search([('name', 'in', ('High Value', 'Quick Sale', 'Low Interest'))]) if 'High Value' not in all_tags.mapped('name'): diff --git a/estate/models/estate_property_issues.py b/estate/models/estate_property_issues.py index cdd8abc5515..c12fe7146ab 100644 --- a/estate/models/estate_property_issues.py +++ b/estate/models/estate_property_issues.py @@ -19,8 +19,11 @@ class EstatepropertyIssues(models.Model): ) issue_state = fields.Selection( selection=[ - ('new', "New"), ('in progress', "In Progress"), - ('resolved', "Resolved"), ('cancelled', "Cancelled")], + ('new', "New"), + ('in progress', "In Progress"), + ('resolved', "Resolved"), + ('cancelled', "Cancelled") + ], default='new', string="Status" ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index d65be4bbf84..dee5838e7c4 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ from datetime import timedelta + from odoo import api, fields, models from odoo.exceptions import UserError @@ -77,6 +78,8 @@ def create(self, vals_list): return super().create(vals_list) def action_accept(self): + if self.env.user.has_group('estate.group_estate_agent'): + raise UserError("Agents can add offers but cannot accept them.") for record in self: if record.property_id.state == 'sold': raise UserError("This property is already sold!") @@ -93,6 +96,8 @@ def action_accept(self): return True def action_refuse(self): + if self.env.user.has_group('estate.group_estate_agent'): + raise UserError("Agents can add offers but cannot refuse them.") for record in self: record.status = 'refused' return True diff --git a/estate/models/estate_property_types.py b/estate/models/estate_property_types.py index 261732401a2..7a3b3c8a6f5 100644 --- a/estate/models/estate_property_types.py +++ b/estate/models/estate_property_types.py @@ -17,7 +17,7 @@ class EstatePropertyType(models.Model): 'A property type with this name already exists.' ) - @api.depends('offer_ids') + @api.depends('offer_ids.price') def _compute_offer_count(self): for record in self: record.offer_count = len(record.offer_ids) diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..0dedd414151 --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,32 @@ + + + Estate + 130 + + + + Estate + 1 + + + + + User + + + + + Agent + + + + + Manager + + + + + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 5c477b276a0..bd34e93304f 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,7 +1,14 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_user,access_estate_property_user,model_estate_property,estate.group_estate_users,1,1,1,0 +access_estate_property_agent,access_estate_property_agent,model_estate_property,estate.group_estate_agent,1,1,1,0 +access_estate_property_manager,access_estate_property_manager,model_estate_property,estate.group_estate_manager,1,1,1,1 + access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 -access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 + +access_estate_property_offer_user,access_estate_property_offer,model_estate_property_offer,estate.group_estate_users,1,1,1,0 +access_estate_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate.group_estate_agent,1,1,1,0 +access_estate_property_offer_manager,access_estate_property_offer_manager,model_estate_property_offer,estate.group_estate_manager,1,1,0,0 + access_estate_property_visit,access_estate_property_visit,model_estate_property_visit,base.group_user,1,1,1,1 access_estate_property_issues,access_estate_property_issues,model_estate_property_issues,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 3da79dea184..8f22ca8897b 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -15,8 +15,10 @@ - + + + diff --git a/second_uom/static/src/second_uom_popup/second_uom_popup.js b/second_uom/static/src/second_uom_popup/second_uom_popup.js new file mode 100644 index 00000000000..43164cf6f90 --- /dev/null +++ b/second_uom/static/src/second_uom_popup/second_uom_popup.js @@ -0,0 +1,14 @@ +import { NumberPopup } from "@point_of_sale/app/components/popups/number_popup/number_popup"; + +export class SecondUomPopup extends NumberPopup { + static template = "second_uom.SecondUomPopup"; + static props = { + ...NumberPopup.props, + uomName: { type: String }, + }; + + confirm() { + this.props.getPayload(this.state.buffer); + this.props.close(); + } +} diff --git a/second_uom/static/src/second_uom_popup/second_uom_popup.xml b/second_uom/static/src/second_uom_popup/second_uom_popup.xml new file mode 100644 index 00000000000..17685b3236d --- /dev/null +++ b/second_uom/static/src/second_uom_popup/second_uom_popup.xml @@ -0,0 +1,15 @@ + + + + +
+ Unit: +
+
+ + + () + + +
+
diff --git a/second_uom/views/product_template_views.xml b/second_uom/views/product_template_views.xml new file mode 100644 index 00000000000..401d09c00c1 --- /dev/null +++ b/second_uom/views/product_template_views.xml @@ -0,0 +1,19 @@ + + + + product.template.form.inherit.second.uom + product.template + + + + + + + + + \ No newline at end of file