From f8cfbe4027f75986b8251940dea96e59058f3a0c Mon Sep 17 00:00:00 2001 From: habar Date: Tue, 10 Mar 2026 19:06:07 +0530 Subject: [PATCH 01/28] [ADD] estate: add initial module and property model Define the core 'estate.property' model and module manifest. This foundation allows managing listings with essential fields like price, area, and availability. --- estate/__init__.py | 1 + estate/__manifest__.py | 12 ++++++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 20 ++++++++++++++++++++ 4 files changed, 34 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..9c14ad3b6fc --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,12 @@ +{ + 'name': "estate", + 'version': '1.0', + 'depends': ['base'], + 'author': "Harshvardhan", + 'category': 'Category', + 'description': """ + This is the sample module for practise + """, + 'application': True, + 'license': 'LGPL-3', +} 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..e408a012f7d --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,20 @@ +from odoo import models, fields + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Real Estate Property' + + name = fields.Char(string="Name", required=True) + description = fields.Text(string="Description") + postcode = fields.Char(string="Postcode") + date_availability = fields.Date(string="Date") + expected_price = fields.Float(string="Expected Price", required=True) + selling_price = fields.Float(string="Selling Price") + bedrooms = fields.Integer(string="Bedrooms") + living_area = fields.Integer(string="Living Area") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area") + garden_orientation = fields.Selection([('north', 'North'), ('east', 'East'), ('west', 'West'), ('south', 'South')]) + \ No newline at end of file From ada46fd524d5c2a156d3c6038e19e8c5463cfa7c Mon Sep 17 00:00:00 2001 From: habar Date: Wed, 11 Mar 2026 18:32:14 +0530 Subject: [PATCH 02/28] [IMP] estate: grant basic access rights for users Implement security access rights (ACL) for the estate module. This is required to allow standard users to create and manage properties and offers via the UI, preventing access error messages on module installation. --- estate/__manifest__.py | 10 +++++++--- estate/models/estate_property.py | 11 ++++++++--- estate/security/ir.model.access.csv | 2 ++ 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9c14ad3b6fc..05273d27a5d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,11 +2,15 @@ 'name': "estate", 'version': '1.0', 'depends': ['base'], - 'author': "Harshvardhan", - 'category': 'Category', + 'author': "habar", + 'category': 'Tutorials', 'description': """ - This is the sample module for practise + This is the sample module for practise. """, + 'data': [ + 'security/ir.model.access.csv', + ], 'application': True, 'license': 'LGPL-3', + 'installable': True, } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index e408a012f7d..268ee66e8e0 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,9 +1,10 @@ from odoo import models, fields + class EstateProperty(models.Model): _name = 'estate.property' _description = 'Real Estate Property' - + name = fields.Char(string="Name", required=True) description = fields.Text(string="Description") postcode = fields.Char(string="Postcode") @@ -16,5 +17,9 @@ class EstateProperty(models.Model): garage = fields.Boolean(string="Garage") garden = fields.Boolean(string="Garden") garden_area = fields.Integer(string="Garden Area") - garden_orientation = fields.Selection([('north', 'North'), ('east', 'East'), ('west', 'West'), ('south', 'South')]) - \ No newline at end of file + garden_orientation = fields.Selection([ + ('north', 'North'), + ('east', 'East'), + ('west', 'West'), + ('south', 'South') + ]) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..0e11f47e58d --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From 88deabfe75fecf120a717a0473d4462d68f517c0 Mon Sep 17 00:00:00 2001 From: habar Date: Thu, 12 Mar 2026 18:46:41 +0530 Subject: [PATCH 03/28] [IMP] estate: add actions and menus for property model Introduce the UI entry points for the real estate module. Defined the window action and menu hierarchy to allow users to view and manage property records from the web interface. --- estate/__manifest__.py | 4 +++- estate/models/estate_property.py | 23 ++++++++++++++++------- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 24 ++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 8 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 05273d27a5d..59f3f604002 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,5 +1,5 @@ { - 'name': "estate", + 'name': "Estate", 'version': '1.0', 'depends': ['base'], 'author': "habar", @@ -9,6 +9,8 @@ """, 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'application': True, 'license': 'LGPL-3', diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 268ee66e8e0..d5a08f3a51b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,25 +1,34 @@ from odoo import models, fields +from dateutil.relativedelta import relativedelta class EstateProperty(models.Model): _name = 'estate.property' _description = 'Real Estate Property' - - name = fields.Char(string="Name", required=True) + + name = fields.Char(string="Title", required=True) description = fields.Text(string="Description") postcode = fields.Char(string="Postcode") - date_availability = fields.Date(string="Date") + date_availability = fields.Date(string="Available From", copy=False, default=fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(string="Expected Price", required=True) - selling_price = fields.Float(string="Selling Price") - bedrooms = fields.Integer(string="Bedrooms") - living_area = fields.Integer(string="Living Area") + selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) + bedrooms = fields.Integer(string="Bedrooms", default=2) + living_area = fields.Integer(string="Living Area (spm)") facades = fields.Integer(string="Facades") garage = fields.Boolean(string="Garage") garden = fields.Boolean(string="Garden") - garden_area = fields.Integer(string="Garden Area") + garden_area = fields.Integer(string="Garden Area (spm)") garden_orientation = fields.Selection([ ('north', 'North'), ('east', 'East'), ('west', 'West'), ('south', 'South') ]) + active = fields.Boolean(string="Active", default=True) + state = fields.Selection([ + ('new', 'New'), + ('offerreceived', 'Offer Received'), + ('offeraccepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], string="State", copy=False, default='new') diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..99abf1350f5 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..35db6dafd91 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,24 @@ + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + Properties + estate.property + list,form + + From f2c43b540f12a179f3a4f7f68162e4c102267303 Mon Sep 17 00:00:00 2001 From: habar Date: Mon, 16 Mar 2026 18:45:04 +0530 Subject: [PATCH 04/28] [IMP] estate: add form and search views Introduce custom layouts for property management. The form view provides a structured data entry interface, while the search view enables filtering by key attributes like price and status. --- estate/__manifest__.py | 6 +-- estate/models/estate_property.py | 8 ++-- estate/security/ir.model.access.csv | 2 +- estate/views/estate_property_views.xml | 56 ++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 59f3f604002..c238e0514cd 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,9 +8,9 @@ This is the sample module for practise. """, 'data': [ - 'security/ir.model.access.csv', - 'views/estate_property_views.xml', - 'views/estate_menus.xml', + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'application': True, 'license': 'LGPL-3', diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d5a08f3a51b..7aba45c36e2 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ -from odoo import models, fields from dateutil.relativedelta import relativedelta +from odoo import fields, models class EstateProperty(models.Model): @@ -11,7 +11,7 @@ class EstateProperty(models.Model): postcode = fields.Char(string="Postcode") date_availability = fields.Date(string="Available From", copy=False, default=fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(string="Expected Price", required=True) - selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) + selling_price = fields.Float(string="Selling Price", copy=False) bedrooms = fields.Integer(string="Bedrooms", default=2) living_area = fields.Integer(string="Living Area (spm)") facades = fields.Integer(string="Facades") @@ -27,8 +27,8 @@ class EstateProperty(models.Model): active = fields.Boolean(string="Active", default=True) state = fields.Selection([ ('new', 'New'), - ('offerreceived', 'Offer Received'), - ('offeraccepted', 'Offer Accepted'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled') ], string="State", copy=False, default='new') diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 0e11f47e58d..32389642d4f 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 -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 diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 35db6dafd91..14e416db34c 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,5 +1,44 @@ + + estate.property.form + estate.property + +
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ estate.property.list estate.property @@ -16,6 +55,23 @@ + + estate.property.search + estate.property + + + + + + + + + + + + + + Properties estate.property From 8a41f902a324ee7fac3529774548a72ad4eb8cc6 Mon Sep 17 00:00:00 2001 From: habar Date: Tue, 17 Mar 2026 18:59:42 +0530 Subject: [PATCH 05/28] [IMP] estate: implement relational models and data mapping Introduce core relations for property management. Added property types, tags, and offer tracking to support complex data structures. This enables interaction through Many2one, Many2many, and One2many relationships. This completes Chapter 7 by linking properties with their associated types, tags, and offers. --- estate/__manifest__.py | 3 +++ estate/models/__init__.py | 3 +++ estate/models/estate_property.py | 25 +++++++++++-------- estate/models/estate_property_offer.py | 14 +++++++++++ estate/models/estate_property_tag.py | 8 ++++++ estate/models/estate_property_type.py | 8 ++++++ estate/security/ir.model.access.csv | 3 +++ estate/views/estate_menus.xml | 4 +++ estate/views/estate_property_offer_views.xml | 26 ++++++++++++++++++++ estate/views/estate_property_tag_views.xml | 8 ++++++ estate/views/estate_property_type_views.xml | 8 ++++++ estate/views/estate_property_views.xml | 25 +++++++++++++------ 12 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c238e0514cd..4f106ce9382 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,6 +10,9 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ], 'application': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 7aba45c36e2..fc9b7e100f5 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,7 +5,7 @@ class EstateProperty(models.Model): _name = 'estate.property' _description = 'Real Estate Property' - + name = fields.Char(string="Title", required=True) description = fields.Text(string="Description") postcode = fields.Char(string="Postcode") @@ -19,16 +19,21 @@ class EstateProperty(models.Model): garden = fields.Boolean(string="Garden") garden_area = fields.Integer(string="Garden Area (spm)") garden_orientation = fields.Selection([ - ('north', 'North'), - ('east', 'East'), - ('west', 'West'), - ('south', 'South') + ('north', "North"), + ('east', "East"), + ('west', "West"), + ('south', "South") ]) active = fields.Boolean(string="Active", default=True) state = fields.Selection([ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('cancelled', 'Cancelled') + ('new', "New"), + ('offer_received', "Offer Received"), + ('offer_accepted', "Offer Accepted"), + ('sold', "Sold"), + ('cancelled', "Cancelled") ], string="State", copy=False, default='new') + property_type_id = fields.Many2one('estate.property.type', string="Property Type", ondelete="cascade") + sales_person_id = fields.Many2one('res.users', string='Salesman', ondelete='cascade') + buyer_id = fields.Many2one('res.partner', string='Buyer', ondelete='cascade') + property_tag_ids = fields.Many2many('estate.property.tag') + 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..aacaf0e29a0 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real Estate Offer" + + price = fields.Float(string='Price') + status = fields.Selection([ + ('accepted', 'Accepted'), + ('refused', 'Refused') + ], string='Status', 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_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..01da11e0ec2 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'Real Estate Tag' + + name = fields.Char(string='Tag', required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..1f90b1c59e2 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate property type" + + name = fields.Char(string="Type", required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 32389642d4f..89f97c50842 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 99abf1350f5..a40ec4a5649 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,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..88694b2f445 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,26 @@ + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + +
+ + + estate.property.offer.list + estate.property.offer + + + + + + + + +
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..4dcd3674e34 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..dd480955e4e --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,8 @@ + + + + Property Type + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 14e416db34c..031276ee7a0 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -7,16 +7,16 @@

+

- - - - - - - - + + + + + + + @@ -33,6 +33,15 @@ + + + + + + + + +
From 7833b1a0ae75b4633ec14e010827e0353bba2608 Mon Sep 17 00:00:00 2001 From: habar Date: Tue, 24 Mar 2026 18:17:37 +0530 Subject: [PATCH 06/28] [IMP] estate: added computed field automatic area calculation Add a new field that automatically calculates the total property area. By linking the living area and garden area, the system now updates the total size whenever either value changes. This removes the need for manual entry and prevents mistakes. --- estate/models/estate_property.py | 10 ++++++++-- estate/views/estate_property_views.xml | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index fc9b7e100f5..2076549bbbe 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -9,7 +9,7 @@ class EstateProperty(models.Model): name = fields.Char(string="Title", required=True) description = fields.Text(string="Description") postcode = fields.Char(string="Postcode") - date_availability = fields.Date(string="Available From", copy=False, default=fields.Date.today() + relativedelta(months=3)) + date_availability = fields.Date(string="Available From", copy=False, default=lambda self: fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(string="Expected Price", required=True) selling_price = fields.Float(string="Selling Price", copy=False) bedrooms = fields.Integer(string="Bedrooms", default=2) @@ -18,6 +18,7 @@ class EstateProperty(models.Model): garage = fields.Boolean(string="Garage") garden = fields.Boolean(string="Garden") garden_area = fields.Integer(string="Garden Area (spm)") + total_area = fields.Float(string="Total Area (sqm)", compute="_computed_total_area", store=True) garden_orientation = fields.Selection([ ('north', "North"), ('east', "East"), @@ -37,3 +38,8 @@ class EstateProperty(models.Model): buyer_id = fields.Many2one('res.partner', string='Buyer', ondelete='cascade') property_tag_ids = fields.Many2many('estate.property.tag') offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") + + @api.depends("living_area", "garden_area") + def _computed_total_area(self): + for rec in self: + rec.total_area = rec.living_area + rec.garden_area diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 031276ee7a0..20b248a9b01 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -30,6 +30,7 @@ + @@ -52,7 +53,7 @@ estate.property.list estate.property - + From 85bc1c97d169d5e82387756fc26b9ba74b2fe944 Mon Sep 17 00:00:00 2001 From: habar Date: Wed, 25 Mar 2026 21:53:29 +0530 Subject: [PATCH 07/28] [IMP] estate: add computed field for best price Implement business logic to automatically track the highest bid for a property. Using a computed field, the system now identifies the 'best price' from all related offers, ensuring sellers see the most competitive deal at a glance. --- estate/models/estate_property.py | 8 ++++++++ estate/views/estate_property_views.xml | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2076549bbbe..04d155d0350 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from dateutil.relativedelta import relativedelta + from odoo import api, fields, models @@ -38,8 +39,15 @@ class EstateProperty(models.Model): buyer_id = fields.Many2one('res.partner', string='Buyer', ondelete='cascade') property_tag_ids = fields.Many2many('estate.property.tag') offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") + best_price = fields.Float(string="Best Offer", compute="_computed_best_offer", store=True) @api.depends("living_area", "garden_area") def _computed_total_area(self): for rec in self: rec.total_area = rec.living_area + rec.garden_area + + @api.depends("offer_ids.price") + def _computed_best_offer(self): + for rec in self: + prices = rec.offer_ids.mapped("price") + rec.best_price = max(prices) if prices else 0.0 diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 20b248a9b01..2324e6ae535 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -16,6 +16,7 @@ + @@ -35,7 +36,7 @@ - + @@ -61,6 +62,8 @@ + + @@ -76,6 +79,8 @@ + + From bfa85bf6ffe043f71dd3380565dd251f1b8e83cc Mon Sep 17 00:00:00 2001 From: habar Date: Thu, 26 Mar 2026 18:38:55 +0530 Subject: [PATCH 08/28] [IMP] estate: add validity and deadline to offers Introduce expiration logic for property offers by adding validity and deadline fields. This allows the system to automatically calculate when an offer is no longer valid. The compute and inverse methods ensure synchronization between the duration (days) and the specific end date. --- estate/models/estate_property.py | 25 +++++++++++++------- estate/models/estate_property_offer.py | 19 +++++++++++++-- estate/views/estate_property_offer_views.xml | 4 ++++ 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 04d155d0350..679d9ae0cdd 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,18 +8,18 @@ class EstateProperty(models.Model): _description = 'Real Estate Property' name = fields.Char(string="Title", required=True) - description = fields.Text(string="Description") - postcode = fields.Char(string="Postcode") + description = fields.Text() + postcode = fields.Char() date_availability = fields.Date(string="Available From", copy=False, default=lambda self: fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(string="Expected Price", required=True) selling_price = fields.Float(string="Selling Price", copy=False) - bedrooms = fields.Integer(string="Bedrooms", default=2) + bedrooms = fields.Integer(default=2) living_area = fields.Integer(string="Living Area (spm)") - facades = fields.Integer(string="Facades") - garage = fields.Boolean(string="Garage") - garden = fields.Boolean(string="Garden") + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() garden_area = fields.Integer(string="Garden Area (spm)") - total_area = fields.Float(string="Total Area (sqm)", compute="_computed_total_area", store=True) + total_area = fields.Float(string="Total Area (sqm)", compute="_computed_total_area") garden_orientation = fields.Selection([ ('north', "North"), ('east', "East"), @@ -33,13 +33,13 @@ class EstateProperty(models.Model): ('offer_accepted', "Offer Accepted"), ('sold', "Sold"), ('cancelled', "Cancelled") - ], string="State", copy=False, default='new') + ], copy=False, default='new') property_type_id = fields.Many2one('estate.property.type', string="Property Type", ondelete="cascade") sales_person_id = fields.Many2one('res.users', string='Salesman', ondelete='cascade') buyer_id = fields.Many2one('res.partner', string='Buyer', ondelete='cascade') property_tag_ids = fields.Many2many('estate.property.tag') offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") - best_price = fields.Float(string="Best Offer", compute="_computed_best_offer", store=True) + best_price = fields.Float(string="Best Offer", compute="_computed_best_offer", search="_search_best_offer", store=False) @api.depends("living_area", "garden_area") def _computed_total_area(self): @@ -51,3 +51,10 @@ def _computed_best_offer(self): for rec in self: prices = rec.offer_ids.mapped("price") rec.best_price = max(prices) if prices else 0.0 + + def _search_best_offer(self, operator, value): + return [ + '&', + ('offer_ids.price', '>', 10000), + ('offer_ids.price', operator, value) + ] diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index aacaf0e29a0..2c08b12128c 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -9,6 +11,19 @@ class EstatePropertyOffer(models.Model): status = fields.Selection([ ('accepted', 'Accepted'), ('refused', 'Refused') - ], string='Status', copy=False) + ], copy=False) partner_id = fields.Many2one('res.partner', required=True) property_id = fields.Many2one('estate.property', required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date(string="Deadline", compute="_compute_deadline_method", inverse="_inverse_deadline_method") + + @api.depends("create_date", "validity") + def _compute_deadline_method(self): + for rec in self: + create_date = rec.create_date.date() if rec.create_date else fields.Date.today() + rec.date_deadline = create_date + relativedelta(days=rec.validity) + + def _inverse_deadline_method(self): + for rec in self: + create_date = rec.create_date.date() if rec.create_date else fields.Date.today() + rec.validity = relativedelta(rec.date_deadline, create_date).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 88694b2f445..f25a16099dd 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -7,6 +7,8 @@
+ + @@ -19,6 +21,8 @@ + + From 86ada8ba51126cdfa91baf47a92b65f4386845a1 Mon Sep 17 00:00:00 2001 From: habar Date: Fri, 27 Mar 2026 18:44:18 +0530 Subject: [PATCH 09/28] [IMP] estate: added onchange method in the estate property model 1) Define _onchange_garden() to automatically set default garden_area and garden_orientation when the garden is enabled. 2) Clear these fields when the garden is disabled. 3) Add a warning notification when the garden checkbox is checked. --- estate/models/estate_property.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 679d9ae0cdd..39d10d115fd 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -14,11 +14,11 @@ class EstateProperty(models.Model): expected_price = fields.Float(string="Expected Price", required=True) selling_price = fields.Float(string="Selling Price", copy=False) bedrooms = fields.Integer(default=2) - living_area = fields.Integer(string="Living Area (spm)") + living_area = fields.Float(string="Living Area (spm)") facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer(string="Garden Area (spm)") + garden_area = fields.Float(string="Garden Area (spm)") total_area = fields.Float(string="Total Area (sqm)", compute="_computed_total_area") garden_orientation = fields.Selection([ ('north', "North"), @@ -48,9 +48,8 @@ def _computed_total_area(self): @api.depends("offer_ids.price") def _computed_best_offer(self): - for rec in self: - prices = rec.offer_ids.mapped("price") - rec.best_price = max(prices) if prices else 0.0 + prices = self.offer_ids.mapped("price") + self.best_price = max(prices) if prices else 0.0 def _search_best_offer(self, operator, value): return [ @@ -58,3 +57,20 @@ def _search_best_offer(self, operator, value): ('offer_ids.price', '>', 10000), ('offer_ids.price', operator, value) ] + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + + return { + 'warning': { + 'title': "Garden Enabled", + 'message': "Default area set to 10 and orientation north", + 'type': "notification" + } + } + else: + self.garden_area = 0 + self.garden_orientation = False From 109c0d53bc712d7d7090c5ba6c46d31e6db7c9e8 Mon Sep 17 00:00:00 2001 From: habar Date: Mon, 30 Mar 2026 18:43:27 +0530 Subject: [PATCH 10/28] [IMP] estate: define actions & buttons Add 'Cancel' and 'Sold' buttons to manage property status transitions. A property cannot be cancelled once sold, and vice versa, to maintain data integrity. User-friendly error messages (Exceptions) are triggered to prevent invalid state changes via the UI. --- estate/models/estate_property.py | 15 +++++++++++++++ estate/views/estate_property_views.xml | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 39d10d115fd..17be0a2f28c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.exceptions import UserError class EstateProperty(models.Model): @@ -74,3 +75,17 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = False + + def set_property_cancelled(self): + for rec in self: + if rec.state == 'cancelled': + raise UserError("Sold property can not be cancelled.") + rec.state = 'cancelled' + return True + + def set_property_sold(self): + for rec in self: + if rec.state == 'cancelled': + raise UserError("Cancelled properties can not be sold.") + rec.state = 'sold' + return True diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 2324e6ae535..6c0cfaed397 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,11 +5,16 @@ estate.property
+
+ + +

+ @@ -81,7 +86,7 @@ - + From 5c962b17189d57e3a2f8d08490aeb74de9d1d1f4 Mon Sep 17 00:00:00 2001 From: habar Date: Tue, 31 Mar 2026 18:10:49 +0530 Subject: [PATCH 11/28] [IMP] estate: added methods to the estate property offer This functionality define to accept & refuse offer once offer is accepted it set selling price and buyer to the corresponding property. generates error if more than one offer is try to accept, added buttons to the corresponding actions used 'checked' & 'unchecked' icons. --- estate/models/estate_property_offer.py | 17 +++++++++++++++++ estate/views/estate_property_offer_views.xml | 2 ++ 2 files changed, 19 insertions(+) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 2c08b12128c..78b2a5e7272 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): @@ -27,3 +28,19 @@ def _inverse_deadline_method(self): for rec in self: create_date = rec.create_date.date() if rec.create_date else fields.Date.today() rec.validity = relativedelta(rec.date_deadline, create_date).days + + def action_accept_offer(self): + for rec in self: + if any(i.status == "accepted" for i in rec.property_id.offer_ids): + raise UserError("Offer already accepted for given property.") + + rec.status = "accepted" + rec.property_id.buyer_id = rec.partner_id.id + rec.property_id.selling_price = rec.price + rec.property_id.state = "offer_accepted" + return True + + def action_refuse_offer(self): + for rec in self: + rec.status = "refused" + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index f25a16099dd..c0afbad9e16 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -23,6 +23,8 @@ + - + +

@@ -85,7 +85,6 @@ - From cf9f6d5c8209bcf0462f2cd24103ce3f6b37a184 Mon Sep 17 00:00:00 2001 From: habar Date: Mon, 6 Apr 2026 19:32:36 +0530 Subject: [PATCH 13/28] [IMP] estate: added 90% minimum selling price constraint Added a Python constraint using float_compare & float_is_zero to ensure accurate float comparison & skips cases where the selling price is zero, as it represents the initial state. It also ensures that the selling price is always greater than or equal to 90% of the expected price. --- estate/models/estate_property.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ce6114aab2a..2d55399061a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,8 +1,8 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models -from odoo.exceptions import UserError -from odoo.tools import _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools import _, float_compare, float_is_zero class EstateProperty(models.Model): @@ -10,7 +10,7 @@ class EstateProperty(models.Model): _description = 'Real Estate Property' name = fields.Char(string="Title", required=True) - description = fields.Text(translate=True) + description = fields.Text() postcode = fields.Char() date_availability = fields.Date(string="Available From", copy=False, default=lambda self: fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(string="Expected Price", required=True) @@ -97,3 +97,12 @@ def action_property_sold(self): raise UserError(_("%s property of %s can not be sold.", rec.state, rec.name)) rec.state = 'sold' return True + + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for rec in self: + if float_is_zero(rec.selling_price, precision_digits=2): + continue + limit_price = rec.expected_price * 0.9 + if float_compare(rec.selling_price, limit_price, precision_digits=2) < 0: + raise ValidationError(_("selling price cannot be lower than 90% of the expected price")) From bb952727ae9433180e9b64082ac83733eae182d3 Mon Sep 17 00:00:00 2001 From: habar Date: Wed, 8 Apr 2026 20:31:44 +0530 Subject: [PATCH 14/28] [IMP] estate: improved UI, ordering, search, and property management Enhanced UI & UX: Added one2many inline views, stage-tracking widgets, color tags, and status decorations (green for offers, muted for sold). Advanced Logic: Implemented manual/model ordering, made list views editable, and added conditional visibility/readonly rules for Sold, Cancelled, or Accepted properties. Search & Stats: Set 'Available' as the default filter, updated living area search logic, and added a Stat button on Property Types to track offer counts via stored related fields. --- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 19 +++++---- estate/models/estate_property_offer.py | 10 +++++ estate/models/estate_property_tag.py | 2 + estate/models/estate_property_type.py | 12 +++++- estate/views/estate_property_offer_views.xml | 15 +++++-- estate/views/estate_property_tag_views.xml | 11 +++++ estate/views/estate_property_type_views.xml | 44 ++++++++++++++++++++ estate/views/estate_property_views.xml | 37 ++++++++-------- 9 files changed, 120 insertions(+), 32 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 4f106ce9382..472b70f3761 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,9 +10,9 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', - 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ], 'application': True, diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2d55399061a..db1b0510399 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,6 +8,7 @@ class EstateProperty(models.Model): _name = 'estate.property' _description = 'Real Estate Property' + _order = "id desc" name = fields.Char(string="Title", required=True) description = fields.Text() @@ -84,6 +85,15 @@ def _onchange_garden(self): self.garden_area = 0 self.garden_orientation = False + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for rec in self: + if float_is_zero(rec.selling_price, precision_digits=2): + continue + limit_price = rec.expected_price * 0.9 + if float_compare(rec.selling_price, limit_price, precision_digits=2) < 0: + raise ValidationError(_("selling price cannot be lower than 90% of the expected price")) + def action_property_cancelled(self): for rec in self: if rec.state == 'sold': @@ -97,12 +107,3 @@ def action_property_sold(self): raise UserError(_("%s property of %s can not be sold.", rec.state, rec.name)) rec.state = 'sold' return True - - @api.constrains('selling_price', 'expected_price') - def _check_selling_price(self): - for rec in self: - if float_is_zero(rec.selling_price, precision_digits=2): - continue - limit_price = rec.expected_price * 0.9 - if float_compare(rec.selling_price, limit_price, precision_digits=2) < 0: - raise ValidationError(_("selling price cannot be lower than 90% of the expected price")) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 9f571195bc8..dcaef53ef91 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -7,6 +7,7 @@ class EstatePropertyOffer(models.Model): _name = "estate.property.offer" _description = "Real Estate Offer" + _order = "price desc" price = fields.Float(string='Price') status = fields.Selection([ @@ -17,6 +18,7 @@ class EstatePropertyOffer(models.Model): property_id = fields.Many2one('estate.property', required=True) validity = fields.Integer(default=7) date_deadline = fields.Date(string="Deadline", compute="_compute_deadline_method", inverse="_inverse_deadline_method") + property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) _check_offer_price = models.Constraint( 'CHECK(price >= 1)', @@ -34,6 +36,14 @@ def _inverse_deadline_method(self): create_date = rec.create_date.date() if rec.create_date else fields.Date.today() rec.validity = relativedelta(rec.date_deadline, create_date).days + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + for record in records: + if record.property_id.state == 'new': + record.property_id.state = 'offer_received' + return records + def action_accept_offer(self): for rec in self: if any(i.status == "accepted" for i in rec.property_id.offer_ids): diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 9ca81dce04c..68827589cc6 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -4,8 +4,10 @@ class EstatePropertyTag(models.Model): _name = 'estate.property.tag' _description = 'Real Estate Tag' + _order = "name" name = fields.Char(string='Tag', required=True) + color = fields.Integer() _unique_tag_name = models.Constraint( 'unique(name)', diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 834537697fa..3d96d065221 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,13 +1,23 @@ -from odoo import fields, models +from odoo import api, fields, models class EstatePropertyType(models.Model): _name = "estate.property.type" _description = "Estate property type" + _order = "sequence, name" name = fields.Char(string="Type", required=True) + property_ids = fields.One2many('estate.property', 'property_type_id') + sequence = fields.Integer() + offer_ids = fields.One2many('estate.property.offer', 'property_type_id') + offer_count = fields.Integer(string="Offers Count", compute="_compute_offer_count") _unique_type_name = models.Constraint( 'unique(name)', 'The property type name must be unique.' ) + + @api.depends("offer_ids") + def _compute_offer_count(self): + for rec in self: + rec.offer_count = len(rec.offer_ids) diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index c0afbad9e16..854e2b8d9de 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,5 +1,13 @@ + + Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + {'default_property_type_id': active_id} + + estate.property.offer.form estate.property.offer @@ -18,14 +26,13 @@ estate.property.offer.list estate.property.offer - + - + + +

+
+ + + + + + + + + + + +
+ +
+ + + + estate.property.type.list + estate.property.type + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index a6c9b93eaf7..448233f51dc 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,27 +1,34 @@ + + Properties + estate.property + list,form + {'search_default_available': 1} + + estate.property.form estate.property
- - + + +

-

+

- - + @@ -34,14 +41,13 @@ - - + + - - + @@ -59,14 +65,16 @@ estate.property.list estate.property - + + + - + @@ -82,6 +90,7 @@ + @@ -90,10 +99,4 @@ - - - Properties - estate.property - list,form - From 76e92f7caba671d5aacec4c20cf1c5f11ee603b3 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Sat, 11 Apr 2026 19:38:28 +0530 Subject: [PATCH 15/28] [IMP] estate: added business logic to CRUD operations Deletion protection:Added an ondelete restriction to prevent deleting properties unless their state is 'New' or 'Cancelled'. Offer creation logic: Automatically update property state to 'Offer Received' when a new offer is created. Added a validation error to prevent creating an offer with an amount lower than an existing one. --- estate/models/estate_property.py | 7 +++++++ estate/models/estate_property_offer.py | 11 ++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index db1b0510399..b941562644a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -94,6 +94,13 @@ def _check_selling_price(self): if float_compare(rec.selling_price, limit_price, precision_digits=2) < 0: raise ValidationError(_("selling price cannot be lower than 90% of the expected price")) + @api.ondelete(at_uninstall=False) + def _check_state(self): + for rec in self: + if rec.state not in ['new', 'cancelled']: + raise UserError(_("As this property state in %s therefor you can not delete it.", rec.state)) + return True + def action_property_cancelled(self): for rec in self: if rec.state == 'sold': diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index dcaef53ef91..168dbe4c6aa 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -38,11 +38,12 @@ def _inverse_deadline_method(self): @api.model_create_multi def create(self, vals_list): - records = super().create(vals_list) - for record in records: - if record.property_id.state == 'new': - record.property_id.state = 'offer_received' - return records + for rec in vals_list: + property_rec = self.env['estate.property'].browse(rec.get('property_id')) + if rec.get('price') < property_rec.best_price: + raise UserError("The offer must be higher than the existing offer.") + property_rec.state = "offer_received" + return super().create(vals_list) def action_accept_offer(self): for rec in self: From 58062c66837861ce1313c210d2f99310f7e73a5b Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Mon, 13 Apr 2026 17:28:23 +0530 Subject: [PATCH 16/28] [IMP] estate: extend res.users with property inheritance Inherit res.users model added property_ids field to display the list of properties linked to a salesperson. The field is One2many inverse of the field that references the salesperson in estate.property, Added domain to the field so it only lists the available properties. create res_users_view inherit 'base.view_users_form' using XPath to include a new notebook page for properties linked to the salesperson. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 2 +- estate/models/res_users.py | 7 +++++++ estate/views/res_users_views.xml | 15 +++++++++++++++ 5 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 472b70f3761..a32d6893cf2 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -14,6 +14,7 @@ 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', 'views/estate_menus.xml', + 'views/res_users_views.xml' ], 'application': True, 'license': 'LGPL-3', diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index b941562644a..4c0fbd73b3b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -38,7 +38,7 @@ class EstateProperty(models.Model): ('cancelled', "Cancelled") ], copy=False, default='new') property_type_id = fields.Many2one('estate.property.type', string="Property Type", ondelete="cascade") - sales_person_id = fields.Many2one('res.users', string='Salesman', ondelete='cascade') + sales_person_id = fields.Many2one('res.users', string='Sales Person', ondelete='cascade', default=lambda self: self.env.user) buyer_id = fields.Many2one('res.partner', string='Buyer', ondelete='cascade') property_tag_ids = fields.Many2many('estate.property.tag') offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..2dafdccb8f1 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many('estate.property', 'sales_person_id', domain=[('state', 'not in', ('sold', 'cancelled'))]) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..7b862e43e57 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.form.inherit.estate + res.users + + + + + + + + + + From 446d92c7584be273968555cc961fdde1007ac072 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Fri, 17 Apr 2026 18:04:19 +0530 Subject: [PATCH 17/28] [ADD] estate_account: add estate_account module for invoice generation Added a link module to handle invoice creation logic: Model: Inherited estate.property and overrode action_property_sold. Logic: Generates an account.move with move_type='out_invoice' using the property partner. Invoice Lines: Commission: 6% of the selling price. Fees: 100.00 flat administrative charge. --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 16 +++++++++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 26 ++++++++++++++++++++++++ 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..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..18917309f0f --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': "Estate Account", + 'version': '1.0', + 'summary': "Link module between Estate and Accounting", + 'description': """ + This module links the Estate module with Accounting. + It automatically creates a customer invoice when a property is sold. + """, + 'author': "Habar", + 'category': 'Real Estate', + 'depends': ['estate', 'account'], + 'data': [], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..09e9c0a5f43 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,26 @@ +from odoo import models, Command + + +class EstateAccountProperty(models.Model): + _inherit = "estate.property" + + def action_property_sold(self): + self.env['account.move'].create({ + 'partner_id': self.buyer_id.id, + 'move_type': 'out_invoice', + + 'invoice_line_ids': [ + Command.create({ + 'name': "6% Commission", + 'quantity': 1, + 'price_unit': self.selling_price * 0.06, + }), + Command.create({ + 'name': "Administrative Fees", + 'quantity': 1, + 'price_unit': 100.00, + }), + ], + }) + + return super().action_property_sold() From c882d5e5e0d22dd3138f2eebe7645ef9d60273c5 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Sun, 19 Apr 2026 20:31:29 +0530 Subject: [PATCH 18/28] [IMP] estate: add kanban view and visit scheduling Implement Kanban view using QWeb templates with conditional logic (t-if). Add property visit scheduling with List, Form, and Calendar views. Add constraint to prevent overlapping visits for the same property. Implement automatic Calendar event creation upon visit scheduling. Complete assigned group tasks and UI improvements. --- estate/__manifest__.py | 3 +- estate/models/__init__.py | 1 + estate/models/estate_property.py | 7 +++ estate/models/estate_property_visit.py | 65 ++++++++++++++++++++++++++ estate/security/ir.model.access.csv | 2 + estate/views/estate_property_views.xml | 40 +++++++++++++++- estate/views/estate_property_visit.xml | 49 +++++++++++++++++++ 7 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 estate/models/estate_property_visit.py create mode 100644 estate/views/estate_property_visit.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index a32d6893cf2..cbc8c686d7d 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,7 +1,7 @@ { 'name': "Estate", 'version': '1.0', - 'depends': ['base'], + 'depends': ['base', 'calendar'], 'author': "habar", 'category': 'Tutorials', 'description': """ @@ -9,6 +9,7 @@ """, 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_visit.xml', 'views/estate_property_views.xml', 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 9a2189b6382..3e674fc08aa 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -3,3 +3,4 @@ from . import estate_property_tag from . import estate_property_offer from . import res_users +from . import estate_property_visit diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4c0fbd73b3b..e9c50bc7dfc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -43,6 +43,13 @@ class EstateProperty(models.Model): property_tag_ids = fields.Many2many('estate.property.tag') offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") best_price = fields.Float(string="Best Offer", compute="_computed_best_offer") + visit_ids = fields.One2many('estate.property.visit', 'property_id', string="Visits") + visit_count = fields.Integer(compute="_compute_visit_count") + + @api.depends('visit_ids') + def _compute_visit_count(self): + for rec in self: + rec.visit_count = len(rec.visit_ids) _check_expected_price = models.Constraint( 'CHECK(expected_price >= 1)', diff --git a/estate/models/estate_property_visit.py b/estate/models/estate_property_visit.py new file mode 100644 index 00000000000..12eae195d14 --- /dev/null +++ b/estate/models/estate_property_visit.py @@ -0,0 +1,65 @@ +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class EstatePropertyVisit(models.Model): + _name = 'estate.property.visit' + _description = 'Property Visit' + _rec_name = 'property_id' + + property_id = fields.Many2one('estate.property', required=True, string="Property") + user_id = fields.Many2one(related="property_id.sales_person_id", string="Salesperson") + buyer_id = fields.Many2one('res.partner', required=True, string="Customer") + schedule_date = fields.Datetime(required=True, string="Schedule Datetime") + calendar_event_id = fields.Many2one('calendar.event', string="Calendar Event", ondelete='cascade') + state = fields.Selection([ + ('scheduled', 'Scheduled'), + ('done', 'Done'), + ('cancel', 'Cancelled'), + ], string="Status", default='scheduled', copy=False) + + def action_set_done(self): + self.state = 'done' + + def action_set_cancel(self): + self.state = 'cancel' + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + for rec in records: + event = self.env['calendar.event'].create({ + 'name': f"{rec.property_id.name} - {rec.buyer_id.name}", + 'start': rec.schedule_date, + 'stop': rec.schedule_date + relativedelta(hours=1), + 'user_id': rec.user_id.id, + 'partner_ids': [(4, rec.buyer_id.id)], + }) + rec.calendar_event_id = event.id + return records + + def write(self, vals): + res = super().write(vals) + if 'schedule_date' in vals or 'user_id' in vals: + for rec in self: + if rec.calendar_event_id: + rec.calendar_event_id.write({ + 'start': rec.schedule_date, + 'stop': rec.schedule_date + relativedelta(hours=1), + 'user_id': rec.user_id.id, + }) + return res + + @api.constrains('schedule_date', 'property_id') + def _check_visit_date(self): + for rec in self: + exists = self.search([ + ('property_id', '=', rec.property_id.id), + ('id', '!=', rec.id), + ('schedule_date', '=', rec.schedule_date) + ]) + + if exists: + raise ValidationError(f"This date({rec.schedule_date}) is already scheduled by an customer.") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 89f97c50842..fb1d98d4c34 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -3,3 +3,5 @@ access_estate_property,access_estate_property,model_estate_property,base.group_u 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_visit,access_estate_property_visit,model_estate_property_visit,base.group_user,1,1,1,1 + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 448233f51dc..d37fed690c6 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ Properties estate.property - list,form + list,form,kanban {'search_default_available': 1} @@ -18,6 +18,14 @@ +
+ +

@@ -28,7 +36,7 @@ - +
@@ -53,6 +61,7 @@ +
@@ -81,6 +90,33 @@
+ + estate.property.kanban + estate.property + + + + + +
+ +
+ Expected Price +
+
+ Best Price +
+
+ Selling Price +
+ +
+
+
+
+
+
+ estate.property.search estate.property diff --git a/estate/views/estate_property_visit.xml b/estate/views/estate_property_visit.xml new file mode 100644 index 00000000000..90b5b389e57 --- /dev/null +++ b/estate/views/estate_property_visit.xml @@ -0,0 +1,49 @@ + + + Visits + estate.property.visit + list,form + [('property_id', '=', active_id)] + {'default_property_id': active_id} + + + + estate.property.visit.form + estate.property.visit + + +
+
+ + + + + + + + + + + + + +
+
+ + + estate.property.visit.list + estate.property.visit + + + + + + + + +
From 830cc40aa3d05d040dd9e92f90a99dcbdaa7c659 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Wed, 29 Apr 2026 17:23:41 +0530 Subject: [PATCH 19/28] [IMP] awesome_owl: add owl components (counter, card) Implement counter component, add increment method to increase count & used useState hook as well as define template for it. Create card component define props (title, content) to pass information from parent to child component, understand the use of markup(). Similarly, Also add incrementSum (callback props) method to calculate sum of two counters. --- awesome_owl/static/src/card/card.js | 10 ++++++++++ awesome_owl/static/src/card/card.xml | 10 ++++++++++ awesome_owl/static/src/counter/counter.js | 17 +++++++++++++++++ awesome_owl/static/src/counter/counter.xml | 8 ++++++++ awesome_owl/static/src/playground.js | 15 ++++++++++++++- awesome_owl/static/src/playground.xml | 11 +++++++++-- 6 files changed, 68 insertions(+), 3 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..5ee0a9f3d2d --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl" + +export class Card extends Component { + static template = "awesome_owl.Card" + + static props = { + title: String, + content: String, + }; +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..69a84de1a6c --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,10 @@ + + +
+
+

+

+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..12e4195d62d --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,17 @@ +import { Component, useState } from "@odoo/owl" + +export class Counter extends Component { + static template = "awesome_owl.Counter" + + setup(){ + this.state = useState({ value: 0 }); + } + + increment(){ + this.state.value++; + + if(this.props.onChange){ + this.props.onChange(); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..be1a4612043 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,8 @@ + + +
+

Counter :

+ +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..e6d4c413eb6 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,18 @@ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card }; + + setup(){ + this.htmlContent = markup("some content.") + this.normalString = "

+ - From c502fc5f827d4a754753a3df4c34f8edefa88ce7 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Thu, 30 Apr 2026 21:46:14 +0530 Subject: [PATCH 20/28] [IMP] awesome_owl: implement interactive todo list Introduce TodoList and TodoItem components to manage tasks. The TodoList maintains a reactive state of todos, allowing users to add new tasks via an input field (on Enter key). --- awesome_owl/static/src/playground.js | 3 ++- awesome_owl/static/src/playground.xml | 3 +++ awesome_owl/static/src/todo/todo_item.js | 13 +++++++++++++ awesome_owl/static/src/todo/todo_item.xml | 8 ++++++++ awesome_owl/static/src/todo/todo_list.js | 23 +++++++++++++++++++++++ awesome_owl/static/src/todo/todo_list.xml | 10 ++++++++++ 6 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 awesome_owl/static/src/todo/todo_item.js create mode 100644 awesome_owl/static/src/todo/todo_item.xml create mode 100644 awesome_owl/static/src/todo/todo_list.js create mode 100644 awesome_owl/static/src/todo/todo_list.xml diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index e6d4c413eb6..0bb5c15773e 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,10 +1,11 @@ import { Component, markup, useState } from "@odoo/owl"; import { Counter } from "./counter/counter"; import { Card } from "./card/card"; +import { TodoList } from "./todo/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; - static components = { Counter, Card }; + static components = { Counter, Card, TodoList }; setup(){ this.htmlContent = markup("some content.") diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index acea21df2b6..e54afd61362 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -12,6 +12,9 @@


+ +

Todo List

+ diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js new file mode 100644 index 00000000000..b1204467035 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,13 @@ +import {Component} from "@odoo/owl" + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem" + + static props = { + todo:{ + id:Number, + description:String, + isCompleted:Boolean + } + } +} diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml new file mode 100644 index 00000000000..b8a420c1aa7 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,8 @@ + + +
+ . + +
+
+
diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js new file mode 100644 index 00000000000..5eeb8e57646 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,23 @@ +import {Component, useState} from "@odoo/owl" +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + setup() { + this.nextId = 1; + this.todos = useState([]); + } + + addTodo(ev) { + if (ev.keyCode === 13 && ev.target.value.trim() !== "") { + this.todos.push({ + id: this.nextId++, + description: ev.target.value, + isCompleted: false, + }); + ev.target.value = ""; + } + } +} diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml new file mode 100644 index 00000000000..3ebc8782aac --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,10 @@ + + + +
+ + + +
+
+
From 5946899ca801ff1ab9f58ee80059e65a91002e47 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Fri, 1 May 2026 17:59:56 +0530 Subject: [PATCH 21/28] [IMP] awesome_owl: add autofocus, toggle, and delete to todos Enhance the todo list interactivity by allowing users to manage task lifecycles. This implementation focuses on component communication and DOM management using Owl hooks. Key changes: - Create 'useAutofocus' hook in utils.js using useRef for the input. - Implement 'toggleState' callback prop to mark tasks as completed. - Implement 'removeTodo' callback prop to delete tasks from the list. --- awesome_owl/static/src/todo/todo_item.js | 16 +++++++++++--- awesome_owl/static/src/todo/todo_item.xml | 6 ++++-- awesome_owl/static/src/todo/todo_list.js | 26 +++++++++++++++++++---- awesome_owl/static/src/todo/todo_list.xml | 6 +++--- awesome_owl/static/src/utils.js | 11 ++++++++++ 5 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js index b1204467035..328b901cb40 100644 --- a/awesome_owl/static/src/todo/todo_item.js +++ b/awesome_owl/static/src/todo/todo_item.js @@ -1,4 +1,4 @@ -import {Component} from "@odoo/owl" +import { Component } from "@odoo/owl" export class TodoItem extends Component { static template = "awesome_owl.TodoItem" @@ -7,7 +7,17 @@ export class TodoItem extends Component { todo:{ id:Number, description:String, - isCompleted:Boolean - } + isCompleted:Boolean, + }, + toggleState: Function, + removeTodo: Function, + } + + onToggle(){ + this.props.toggleState(this.props.todo.id) + } + + onRemove(){ + this.props.removeTodo(this.props.todo.id) } } diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml index b8a420c1aa7..83f9c436449 100644 --- a/awesome_owl/static/src/todo/todo_item.xml +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -1,8 +1,10 @@ - +
- . + + . +
diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js index 5eeb8e57646..1d479851779 100644 --- a/awesome_owl/static/src/todo/todo_list.js +++ b/awesome_owl/static/src/todo/todo_list.js @@ -1,17 +1,21 @@ -import {Component, useState} from "@odoo/owl" +import { Component, useState } from "@odoo/owl" import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../utils"; export class TodoList extends Component { static template = "awesome_owl.TodoList"; static components = { TodoItem }; setup() { - this.nextId = 1; + this.nextId = 1; this.todos = useState([]); + this.inputRef = useAutofocus("TodoInput") + this.toggleTodo = this.toggleTodo.bind(this); + this.deleteTodo = this.deleteTodo.bind(this); } - addTodo(ev) { - if (ev.keyCode === 13 && ev.target.value.trim() !== "") { + addTodo(ev) { + if (ev.key === 'Enter' && ev.target.value.trim() !== "") { this.todos.push({ id: this.nextId++, description: ev.target.value, @@ -20,4 +24,18 @@ export class TodoList extends Component { ev.target.value = ""; } } + + toggleTodo(todoId){ + const todo = this.todos.find(t => t.id === todoId); + if(todo){ + todo.isCompleted = !todo.isCompleted + } + } + + deleteTodo(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/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml index 3ebc8782aac..7b286a67b1e 100644 --- a/awesome_owl/static/src/todo/todo_list.xml +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -1,9 +1,9 @@ - + - +
- +
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..a1fb57dc9a2 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,11 @@ +import { useRef, onMounted } from "@odoo/owl" + +export function useAutofocus(name){ + const ref = useRef(name); + onMounted(() => { + if (ref.el) { + ref.el.focus(); + } + }); + return ref; +} From 33b38e2c7003c1bf1beddca62ea75397606580cc Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Sat, 2 May 2026 19:07:42 +0530 Subject: [PATCH 22/28] [IMP] awesome_owl: upgrade Card to use slots and toggle state Refactor the Card component to be more generic and interactive. By using the slot system, the component can now host any arbitrary content (like other components). Key changes: - Replace 'content' prop with a default . - Add an 'isOpen' state to manage content visibility. - Add a toggle button in the card header to show/hide body. - Implement 't-if' for conditional rendering of the body. --- awesome_owl/static/src/card/card.js | 4 +++- awesome_owl/static/src/card/card.xml | 10 ++++++++-- awesome_owl/static/src/playground.js | 11 ++++++++++- awesome_owl/static/src/playground.xml | 13 +++++++++---- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index 5ee0a9f3d2d..cfe4c447821 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -5,6 +5,8 @@ export class Card extends Component { static props = { title: String, - content: String, + slots: {optional:true}, + isOpen: Boolean, + openCard: Function }; } diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index 69a84de1a6c..6964178548c 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -1,9 +1,15 @@
-
+

-

+ +
+
+

diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 0bb5c15773e..d2364dfea73 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -10,10 +10,19 @@ export class Playground extends Component { setup(){ this.htmlContent = markup("some content.") this.normalString = "

- +

+ + + +
-

Todo List

- +
+

Todo List

+ +
From 85846012cec9fe5213941f86f687e591853919b5 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Sun, 3 May 2026 20:39:16 +0530 Subject: [PATCH 23/28] [FIX] estate: address review comments and align with coding guidelines Refactor the estate module to follow Odoo technical standards and improve performance based on peer review. Key changes: - Rename compute methods to follow '_compute_' pattern. - Implement 'read_group' in 'estate.property.type' for better performance. - Optimize 'estate.property.visit' constraints by moving logic outside loops. - Fix typos in field labels (spm -> sqm) and enable 'search' on 'best_price'. - Ensure all UserError messages are properly wrapped in _() for translation. - Clean up XML by removing unnecessary 'limit' attributes and fixing formatting. --- estate/models/estate_property.py | 69 ++++++++++++++------------ estate/models/estate_property_offer.py | 23 +++++---- estate/models/estate_property_visit.py | 23 +++------ estate/views/estate_property_views.xml | 8 +-- 4 files changed, 62 insertions(+), 61 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index e9c50bc7dfc..9d247d07738 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -17,12 +17,12 @@ class EstateProperty(models.Model): expected_price = fields.Float(string="Expected Price", required=True) selling_price = fields.Float(string="Selling Price", copy=False) bedrooms = fields.Integer(default=2) - living_area = fields.Float(string="Living Area (spm)") + living_area = fields.Float(string="Living Area (sq. m)") facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Float(string="Garden Area (spm)") - total_area = fields.Float(string="Total Area (sqm)", compute="_computed_total_area") + garden_area = fields.Float(string="Garden Area (sq. m)") + total_area = fields.Float(string="Total Area (sq. m)", compute="_computed_total_area", store=True) garden_orientation = fields.Selection([ ('north', "North"), ('east', "East"), @@ -42,32 +42,22 @@ class EstateProperty(models.Model): buyer_id = fields.Many2one('res.partner', string='Buyer', ondelete='cascade') property_tag_ids = fields.Many2many('estate.property.tag') offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") - best_price = fields.Float(string="Best Offer", compute="_computed_best_offer") + best_price = fields.Float(string="Best Offer", compute="_computed_best_price", store=True) visit_ids = fields.One2many('estate.property.visit', 'property_id', string="Visits") visit_count = fields.Integer(compute="_compute_visit_count") - @api.depends('visit_ids') - def _compute_visit_count(self): - for rec in self: - rec.visit_count = len(rec.visit_ids) - _check_expected_price = models.Constraint( 'CHECK(expected_price >= 1)', 'The expected price must be strictly positive.' ) - _check_selling_price = models.Constraint( - 'CHECK(selling_price > 0)', - 'The selling price must be positive.' - ) - @api.depends("living_area", "garden_area") def _computed_total_area(self): for rec in self: rec.total_area = rec.living_area + rec.garden_area @api.depends("offer_ids.price") - def _computed_best_offer(self): + def _computed_best_price(self): for rec in self: prices = rec.offer_ids.mapped('price') if prices: @@ -75,22 +65,21 @@ def _computed_best_offer(self): else: rec.best_price = 0.0 - @api.onchange("garden") - def _onchange_garden(self): - if self.garden: - self.garden_area = 10 - self.garden_orientation = 'north' + @api.depends('visit_ids') + def _compute_visit_count(self): + visits = self.env['estate.property.visit']._read_group( + domain=[ + ('property_id', 'in', self.ids), + ('state', '=', 'scheduled') + ], groupby=['property_id'], aggregates=['__count'] + ) - return { - 'warning': { - 'title': "Garden Enabled", - 'message': "Default area set to 10 and orientation north", - 'type': "notification" - } - } - else: - self.garden_area = 0 - self.garden_orientation = False + count_dict = { + prop.id: count for prop, count in visits + } + + for rec in self: + rec.visit_count = count_dict.get(rec.id, 0) @api.constrains('selling_price', 'expected_price') def _check_selling_price(self): @@ -101,6 +90,15 @@ def _check_selling_price(self): if float_compare(rec.selling_price, limit_price, precision_digits=2) < 0: raise ValidationError(_("selling price cannot be lower than 90% of the expected price")) + @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 + @api.ondelete(at_uninstall=False) def _check_state(self): for rec in self: @@ -119,5 +117,14 @@ def action_property_sold(self): for rec in self: if rec.state == 'cancelled': raise UserError(_("%s property of %s can not be sold.", rec.state, rec.name)) - rec.state = 'sold' + elif not rec.selling_price: + raise UserError(_("Property can not be sold without selling price")) + else: + rec.state = 'sold' + return True + + def action_best_offer(self): + self.ensure_one() + best_offer = self.offer_ids.filtered(lambda o: o.price == self.best_price) + best_offer.action_accept_offer() return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 168dbe4c6aa..225ee53a814 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,6 @@ from dateutil.relativedelta import relativedelta -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -17,7 +17,7 @@ class EstatePropertyOffer(models.Model): partner_id = fields.Many2one('res.partner', required=True) property_id = fields.Many2one('estate.property', required=True) validity = fields.Integer(default=7) - date_deadline = fields.Date(string="Deadline", compute="_compute_deadline_method", inverse="_inverse_deadline_method") + date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline") property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) _check_offer_price = models.Constraint( @@ -26,12 +26,12 @@ class EstatePropertyOffer(models.Model): ) @api.depends("create_date", "validity") - def _compute_deadline_method(self): + def _compute_date_deadline(self): for rec in self: create_date = rec.create_date.date() if rec.create_date else fields.Date.today() rec.date_deadline = create_date + relativedelta(days=rec.validity) - def _inverse_deadline_method(self): + def _inverse_date_deadline(self): for rec in self: create_date = rec.create_date.date() if rec.create_date else fields.Date.today() rec.validity = relativedelta(rec.date_deadline, create_date).days @@ -41,19 +41,22 @@ def create(self, vals_list): for rec in vals_list: property_rec = self.env['estate.property'].browse(rec.get('property_id')) if rec.get('price') < property_rec.best_price: - raise UserError("The offer must be higher than the existing offer.") + raise UserError(_("The offer must be higher than the existing offer.")) property_rec.state = "offer_received" return super().create(vals_list) def action_accept_offer(self): for rec in self: if any(i.status == "accepted" for i in rec.property_id.offer_ids): - raise UserError("Offer already accepted for given property.") - + raise UserError(_("Offer already accepted for given property.")) rec.status = "accepted" - rec.property_id.buyer_id = rec.partner_id.id - rec.property_id.selling_price = rec.price - rec.property_id.state = "offer_accepted" + rec.property_id.write({ + 'buyer_id': rec.partner_id.id, + 'selling_price': rec.price, + 'state': 'offer_accepted' + }) + other_offers = self.property_id.offer_ids.filtered(lambda o: o.status != 'accepted') + other_offers.write({'status': 'refused'}) return True def action_refuse_offer(self): diff --git a/estate/models/estate_property_visit.py b/estate/models/estate_property_visit.py index 12eae195d14..7a4e25dd6e2 100644 --- a/estate/models/estate_property_visit.py +++ b/estate/models/estate_property_visit.py @@ -1,7 +1,6 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models -from odoo.exceptions import ValidationError class EstatePropertyVisit(models.Model): @@ -20,11 +19,9 @@ class EstatePropertyVisit(models.Model): ('cancel', 'Cancelled'), ], string="Status", default='scheduled', copy=False) - def action_set_done(self): - self.state = 'done' - - def action_set_cancel(self): - self.state = 'cancel' + _unique_property_visit = models.Constraint( + 'UNIQUE("schedule_date", "property_id")', 'This date is already scheduled by an property.' + ) @api.model_create_multi def create(self, vals_list): @@ -52,14 +49,8 @@ def write(self, vals): }) return res - @api.constrains('schedule_date', 'property_id') - def _check_visit_date(self): - for rec in self: - exists = self.search([ - ('property_id', '=', rec.property_id.id), - ('id', '!=', rec.id), - ('schedule_date', '=', rec.schedule_date) - ]) + def action_set_done(self): + self.state = 'done' - if exists: - raise ValidationError(f"This date({rec.schedule_date}) is already scheduled by an customer.") + def action_set_cancel(self): + self.state = 'cancel' diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d37fed690c6..23d9719dd86 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -15,10 +15,11 @@
+
-
+
+ +
+
+ +
+
Number of new orders this month
+
+
+
+ +
+
Average time for an order to go from 'new' to 'sent' or 'cancelled'
+
+
+
+ +
+
Average amount of t-shirt by order this month
+
+
+
+ +
+
Number of cancelled orders this month
+
+
+
+ +
+
Total amount of new orders this month
+
+
+
+
+ - diff --git a/awesome_dashboard/static/src/item/dashboardItem.js b/awesome_dashboard/static/src/item/dashboardItem.js new file mode 100644 index 00000000000..c110c142b37 --- /dev/null +++ b/awesome_dashboard/static/src/item/dashboardItem.js @@ -0,0 +1,14 @@ +import { Component } from "@odoo/owl" + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static props = { + size: Number, + slots: { optional: true } + } + + static defaultSize = { + size: 1 + } +} diff --git a/awesome_dashboard/static/src/item/dashboardItem.xml b/awesome_dashboard/static/src/item/dashboardItem.xml new file mode 100644 index 00000000000..431eff9eb5e --- /dev/null +++ b/awesome_dashboard/static/src/item/dashboardItem.xml @@ -0,0 +1,7 @@ + + +
+ +
+
+
From d999cbdc0819bc67721752da833660f3c2fad2c0 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Tue, 5 May 2026 18:45:09 +0530 Subject: [PATCH 25/28] [IMP] awesome_dashboard: cache statistics and add t-shirt size pie chart Created a new 'awesome_dashboard.statistics' service to manage data. Used the 'memoize' utility to cache RPC calls to /statistics, preventing unnecessary network requests on every component remount. Implemented a 'PieChart' component that lazy-loads Chart.js to keep the initial bundle size small. Added a pie chart to the dashboard to show the distribution of t-shirt sizes (m, s, xl) using real statistics. The goal is to provide a smoother user experience and a better visual representation of the product data. --- awesome_dashboard/static/src/chart/piChart.js | 51 +++++++++++++++++++ .../static/src/chart/piChart.xml | 10 ++++ awesome_dashboard/static/src/dashboard.js | 10 ++-- awesome_dashboard/static/src/dashboard.xml | 3 ++ 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 awesome_dashboard/static/src/chart/piChart.js create mode 100644 awesome_dashboard/static/src/chart/piChart.xml diff --git a/awesome_dashboard/static/src/chart/piChart.js b/awesome_dashboard/static/src/chart/piChart.js new file mode 100644 index 00000000000..19a5f6ef520 --- /dev/null +++ b/awesome_dashboard/static/src/chart/piChart.js @@ -0,0 +1,51 @@ +import { Component, onWillStart, useRef, onMounted } from "@odoo/owl" +import { loadJS } from "@web/core/assets" + +export class PiChart extends Component { + static template = "awesome_dashboard.PiChart"; + + static props = { + data: Object, + label: {String, optional: true} + } + + setup(){ + this.canvasRef = useRef("canvas") + + onWillStart(async ()=>{ + await loadJS("/web/static/lib/Chart/Chart.js") + }) + + onMounted(()=>{ + this.renderChart(); + }) + } + + renderChart(){ + const labels = Object.keys(this.props.data) + const data = Object.values(this.props.data) + + new Chart(this.canvasRef.el, { + type: "pie", + data: { + labels: labels, + datasets: [{ + label: this.props.label, + data: data, + backgroundColor: [ + '#ff6384', '#36a2eb', '#ffce56' + ], + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + } + } + } + }) + } +} diff --git a/awesome_dashboard/static/src/chart/piChart.xml b/awesome_dashboard/static/src/chart/piChart.xml new file mode 100644 index 00000000000..1820b0fcd2f --- /dev/null +++ b/awesome_dashboard/static/src/chart/piChart.xml @@ -0,0 +1,10 @@ + + +
+

T-shirt Size

+
+ +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 9621cee065c..8f9993557d0 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -3,11 +3,11 @@ import { registry } from "@web/core/registry"; import { Layout } from "@web/search/layout"; import { useService } from "@web/core/utils/hooks"; import { DashboardItem } from "./item/dashboardItem"; -import { rpc } from "@web/core/network/rpc"; +import { PiChart } from "./chart/piChart"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout, DashboardItem } + static components = { Layout, DashboardItem, PiChart } setup(){ this.display = { @@ -15,10 +15,10 @@ class AwesomeDashboard extends Component { } this.action = useService("action") - this.statistics = {}; + this.statiscticsService = useService("awesome_dashboard.statistics_service") onWillStart(async () => { - this.statistics = await rpc("/awesome_dashboard/statistics"); + this.statistics = await this.statiscticsService.loadStatistics() }) } @@ -26,7 +26,7 @@ class AwesomeDashboard extends Component { this.action.doAction("base.action_partner_form") } - async openLeads(){ + openLeads(){ this.action.doAction({ type: 'ir.actions.act_window', name: 'crm leads', diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index e5eea53d0d9..37d44d4db33 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -37,6 +37,9 @@
+ + + From 415e690fec96101cd6394ffe4923944364a0a2c0 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Wed, 6 May 2026 18:56:19 +0530 Subject: [PATCH 26/28] [IMP] awesome_dashboard: add auto-reload, lazy loading, and generic item system - Real-time Updates: Modified the statistics service to use a 'reactive' object and implemented a 10-minute auto-reload (setInterval) to keep data fresh. - Performance: Implemented 'LazyComponent' and moved dashboard assets to a dedicated bundle to ensure code is only loaded when the actionis accessed. - Generic Architecture: Refactored the dashboard to be data-driven. Created 'NumberCard' and 'PieChartCard' components that are now registered as generic items and rendered using 't-component'. - Clean Code: Extracted dashboard item definitions into a separate 'dashboard_items.js' file for easier customization. --- awesome_dashboard/__manifest__.py | 7 ++- .../static/src/dashboard/chart/piChart.js | 51 +++++++++++++++++++ .../static/src/dashboard/chart/piChart.xml | 10 ++++ .../src/dashboard/components/number_card.js | 9 ++++ .../src/dashboard/components/number_card.xml | 12 +++++ .../dashboard/components/pie_chart_card.js | 12 +++++ .../dashboard/components/pie_chart_card.xml | 8 +++ .../static/src/dashboard/dashboard.js | 39 ++++++++++++++ .../static/src/dashboard/dashboard.scss | 5 ++ .../static/src/dashboard/dashboard.xml | 21 ++++++++ .../static/src/dashboard/dashboard_items.js | 41 +++++++++++++++ .../src/dashboard/item/dashboardItem.js | 14 +++++ .../src/dashboard/item/dashboardItem.xml | 7 +++ .../src/dashboard/statistics_service.js | 18 +++++++ .../static/src/dashboard_action.js | 14 +++++ 15 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard/chart/piChart.js create mode 100644 awesome_dashboard/static/src/dashboard/chart/piChart.xml create mode 100644 awesome_dashboard/static/src/dashboard/components/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/components/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/components/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/components/pie_chart_card.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_items.js create mode 100644 awesome_dashboard/static/src/dashboard/item/dashboardItem.js create mode 100644 awesome_dashboard/static/src/dashboard/item/dashboardItem.xml create mode 100644 awesome_dashboard/static/src/dashboard/statistics_service.js create mode 100644 awesome_dashboard/static/src/dashboard_action.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..3d426f54022 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -22,8 +22,11 @@ 'views/views.xml', ], 'assets': { - 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + 'web.assets_backend': [ + 'awesome_dashboard/static/src/dashboard_action.js', + ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', ], }, 'license': 'AGPL-3' diff --git a/awesome_dashboard/static/src/dashboard/chart/piChart.js b/awesome_dashboard/static/src/dashboard/chart/piChart.js new file mode 100644 index 00000000000..19a5f6ef520 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/chart/piChart.js @@ -0,0 +1,51 @@ +import { Component, onWillStart, useRef, onMounted } from "@odoo/owl" +import { loadJS } from "@web/core/assets" + +export class PiChart extends Component { + static template = "awesome_dashboard.PiChart"; + + static props = { + data: Object, + label: {String, optional: true} + } + + setup(){ + this.canvasRef = useRef("canvas") + + onWillStart(async ()=>{ + await loadJS("/web/static/lib/Chart/Chart.js") + }) + + onMounted(()=>{ + this.renderChart(); + }) + } + + renderChart(){ + const labels = Object.keys(this.props.data) + const data = Object.values(this.props.data) + + new Chart(this.canvasRef.el, { + type: "pie", + data: { + labels: labels, + datasets: [{ + label: this.props.label, + data: data, + backgroundColor: [ + '#ff6384', '#36a2eb', '#ffce56' + ], + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + } + } + } + }) + } +} diff --git a/awesome_dashboard/static/src/dashboard/chart/piChart.xml b/awesome_dashboard/static/src/dashboard/chart/piChart.xml new file mode 100644 index 00000000000..80423044b1b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/chart/piChart.xml @@ -0,0 +1,10 @@ + + +
+

T-shirt Size

+
+ +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/components/number_card.js b/awesome_dashboard/static/src/dashboard/components/number_card.js new file mode 100644 index 00000000000..578e0499bc7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/number_card.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl" + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard" + static props = { + title: String, + value: Number + } +} diff --git a/awesome_dashboard/static/src/dashboard/components/number_card.xml b/awesome_dashboard/static/src/dashboard/components/number_card.xml new file mode 100644 index 00000000000..c4d0fa723af --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/number_card.xml @@ -0,0 +1,12 @@ + + +
+

+ +

+
+

+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/components/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/components/pie_chart_card.js new file mode 100644 index 00000000000..dd048d50a83 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/pie_chart_card.js @@ -0,0 +1,12 @@ +import { Component } from "@odoo/owl" +import { PiChart } from "../chart/piChart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PiChart } + + static props = { + title: String, + data: Object + } +} diff --git a/awesome_dashboard/static/src/dashboard/components/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/components/pie_chart_card.xml new file mode 100644 index 00000000000..20b97da0218 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/components/pie_chart_card.xml @@ -0,0 +1,8 @@ + + +
+

+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..7a7de3f11c3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,39 @@ +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 "./item/dashboardItem"; +import { PiChart } from "./chart/piChart"; +import { NumberCard } from "./components/number_card"; +import { PieChartCard } from "./components/pie_chart_card"; +import { items } from "./dashboard_items"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, PiChart, NumberCard, PieChartCard } + + setup(){ + this.items = items + this.display = { + controlPanel: {} + } + this.action = useService("action") + this.statistics = useState(useService("awesome_dashboard.statistics_service")) + } + + openCustomers(){ + this.action.doAction("base.action_partner_form") + } + + openLeads(){ + this.action.doAction({ + type: 'ir.actions.act_window', + name: 'crm leads', + res_model: 'crm.lead', + views: [[false,'list'], [false,'form']], + target: 'current', + }) + } +} + +registry.category("lazy_components").add("awesome_dashboard.dashboard", 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..646cc6f5af3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,5 @@ +.o_dashboard { + background-color: #f8f9fa; + display: flex; + flex-direction: column; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..1064f6ed47b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,21 @@ + + + + +
+
+ + +
+
+ + + + + + +
+
+
+
+
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..cc453d80f64 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,41 @@ +import { NumberCard } from "./components/number_card" +import { PieChartCard } from "./components/pie_chart_card" + +export const items = [ + { + id: "nb_new_orders", + Component: NumberCard, + size: 1, + props: (data) => ({ title: "Number of new orders this month", value: data.nb_new_orders }), + }, + { + id: "average_time", + Component: NumberCard, + size: 2, + props: (data) => ({ title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", value: data.average_time }), + }, + { + id: "average_quantity", + Component: NumberCard, + size: 1, + props: (data) => ({ title: "Average amount of t-shirt by order this month", value: data.average_quantity }), + }, + { + id: "nb_cancelled_orders", + Component: NumberCard, + size: 1, + props: (data) => ({ title: "Number of cancelled orders this month", value: data.nb_cancelled_orders }), + }, + { + id: "total_amount", + Component: NumberCard, + size: 1, + props: (data) => ({ title: "Total amount of new orders this month", value: data.total_amount }), + }, + { + id: "orders_by_size", + Component: PieChartCard, + size: 2, + props: (data) => ({ title: "Orders by size", data: data.orders_by_size }), + }, +] diff --git a/awesome_dashboard/static/src/dashboard/item/dashboardItem.js b/awesome_dashboard/static/src/dashboard/item/dashboardItem.js new file mode 100644 index 00000000000..c110c142b37 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/item/dashboardItem.js @@ -0,0 +1,14 @@ +import { Component } from "@odoo/owl" + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static props = { + size: Number, + slots: { optional: true } + } + + static defaultSize = { + size: 1 + } +} diff --git a/awesome_dashboard/static/src/dashboard/item/dashboardItem.xml b/awesome_dashboard/static/src/dashboard/item/dashboardItem.xml new file mode 100644 index 00000000000..431eff9eb5e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/item/dashboardItem.xml @@ -0,0 +1,7 @@ + + +
+ +
+
+
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..69334d97325 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,18 @@ +import { reactive } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; + +const StatisticsService = { + start(env){ + const stats = reactive({data: {}}) + async function loadStatistics(){ + const result = await rpc("/awesome_dashboard/statistics") + Object.assign(stats.data, result) + } + loadStatistics() + setInterval(loadStatistics, 600000) + return stats; + } +} + +registry.category("services").add("awesome_dashboard.statistics_service", 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..96765181454 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +import { Component, xml } from "@odoo/owl" +import { registry } from "@web/core/registry" +import { LazyComponent } from "@web/core/assets" + +export class AwesomeDashboardLoader extends Component { + static components = { LazyComponent } + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader) From e2673c3606c279892546c5d83326091f6b629e2d Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Sat, 9 May 2026 09:54:54 +0530 Subject: [PATCH 27/28] [IMP] awesome_dashboard: make dashboard extensible and customizable This commit introduces a registry system to make the dashboard extensible and adds a settings dialog for user customization. - Registry: Replaced the hardcoded item list with a dedicated registry, allowing other addons to plug in new items easily. - Customization: Added a gear icon button in the control panel to open a settings dialog. Users can toggle items via checkboxes. - Persistence: User preferences (hidden items) are saved in localStorage for a persistent experience. - Translation: Wrapped strings with env._t to support multiple languages. --- awesome_dashboard/static/src/dashboard.js | 40 -------- awesome_dashboard/static/src/dashboard.xml | 46 --------- .../static/src/dashboard/config_dialog.js | 34 +++++++ .../static/src/dashboard/config_dialog.xml | 17 ++++ .../static/src/dashboard/dashboard_items.js | 93 +++++++++++-------- 5 files changed, 104 insertions(+), 126 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/config_dialog.js create mode 100644 awesome_dashboard/static/src/dashboard/config_dialog.xml diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index 8f9993557d0..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,40 +0,0 @@ -import { Component, onWillStart } 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 "./item/dashboardItem"; -import { PiChart } from "./chart/piChart"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout, DashboardItem, PiChart } - - setup(){ - this.display = { - controlPanel: {} - } - - this.action = useService("action") - this.statiscticsService = useService("awesome_dashboard.statistics_service") - - onWillStart(async () => { - this.statistics = await this.statiscticsService.loadStatistics() - }) - } - - openCustomers(){ - this.action.doAction("base.action_partner_form") - } - - openLeads(){ - this.action.doAction({ - type: 'ir.actions.act_window', - name: 'crm leads', - res_model: 'crm.lead', - views: [[false,'list'], [false,'form']], - target: 'current', - }) - } -} - -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 37d44d4db33..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - -
- - -
-
- -
-
Number of new orders this month
-
-
-
- -
-
Average time for an order to go from 'new' to 'sent' or 'cancelled'
-
-
-
- -
-
Average amount of t-shirt by order this month
-
-
-
- -
-
Number of cancelled orders this month
-
-
-
- -
-
Total amount of new orders this month
-
-
-
- - - -
-
-
-
diff --git a/awesome_dashboard/static/src/dashboard/config_dialog.js b/awesome_dashboard/static/src/dashboard/config_dialog.js new file mode 100644 index 00000000000..1331d32e117 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/config_dialog.js @@ -0,0 +1,34 @@ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; + + +export 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, + enabled: !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", + newDisabledItems, + ); + this.props.onUpdateConfiguration(newDisabledItems); + } +} diff --git a/awesome_dashboard/static/src/dashboard/config_dialog.xml b/awesome_dashboard/static/src/dashboard/config_dialog.xml new file mode 100644 index 00000000000..c8273c22c6a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/config_dialog.xml @@ -0,0 +1,17 @@ + + + + Which cards do you whish to see ? + + + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js index cc453d80f64..d09b09e33bf 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_items.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -1,41 +1,54 @@ -import { NumberCard } from "./components/number_card" -import { PieChartCard } from "./components/pie_chart_card" +import { NumberCard } from "./components/number_card"; +import { PieChartCard } from "./components/pie_chart_card"; +import { registry } from "@web/core/registry"; +import { _t } from "@web/core/l10n/translation"; -export const items = [ - { - id: "nb_new_orders", - Component: NumberCard, - size: 1, - props: (data) => ({ title: "Number of new orders this month", value: data.nb_new_orders }), - }, - { - id: "average_time", - Component: NumberCard, - size: 2, - props: (data) => ({ title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", value: data.average_time }), - }, - { - id: "average_quantity", - Component: NumberCard, - size: 1, - props: (data) => ({ title: "Average amount of t-shirt by order this month", value: data.average_quantity }), - }, - { - id: "nb_cancelled_orders", - Component: NumberCard, - size: 1, - props: (data) => ({ title: "Number of cancelled orders this month", value: data.nb_cancelled_orders }), - }, - { - id: "total_amount", - Component: NumberCard, - size: 1, - props: (data) => ({ title: "Total amount of new orders this month", value: data.total_amount }), - }, - { - id: "orders_by_size", - Component: PieChartCard, - size: 2, - props: (data) => ({ title: "Orders by size", data: data.orders_by_size }), - }, -] +const dashboardRegistry = registry.category("awesome_dashboard"); + +dashboardRegistry.add("nb_new_orders", { + id: "nb_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 }), +}); + +dashboardRegistry.add("average_time", { + id: "average_time", + description: _t("Average time for an order"), + Component: NumberCard, + size: 2, + props: (data) => ({ title: _t("Average time for an order to go from 'new' to 'sent' or 'cancelled'"), value: data.average_time }), +}); + +dashboardRegistry.add("average_quantity", { + id: "average_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 }), +}); + +dashboardRegistry.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 }), +}); + +dashboardRegistry.add("total_amount", { + id: "total_amount", + description: _t("Amount orders this month"), + Component: NumberCard, + size: 1, + props: (data) => ({ title: _t("Total amount of new orders this month"), value: data.total_amount }), +}); + +dashboardRegistry.add("orders_by_size", { + id: "orders_by_size", + description: _t("Shirt orders by size"), + Component: PieChartCard, + size: 2, + props: (data) => ({ title: _t("Shirt orders by size"), data: data.orders_by_size }), +}); From b21be695f42bfd3f845f6829011b546cdc3c0b97 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Thu, 14 May 2026 14:56:04 +0530 Subject: [PATCH 28/28] [ADD] add_pricelist_price: calculate and display original pricelist book price Introduce a new custom module that adds a field on both sale order lines and invoice lines. This allows clients to compare strict policy-based pricelist amounts against any manual overrides. WHY this change is being done: Currently, when a salesperson manually alters the on a line, the system loses the immediate visual reference of what the original policy-driven pricelist value should have been. This module bridges that gap for audit and financial comparison purposes without modifying core files directly. Technical decisions taken: 1. Created a standalone custom module inheriting from and to maintain clean separation of custom business logic. 2. Used instead of to enforce accounting right alignment and precise currency-specific decimal rounding. 3. Leveraged Odoo's native method passing both the product and quantity to properly evaluate volume-based pricelist rules. 4. Implemented a field with on to eliminate redundant calculations on invoices and make it reporting-ready. 5. Used with context to keep the accounting interface clean and restricted strictly to Customer Invoices. --- add_pricelist_price/__init__.py | 1 + add_pricelist_price/__manifest__.py | 13 +++++++++ add_pricelist_price/models/__init__.py | 2 ++ .../models/account_move_line.py | 13 +++++++++ add_pricelist_price/models/sale_order_line.py | 29 +++++++++++++++++++ .../views/account_move_line_views.xml | 13 +++++++++ .../views/sale_order_line_views.xml | 13 +++++++++ 7 files changed, 84 insertions(+) create mode 100644 add_pricelist_price/__init__.py create mode 100644 add_pricelist_price/__manifest__.py create mode 100644 add_pricelist_price/models/__init__.py create mode 100644 add_pricelist_price/models/account_move_line.py create mode 100644 add_pricelist_price/models/sale_order_line.py create mode 100644 add_pricelist_price/views/account_move_line_views.xml create mode 100644 add_pricelist_price/views/sale_order_line_views.xml diff --git a/add_pricelist_price/__init__.py b/add_pricelist_price/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/add_pricelist_price/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/add_pricelist_price/__manifest__.py b/add_pricelist_price/__manifest__.py new file mode 100644 index 00000000000..43ef6d470ae --- /dev/null +++ b/add_pricelist_price/__manifest__.py @@ -0,0 +1,13 @@ +{ + 'name': 'Add pricelist price', + 'description': 'Book Price for Sales and Accounting', + 'version': '1.0', + 'author': 'habar', + 'depends': ['sale', 'account'], + 'data': [ + 'views/sale_order_line_views.xml', + 'views/account_move_line_views.xml', + ], + 'installable': True, + 'license': 'LGPL-3', +} diff --git a/add_pricelist_price/models/__init__.py b/add_pricelist_price/models/__init__.py new file mode 100644 index 00000000000..ea3d9579546 --- /dev/null +++ b/add_pricelist_price/models/__init__.py @@ -0,0 +1,2 @@ +from . import sale_order_line +from . import account_move_line diff --git a/add_pricelist_price/models/account_move_line.py b/add_pricelist_price/models/account_move_line.py new file mode 100644 index 00000000000..1acd2dc2cc3 --- /dev/null +++ b/add_pricelist_price/models/account_move_line.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + book_price = fields.Monetary( + string="Book Price", + related="sale_line_ids.book_price", + store=True, + readonly=True, + help="Original book price fetched directly from the linked Sales Order line." + ) diff --git a/add_pricelist_price/models/sale_order_line.py b/add_pricelist_price/models/sale_order_line.py new file mode 100644 index 00000000000..c569c88dc96 --- /dev/null +++ b/add_pricelist_price/models/sale_order_line.py @@ -0,0 +1,29 @@ +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + book_price = fields.Monetary( + string="Book Price", + compute="_compute_book_price", + store=True, + readonly=True, + help="The original price fetched from the pricelist rule before any manual overrides." + ) + + @api.depends("product_id", "product_uom_qty", "order_id.pricelist_id") + def _compute_book_price(self): + for line in self: + if not line.product_id: + line.book_price = 0.0 + continue + + pricelist = line.order_id.pricelist_id + + if pricelist: + unit_price = pricelist._get_product_price(line.product_id, line.product_uom_qty) + else: + unit_price = line.product_id.lst_price or 0.0 + + line.book_price = unit_price diff --git a/add_pricelist_price/views/account_move_line_views.xml b/add_pricelist_price/views/account_move_line_views.xml new file mode 100644 index 00000000000..f6475fa9208 --- /dev/null +++ b/add_pricelist_price/views/account_move_line_views.xml @@ -0,0 +1,13 @@ + + + + account.move.form.inherit.book.price + account.move + + + + + + + + diff --git a/add_pricelist_price/views/sale_order_line_views.xml b/add_pricelist_price/views/sale_order_line_views.xml new file mode 100644 index 00000000000..eab97c4ed7e --- /dev/null +++ b/add_pricelist_price/views/sale_order_line_views.xml @@ -0,0 +1,13 @@ + + + + sale.order.form.inherit.book.price + sale.order + + + + + + + +