From 15bd5faf6a62bac3499658594c68adf9ad9aeb14 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Tue, 10 Mar 2026 19:05:21 +0530 Subject: [PATCH 01/19] [ADD] estate : implement estate module and estate_property table creation - Configured manifest with required metadata and dependencies. - It covers chapter- 2 and 3 task - Added the required fields. --- estate/__init__.py | 1 + estate/__manifest__.py | 15 +++++++++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 26 ++++++++++++++++++++++++++ 4 files changed, 43 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..eb6a1984f43 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': "Estate", + 'version': '0.1', + 'summary': "Real Estate Advertisement", + 'description': """ + This module allows you to manage real estate advertisements, including + properties, agents, and customer inquiries. + """, + 'author': "aykhu", + 'license': 'LGPL-3', + 'website': "https://www.odoo.com/app/estate", + 'category': 'Tutorials', + 'application': True, + 'depends': ['base'], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..c2437ea46ab --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,26 @@ +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Estate Property' + + name = fields.Char(string='Title', required=True) + description = fields.Text(string='Description') + postcode = fields.Char(string='Postal Code') + date_availability = fields.Date(string='Available From') + expected_price = fields.Float(string='Expected Price', required=True) + selling_price = fields.Float(string='Price') + living_area = fields.Float(string='Area (sq m)') + bedrooms = fields.Integer(string='Bedrooms') + facades = fields.Integer(string='Facades') + has_garage = fields.Boolean(string="Has Garage ?") + has_garden = fields.Boolean(string="Has Garden ?") + garden_area = fields.Integer(string="Garden Area (sq m)") + garden_orientation = fields.Selection( + string="Garden Orientation", + selection=[('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West')], + ) From 52fa0f3049fc77d26cc1114c678b3f6afff1462a Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Wed, 11 Mar 2026 18:49:58 +0530 Subject: [PATCH 02/19] [IMP] estate : added access rights - Completed Ch 4 task for Access Rights --- estate/__manifest__.py | 3 +++ estate/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index eb6a1984f43..996bbcb72a9 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,4 +12,7 @@ 'category': 'Tutorials', 'application': True, 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + ], } diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..e57ec5e4eb5 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,0,0,0 \ No newline at end of file From 99d70a3bfc2ce559f201a0aa5a5937e5f033e341 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Thu, 12 Mar 2026 18:46:15 +0530 Subject: [PATCH 03/19] [IMP] estate: added action and menu - Created an action - Define main menu and submenus - Add views to manifest 'data' list - Completed Ch 5 Action and Menu part --- estate/__manifest__.py | 2 ++ estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 7 +++++++ 3 files changed, 17 insertions(+) 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 996bbcb72a9..754f6ef50ec 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -14,5 +14,7 @@ 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], } diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..600d4ae1a85 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..7fba34c13ef --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,7 @@ + + + Test action + estate.property + list,form + + From 53c0bc376a31bfd325abc81cc806b22e16a001a3 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Fri, 13 Mar 2026 19:08:23 +0530 Subject: [PATCH 04/19] [IMP] estate: improved list view and form view - Added is_active and state fields in form - Added fields in list view - Improved and adjusted fields in Form View - Completed Ch 5 and Ch 6 List and Form Views --- estate/models/estate_property.py | 24 +++++++++-- estate/security/ir.model.access.csv | 2 +- estate/views/estate_property_views.xml | 57 ++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index c2437ea46ab..727c2a9aa72 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,22 +1,29 @@ from odoo import fields, models +from dateutil.relativedelta import relativedelta class EstateProperty(models.Model): _name = 'estate.property' _description = 'Estate Property' - name = fields.Char(string='Title', required=True) + name = fields.Char(string='Title', required=True, default='Unknown') description = fields.Text(string='Description') postcode = fields.Char(string='Postal Code') - date_availability = fields.Date(string='Available From') + last_seen = fields.Datetime( + string="Last Seen", default=fields.Datetime.now) + 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='Price') + selling_price = fields.Float( + string='Selling Price', readonly=True, copy=False) living_area = fields.Float(string='Area (sq m)') - bedrooms = fields.Integer(string='Bedrooms') + bedrooms = fields.Integer(string='Bedrooms', default=2) facades = fields.Integer(string='Facades') has_garage = fields.Boolean(string="Has Garage ?") has_garden = fields.Boolean(string="Has Garden ?") garden_area = fields.Integer(string="Garden Area (sq m)") + active = fields.Boolean(string="Is Active ?", default=True) garden_orientation = fields.Selection( string="Garden Orientation", selection=[('north', 'North'), @@ -24,3 +31,12 @@ class EstateProperty(models.Model): ('east', 'East'), ('west', 'West')], ) + state = fields.Selection( + string="Status", + selection=[("New", "New"), + ("Offer Received", "Offer Received"), + ("Offer Accepted", "Offer Accepted"), + ("Sold", "Sold"), + ("Cancelled", "Cancelled")], + required=True, default="New", copy=False + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index e57ec5e4eb5..ab63520e22b 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,0,0,0 \ No newline at end of file +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 7fba34c13ef..fe6484667b3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,4 +4,61 @@ estate.property list,form + + + estate_property.list + estate.property + + + + + + + + + + + + + + + + estate_property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
From c83862c3cccc17b9e182846ef93da1ae5f386a34 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Mon, 16 Mar 2026 18:57:01 +0530 Subject: [PATCH 05/19] [IMP] estate: Implemented model relations and search views - Implemented search view - Added property types, tags, and offers models - Linked properties with buyer, salesperson, types, tags, and offers - Completed Ch 6 and Ch 7. --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 3 ++ estate/models/estate_property.py | 37 ++++++++++++------ estate/models/estate_property_offer.py | 18 +++++++++ estate/models/estate_property_tag.py | 8 ++++ estate/models/estate_property_type.py | 8 ++++ estate/security/ir.model.access.csv | 5 ++- estate/views/estate_menus.xml | 14 ++++--- estate/views/estate_property_offer_views.xml | 30 +++++++++++++++ estate/views/estate_property_tag_views.xml | 8 ++++ estate/views/estate_property_type_views.xml | 8 ++++ estate/views/estate_property_views.xml | 40 +++++++++++++++++--- 12 files changed, 160 insertions(+), 22 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 754f6ef50ec..9c04c972c23 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -15,6 +15,9 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..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 727c2a9aa72..62524473ef8 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,7 @@ -from odoo import fields, models from dateutil.relativedelta import relativedelta +from odoo import fields, models + class EstateProperty(models.Model): _name = 'estate.property' @@ -26,17 +27,31 @@ class EstateProperty(models.Model): active = fields.Boolean(string="Is Active ?", default=True) garden_orientation = fields.Selection( string="Garden Orientation", - selection=[('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West')], + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ], ) state = fields.Selection( string="Status", - selection=[("New", "New"), - ("Offer Received", "Offer Received"), - ("Offer Accepted", "Offer Accepted"), - ("Sold", "Sold"), - ("Cancelled", "Cancelled")], - required=True, default="New", copy=False + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled") + ], + required=True, default="new", copy=False + ) + property_type_id = fields.Many2one( + "estate.property.type", ondelete='Cascade', string="Property Type" + ) + salesperson_id = fields.Many2one( + "res.users", string="Sales Person", default=lambda self: self.env.user ) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + property_tags_ids = fields.Many2many( + "estate.property.tag", string="Property Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..9f1fd69f227 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,18 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + + price = fields.Float(string="Offer Price") + status = fields.Selection( + 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..9e05c2f560f --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTags(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + + name = fields.Char(string="Name", required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..1506088ca76 --- /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="Name", required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index ab63520e22b..0c0b62b7fee 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 -estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 600d4ae1a85..f2de1677bc3 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,8 +1,12 @@ - - - + + + + + + + + - - \ No newline at end of file + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..41d369bdcd4 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,30 @@ + + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + +
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..c3a7fc4fa05 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + Estate Property Tag + 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..be2e51ecdab --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,8 @@ + + + + Estate Property Types + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index fe6484667b3..8a1bf23f1e1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,3 +1,4 @@ + Test action @@ -5,13 +6,16 @@ list,form - - estate_property.list + + + estate.property.list estate.property + + @@ -21,24 +25,47 @@ + + + estate.property.search + estate.property + + + + + + + + + + + + + + - - estate_property.form + + + estate.property.form estate.property

- +

+ + + + @@ -56,6 +83,9 @@ + + +
From ff255a57e45403e09d687cd2327e3cb97f617f4d Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Tue, 24 Mar 2026 18:58:42 +0530 Subject: [PATCH 06/19] [IMP] estate: Added computed fields - Added Best Offer and Total Area fields in property form view - Covers 2 tasks of chapter 8. --- estate/models/estate_property.py | 21 +++++++++++++++++++-- estate/views/estate_property_views.xml | 2 ++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 62524473ef8..df6ea4c4778 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -18,7 +18,7 @@ class EstateProperty(models.Model): expected_price = fields.Float(string='Expected Price', required=True) selling_price = fields.Float( string='Selling Price', readonly=True, copy=False) - living_area = fields.Float(string='Area (sq m)') + living_area = fields.Float(string='Living Area (sq m)') bedrooms = fields.Integer(string='Bedrooms', default=2) facades = fields.Integer(string='Facades') has_garage = fields.Boolean(string="Has Garage ?") @@ -55,3 +55,20 @@ class EstateProperty(models.Model): property_tags_ids = fields.Many2many( "estate.property.tag", string="Property Tags") offer_ids = fields.One2many("estate.property.offer", "property_id") + total_area = fields.Float(compute="_compute_area") + best_price = fields.Float( + string="Best Offer", compute="_compute_best_price" + ) + + @api.depends("living_area", "garden_area") + def _compute_area(self): + for rec in self: + rec.total_area = rec.living_area + rec.garden_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for rec in self: + if rec.offer_ids: + rec.best_price = max(rec.offer_ids.mapped("price")) + else: + rec.best_price = 0.0 diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 8a1bf23f1e1..2d6b5bcff16 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -63,6 +63,7 @@ + @@ -81,6 +82,7 @@ + From d92719340cfaccc0c2fa38ba33e404a7de275004 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Thu, 26 Mar 2026 19:06:25 +0530 Subject: [PATCH 07/19] [IMP] estate: Implemented onchange logic and validity with inverse function - Added Validity and date_deadline fields with inverse function - Implemented onchange logic for garden_area and garden_orientation - Suggested fixes implemented - Covers last 2 tasks of chapter 8. --- estate/models/estate_property.py | 34 ++++++++++++-------- estate/models/estate_property_offer.py | 23 ++++++++++++- estate/views/estate_property_offer_views.xml | 6 ++-- estate/views/estate_property_views.xml | 7 ++-- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index df6ea4c4778..f8c9e27ec1c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -10,14 +10,13 @@ class EstateProperty(models.Model): name = fields.Char(string='Title', required=True, default='Unknown') description = fields.Text(string='Description') postcode = fields.Char(string='Postal Code') - last_seen = fields.Datetime( - string="Last Seen", default=fields.Datetime.now) 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) + string='Selling Price', readonly=True, copy=False + ) living_area = fields.Float(string='Living Area (sq m)') bedrooms = fields.Integer(string='Bedrooms', default=2) facades = fields.Integer(string='Facades') @@ -53,22 +52,31 @@ class EstateProperty(models.Model): ) buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) property_tags_ids = fields.Many2many( - "estate.property.tag", string="Property Tags") + "estate.property.tag", string="Property Tags" + ) offer_ids = fields.One2many("estate.property.offer", "property_id") - total_area = fields.Float(compute="_compute_area") + total_area = fields.Float(compute="_compute_total_area") best_price = fields.Float( string="Best Offer", compute="_compute_best_price" ) @api.depends("living_area", "garden_area") - def _compute_area(self): - for rec in self: - rec.total_area = rec.living_area + rec.garden_area + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area @api.depends("offer_ids.price") def _compute_best_price(self): - for rec in self: - if rec.offer_ids: - rec.best_price = max(rec.offer_ids.mapped("price")) - else: - rec.best_price = 0.0 + if self.offer_ids: + self.best_price = max(self.offer_ids.mapped("price")) + else: + self.best_price = 0.0 + + @api.onchange('has_garden') + def _onchange_garden(self): + if self.has_garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = None + self.garden_orientation = None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 9f1fd69f227..b7b565aa811 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): @@ -16,3 +18,22 @@ class EstatePropertyOffer(models.Model): ) partner_id = fields.Many2one("res.partner", required=True) property_id = fields.Many2one("estate.property", required=True) + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date( + string="Deadline", compute='_compute_date_deadline', inverse="_inverse_deadline", + ) + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for record in self: + start_date = ( + record.create_date.date() if record.create_date else fields.Date.today() + ) + record.date_deadline = start_date + relativedelta(days=record.validity) + + def _inverse_deadline(self): + for record in self: + start_date = ( + record.create_date.date() if record.create_date else fields.Date.today() + ) + record.validity = (record.date_deadline - start_date).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 41d369bdcd4..62a4aa14590 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,6 +1,5 @@ - estate.property.offer.list estate.property.offer @@ -8,12 +7,13 @@ + +
- estate.property.offer.form estate.property.offer @@ -22,7 +22,9 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 2d6b5bcff16..f70a27df12e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,12 +1,11 @@ - Test action + Estate Property estate.property list,form - estate.property.list estate.property @@ -25,7 +24,6 @@ - estate.property.search estate.property @@ -38,13 +36,12 @@ - + - estate.property.form estate.property From 2018ac54f0b1e01bdb7b55a2d8172da4cc894310 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Mon, 30 Mar 2026 18:45:48 +0530 Subject: [PATCH 08/19] [IMP] estate: add state transition actions with validation - Added sold and cancelled action buttons for state transition - Prevent invalid transitions between 'sold' and 'cancelled' states --- estate/models/estate_property.py | 15 +++++++++++++++ estate/views/estate_property_views.xml | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f8c9e27ec1c..778ceff3258 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): @@ -80,3 +81,17 @@ def _onchange_garden(self): else: self.garden_area = None self.garden_orientation = None + + def action_property_sold(self): + for record in self: + if record.state == "cancelled": + raise UserError("Cancelled property cannot be set as sold.") + else: + record.state = "sold" + + def action_property_cancelled(self): + for record in self: + if record.state == "sold": + raise UserError("Sold property cannot be set as cancelled") + else: + record.state = "cancelled" diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index f70a27df12e..9604d0e1bfc 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -47,6 +47,10 @@ estate.property
+
+

@@ -57,6 +61,7 @@ + From e47d710ba82c3fe3c72bce75ad28763e126a94df Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Tue, 31 Mar 2026 18:26:20 +0530 Subject: [PATCH 09/19] [IMP] estate: add offer state actions and enforce single acceptance - Introduce Accept and Refuse buttons for property offers - Assign buyer and selling price to property on acceptance - Restrict multiple offers from being accepted for the same property - Chapter 9 Completed. --- estate/models/estate_property.py | 2 ++ estate/models/estate_property_offer.py | 18 ++++++++++++++++++ estate/views/estate_property_offer_views.xml | 2 ++ estate/views/estate_property_views.xml | 8 ++++++-- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 778ceff3258..2b5b3fb8ba1 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -88,6 +88,7 @@ def action_property_sold(self): raise UserError("Cancelled property cannot be set as sold.") else: record.state = "sold" + return True def action_property_cancelled(self): for record in self: @@ -95,3 +96,4 @@ def action_property_cancelled(self): raise UserError("Sold property cannot be set as cancelled") else: record.state = "cancelled" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index b7b565aa811..cfdb58ed5bf 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): @@ -37,3 +38,20 @@ def _inverse_deadline(self): record.create_date.date() if record.create_date else fields.Date.today() ) record.validity = (record.date_deadline - start_date).days + + def action_accepted(self): + for record in self: + for offer in record.property_id.offer_ids: + if offer.status == "accepted": + raise UserError( + "Only 1 offer can be accepted for each property") + record.status = "accepted" + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + record.property_id.state = "offer_accepted" + return True + + def action_refused(self): + for record in self: + record.status = "refused" + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 62a4aa14590..560d1f8064f 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + + + + + + + + + + + + + + + + + + + + + estate.property.type.list + estate.property.type + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 25c4887284d..2c3aa820792 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,13 +4,14 @@ Estate Property estate.property list,form + {'search_default_available_properties':1} estate.property.list estate.property - + @@ -19,7 +20,7 @@ - + @@ -33,11 +34,12 @@ - + + @@ -48,8 +50,9 @@
-

@@ -57,11 +60,10 @@

- + - - + @@ -86,7 +88,7 @@ - + From fee412e05ed23744f4751d3cb2f28ac6a90c9129 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Fri, 10 Apr 2026 18:35:02 +0530 Subject: [PATCH 13/19] [IMP] estate: implement model and view inheritance - Extended CRUD methods for offer validation, state update - Added constraints to prevent invalid operations (state and offer price check) - Inherited res.users to add property_ids relation - Implemented view inheritance to display properties in user form - Completed chapter 12 --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 11 ++++++++--- estate/models/estate_property_offer.py | 8 ++++++++ estate/models/res_users.py | 10 ++++++++++ estate/views/estate_property_views.xml | 2 +- estate/views/res_users_views.xml | 15 +++++++++++++++ 7 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c5e20e8a34e..ae9edfc5502 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -19,5 +19,6 @@ 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', 'views/estate_menus.xml', + 'views/res_users_views.xml' ], } 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 ccfe771a85c..04f1a06ef95 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -66,9 +66,6 @@ class EstateProperty(models.Model): _check_expected_price = models.Constraint( 'CHECK(expected_price > 0)', 'Price must be strictly positive' ) - _check_selling_price = models.Constraint( - 'CHECK (selling_price > 0)', "Selling price must be strictly positive" - ) @api.depends("living_area", "garden_area") def _compute_total_area(self): @@ -122,3 +119,11 @@ def action_property_cancelled(self): else: record.state = "cancelled" return True + + @api.ondelete(at_uninstall=False) + def delete_state_check(self): + for record in self: + if record.state not in ('new', 'cancelled'): + raise UserError("Only New or Cancelled properties can be deleted") + + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index d857c36b0a4..4c18a5e610d 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -47,7 +47,15 @@ def _inverse_deadline(self): @api.model_create_multi def create(self, vals_list): + for vals in vals_list: + current_price = vals.get('price') + property_id = self.env['estate.property'].browse(vals['property_id']) + for offer in property_id.offer_ids: + if current_price < offer.price: + raise UserError("Offer Price cannot be less than previous offer prices") + offers = super().create(vals_list) + for offer in offers: if offer.property_id.state == 'new': offer.property_id.state = 'offer_received' diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..a2cc9714f47 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _name = 'res.users' + _inherit = ['res.users'] + + property_ids = fields.One2many( + 'estate.property', 'salesperson_id', domain=[('state', 'not in', ('sold', 'cancelled'))] + ) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 2c3aa820792..bf5ad65792a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -15,7 +15,7 @@ - + diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..b3fca2a5e0a --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.form.inherit.estate + res.users + + + + + + + + + + From f00c08dc38e4c15b0ac49e0550bba2df27e7b190 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Tue, 14 Apr 2026 18:40:17 +0530 Subject: [PATCH 14/19] [IMP] estate: integrate accounting module and add kanban view - Integrated account module to create invoices from sold properties - Reused account.move for financial operations - Enhanced kanban view using QWeb templating - Implemented conditional UI logic with t-if and raw values - Chapter 13 and 14 completed. --- estate/models/estate_property.py | 2 +- estate/views/estate_property_views.xml | 32 ++++++++++++++++++++++++- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 6 +++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_account.py | 26 ++++++++++++++++++++ 6 files changed, 66 insertions(+), 2 deletions(-) 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_account.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 04f1a06ef95..b8d4c4bf860 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -60,7 +60,7 @@ class EstateProperty(models.Model): offer_ids = fields.One2many("estate.property.offer", "property_id") total_area = fields.Float(compute="_compute_total_area") best_price = fields.Float( - string="Best Offer", compute="_compute_best_price" + string="Best Offer", compute="_compute_best_price", store=True ) _check_expected_price = models.Constraint( diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index bf5ad65792a..b26d0240f69 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ Estate Property estate.property - list,form + list,form,kanban {'search_default_available_properties':1} @@ -101,4 +101,34 @@ + + + estate.property.kanban + estate.property + + + + + +
+
+ +
+ Expected Price : +
+
+ Best Price : +
+
+ Selling Price : +
+ +
+
+
+
+
+
+
+ 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..b4d4ff5cfad --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,6 @@ +{ + 'name': 'Estate Account', + 'author': "Ayush Khubchandani (aykhu)", + 'license': 'LGPL-3', + 'depends': ['estate', 'account'] +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..02b688798a3 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_account diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py new file mode 100644 index 00000000000..38c75f295ef --- /dev/null +++ b/estate_account/models/estate_account.py @@ -0,0 +1,26 @@ +from odoo import Command, models + + +class EstateAccount(models.Model): + _inherit = 'estate.property' + + def action_property_sold(self): + + super().action_property_sold() + + self.env['account.move'].create([{ + 'partner_id': self.salesperson_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': [ + Command.create({ + 'name': '6% commision', + 'price_unit': self.selling_price * 0.06, + 'quantity': 1, + }), + Command.create({ + 'name': 'Administrative fees', + 'price_unit': 100, + 'quantity': 1, + }) + ] + }]) From 5241d274a25d4624f1673942080c0b70019c9af4 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Thu, 23 Apr 2026 19:02:58 +0530 Subject: [PATCH 15/19] [IMP] estate: add visit scheduling and issue reporting - Added visit scheduling with list, form, and calendar views - Added constraint to prevent overlapping visits within the same property - Automatically create calendar events on visit creation - Added issue reporting with list and form views - Completed Group Tasks. --- estate/__manifest__.py | 6 +- estate/models/__init__.py | 2 + estate/models/estate_property.py | 10 +++ estate/models/estate_property_issues.py | 88 ++++++++++++++++++++ estate/models/estate_property_visit.py | 38 +++++++++ estate/security/ir.model.access.csv | 2 + estate/views/estate_property_issue_views.xml | 64 ++++++++++++++ estate/views/estate_property_views.xml | 8 ++ estate/views/estate_property_visit_views.xml | 44 ++++++++++ 9 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 estate/models/estate_property_issues.py create mode 100644 estate/models/estate_property_visit.py create mode 100644 estate/views/estate_property_issue_views.xml create mode 100644 estate/views/estate_property_visit_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index ae9edfc5502..49f630d403a 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,14 +11,16 @@ 'website': "https://www.odoo.com/app/estate", 'category': 'Tutorials', 'application': True, - 'depends': ['base'], + 'depends': ['base', 'calendar'], 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_issue_views.xml', + 'views/estate_property_visit_views.xml', 'views/estate_property_views.xml', 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', 'views/estate_menus.xml', - 'views/res_users_views.xml' + 'views/res_users_views.xml', ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 9a2189b6382..7be08b9c0b5 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -3,3 +3,5 @@ from . import estate_property_tag from . import estate_property_offer from . import res_users +from . import estate_property_visit +from . import estate_property_issues diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index b8d4c4bf860..9e9d18e81a3 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -62,6 +62,9 @@ class EstateProperty(models.Model): best_price = fields.Float( string="Best Offer", compute="_compute_best_price", store=True ) + visit_ids = fields.One2many('estate.property.visit', 'property_id') + issue_ids = fields.One2many('estate.property.issue', 'property_id') + issue_count = fields.Integer(compute='_compute_issue_count') _check_expected_price = models.Constraint( 'CHECK(expected_price > 0)', 'Price must be strictly positive' @@ -80,6 +83,11 @@ def _compute_best_price(self): else: record.best_price = 0.0 + @api.depends('visit_ids') + def _compute_issue_count(self): + for record in self: + record.issue_count = len(record.issue_ids) + @api.onchange('has_garden') def _onchange_garden(self): if self.has_garden: @@ -108,6 +116,8 @@ def action_property_sold(self): for record in self: if record.state == "cancelled": raise UserError("Cancelled property cannot be set as sold.") + elif record.issue_ids.priority == '3' and record.issue_ids.state != "resolved": + raise UserError("High priority issue is still not resolved") else: record.state = "sold" return True diff --git a/estate/models/estate_property_issues.py b/estate/models/estate_property_issues.py new file mode 100644 index 00000000000..ab0e0a9ed53 --- /dev/null +++ b/estate/models/estate_property_issues.py @@ -0,0 +1,88 @@ +from datetime import timedelta + +from odoo import api, fields, models + +AVAILABLE_PRIORITIES = [ + ('1', 'Low'), + ('2', 'Medium'), + ('3', 'High'), +] + + +class EstatePropertyIssues(models.Model): + _name = 'estate.property.issue' + _description = "Raise and Manage issues in properties" + + name = fields.Char(required=True) + property_id = fields.Many2one('estate.property', required=True) + reported_by = fields.Many2one('res.partner') + assigned_to = fields.Many2one('res.users') + issue_type = fields.Selection( + selection=[ + ('plumbing', 'Plumbing'), + ('electrical', 'Electrical'), + ('structural', 'Structural'), + ('other', 'other'), + ], + required=True + ) + state = fields.Selection( + string="Status", + selection=[ + ("new", "New"), + ("in_progress", "In progress"), + ("resolved", "Resolved"), + ("cancelled", "Cancelled") + ], + default="new", + ) + priority = fields.Selection( + AVAILABLE_PRIORITIES, compute="_compute_priority" + ) + resolved_date = fields.Date() + description = fields.Text() + is_overdue = fields.Boolean(compute="_compute_is_overdue", default=False) + + @api.depends('issue_type') + def _compute_priority(self): + for record in self: + if record.issue_type not in ('electrical', 'structural'): + record.priority = '1' + if record.issue_type == 'electrical': + record.priority = '2' + if record.issue_type == 'structural': + record.priority = '3' + + @api.depends('create_date', 'priority', 'resolved_date') + def _compute_is_overdue(self): + priority_days = { + '3': 2, + '2': 5, + '1': 10, + } + + for record in self: + start_date = record.create_date.date() if record.create_date else fields.Date.today() + resolve_date = record.resolved_date or fields.Date.today() + limit_days = priority_days.get(record.priority) + + if (resolve_date - start_date) > timedelta(days=limit_days): + record.is_overdue = True + else: + record.is_overdue = False + + @api.onchange('assigned_to') + def _check_state(self): + for record in self: + if record.assigned_to and record.state == "new": + record.state = "in_progress" + + def action_set_resolved(self): + for record in self: + record.state = 'resolved' + if not record.resolved_date: + record.resolved_date = fields.Date.today() + + def action_set_cancelled(self): + for record in self: + record.state = 'cancelled' diff --git a/estate/models/estate_property_visit.py b/estate/models/estate_property_visit.py new file mode 100644 index 00000000000..718f72761c1 --- /dev/null +++ b/estate/models/estate_property_visit.py @@ -0,0 +1,38 @@ +from datetime import timedelta + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstatePropertyVisit(models.Model): + _name = "estate.property.visit" + _description = "Schedule for properties" + + salesperson_id = fields.Many2one(related="property_id.salesperson_id") + customer_name = fields.Many2one('res.partner') + visit_date = fields.Datetime(default=fields.Datetime.now()) + property_id = fields.Many2one('estate.property', required=True) + + @api.model_create_multi + def create(self, vals_list): + + visits = super().create(vals_list) + + for visit in visits: + stop_time = visit.visit_date + timedelta(hours=+1) + self.env['calendar.event'].create({ + 'name': 'Property Visit', + 'start': visit.visit_date, + 'stop': stop_time, + }) + + return visits + + @api.constrains('visit_date', 'property_id') + def _check_visit_time(self): + for record in self: + for visit in record.property_id.visit_ids: + if record.id == visit.id or record.visit_date.date() != visit.visit_date.date(): + continue + if record.visit_date - visit.visit_date < timedelta(hours=1): + raise UserError("2 visits cannot have same time") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 0c0b62b7fee..662aadba025 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -3,3 +3,5 @@ estate.access_estate_property,access_estate_property,estate.model_estate_propert estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 +estate.access_estate_visit,access_estate_property_visit,estate.model_estate_property_visit,base.group_user,1,1,1,1 +estate.access_estate_issue,access_estate_property_issue,estate.model_estate_property_issue,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_issue_views.xml b/estate/views/estate_property_issue_views.xml new file mode 100644 index 00000000000..ebbfa10dba9 --- /dev/null +++ b/estate/views/estate_property_issue_views.xml @@ -0,0 +1,64 @@ + + + + + Estate Property Issue + estate.property.issue + list,form + [('property_id', '=', active_id)] + + + + estate.property.issue.form + estate.property.issue + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + estate.property.issue.list + estate.property.issue + + + + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index b26d0240f69..ed0be64e461 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -55,6 +55,11 @@ +
+ +

@@ -96,6 +101,9 @@
+ + +
diff --git a/estate/views/estate_property_visit_views.xml b/estate/views/estate_property_visit_views.xml new file mode 100644 index 00000000000..6a6166c76a4 --- /dev/null +++ b/estate/views/estate_property_visit_views.xml @@ -0,0 +1,44 @@ + + + + + Estate Property Visit + estate.property.visit + list,form,calendar + [('property_id', '=', active_id)] + + + + estate.property.visit.list + estate.property.visit + + + + + + + + + + estate.property.visit.form + estate.property.visit + +
+ + + + +
+ + + estate.property.visit.calendar + estate.property.visit + + + + + + + + +
From 54901bb93299df768b398842c418f564ccb4ccac Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Mon, 20 Apr 2026 14:00:06 +0530 Subject: [PATCH 16/19] [IMP] estate: enhance offers workflow and UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added chatter and activities - Implemented logic to reject all other offers when one is accepted - Added “Accept Best Offer” action to simplify offer selection - Added rainbow animation when a property is marked as sold - Display sold ribbon in form and kanban views - Updated kanban default grouping to state and disabled record drag - Made “Sold” and “Accept Best Offer” buttons conditional primary actions --- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 59 ++++++++++++++------ estate/models/estate_property_issues.py | 12 ++-- estate/models/estate_property_offer.py | 29 +++++----- estate/models/estate_property_visit.py | 4 +- estate/views/estate_property_offer_views.xml | 4 +- estate/views/estate_property_views.xml | 14 +++-- 7 files changed, 75 insertions(+), 49 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 49f630d403a..e8409c57acc 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,7 +11,7 @@ 'website': "https://www.odoo.com/app/estate", 'category': 'Tutorials', 'application': True, - 'depends': ['base', 'calendar'], + 'depends': ['base', 'calendar', 'mail'], 'data': [ 'security/ir.model.access.csv', 'views/estate_property_issue_views.xml', diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9e9d18e81a3..277e6be3c67 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.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, ValidationError from odoo.tools.float_utils import float_compare, float_is_zero @@ -9,6 +9,7 @@ class EstateProperty(models.Model): _name = 'estate.property' _description = 'Estate Property' _order = "id desc" + _inherit = ['mail.thread', 'mail.activity.mixin'] name = fields.Char(string='Title', required=True, default='Unknown') description = fields.Text(string='Description') @@ -85,8 +86,15 @@ def _compute_best_price(self): @api.depends('visit_ids') def _compute_issue_count(self): + data = dict(self.env['estate.property.issue']._read_group( + [('property_id', 'in', self.ids)], + groupby=['property_id'], + aggregates=['__count'] + )) + for record in self: - record.issue_count = len(record.issue_ids) + record.issue_count = data.get(record, 0) + # record.issue_count = len(record.issue_ids) @api.onchange('has_garden') def _onchange_garden(self): @@ -108,32 +116,47 @@ def _check_selling_price(self): min_price, precision_digits=2 ) < 0: - raise ValidationError( + raise ValidationError(_( "The selling price cannot be lower than 90% of the expected price." - ) + )) def action_property_sold(self): - for record in self: - if record.state == "cancelled": - raise UserError("Cancelled property cannot be set as sold.") - elif record.issue_ids.priority == '3' and record.issue_ids.state != "resolved": - raise UserError("High priority issue is still not resolved") - else: - record.state = "sold" + if self.state == "cancelled": + raise UserError(_("Cancelled property cannot be set as sold.")) + elif self.issue_ids.priority == '3' and self.issue_ids.state != "resolved": + raise UserError(_("High priority issue is still not resolved")) + else: + self.state = "sold" return True + def action_set_sold_rainbow_man(self): + self.action_property_sold() + + return { + 'effect': { + 'fadeout': 'slow', + 'img_url': '/web/static/img/smile.svg', + 'type': 'rainbow_man', + } + } + def action_property_cancelled(self): - for record in self: - if record.state == "sold": - raise UserError("Sold property cannot be set as cancelled") - else: - record.state = "cancelled" + if self.state == "sold": + raise UserError(_("Sold property cannot be set as cancelled")) + else: + self.state = "cancelled" + return True + + def action_accept_best_offer(self): + best_offer = self.offer_ids.filtered_domain( + [('price', '=', self.best_price)]) + best_offer.action_offer_accepted() return True @api.ondelete(at_uninstall=False) def delete_state_check(self): for record in self: if record.state not in ('new', 'cancelled'): - raise UserError("Only New or Cancelled properties can be deleted") - + raise UserError( + _("Only New or Cancelled properties can be deleted")) return True diff --git a/estate/models/estate_property_issues.py b/estate/models/estate_property_issues.py index ab0e0a9ed53..6ec1a6ef11c 100644 --- a/estate/models/estate_property_issues.py +++ b/estate/models/estate_property_issues.py @@ -78,11 +78,11 @@ def _check_state(self): record.state = "in_progress" def action_set_resolved(self): - for record in self: - record.state = 'resolved' - if not record.resolved_date: - record.resolved_date = fields.Date.today() + self.state = 'resolved' + if not self.resolved_date: + self.resolved_date = fields.Date.today() + return True def action_set_cancelled(self): - for record in self: - record.state = 'cancelled' + self.state = 'cancelled' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 4c18a5e610d..ebcb794c133 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 @@ -52,7 +52,7 @@ def create(self, vals_list): property_id = self.env['estate.property'].browse(vals['property_id']) for offer in property_id.offer_ids: if current_price < offer.price: - raise UserError("Offer Price cannot be less than previous offer prices") + raise UserError(_("Offer Price cannot be less than previous offer prices")) offers = super().create(vals_list) @@ -62,19 +62,18 @@ def create(self, vals_list): return offers - def action_accepted(self): - for record in self: - for offer in record.property_id.offer_ids: - if offer.status == "accepted": - raise UserError( - "Only 1 offer can be accepted for each property") - record.status = "accepted" - record.property_id.selling_price = record.price - record.property_id.buyer_id = record.partner_id - record.property_id.state = "offer_accepted" + def action_offer_accepted(self): + if self.price < self.property_id.best_price: + raise UserError(_("Another higher price offer exists")) + self.status = "accepted" + self.property_id.selling_price = self.price + self.property_id.buyer_id = self.partner_id + self.property_id.state = "offer_accepted" + + offers = self.property_id.offer_ids.filtered(lambda x: x.status != "accepted") + offers.write({'status': 'refused'}) return True - def action_refused(self): - for record in self: - record.status = "refused" + def action_offer_refused(self): + self.status = "refused" return True diff --git a/estate/models/estate_property_visit.py b/estate/models/estate_property_visit.py index 718f72761c1..24f346e3f02 100644 --- a/estate/models/estate_property_visit.py +++ b/estate/models/estate_property_visit.py @@ -1,6 +1,6 @@ from datetime import timedelta -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -35,4 +35,4 @@ def _check_visit_time(self): if record.id == visit.id or record.visit_date.date() != visit.visit_date.date(): continue if record.visit_date - visit.visit_date < timedelta(hours=1): - raise UserError("2 visits cannot have same time") + raise UserError(_("2 visits cannot have same time")) diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 17d0a28e5a9..a3e0898a041 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -17,8 +17,8 @@ - + + +
+ +
+
+ + + diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..1140c493eb4 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,16 @@ +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.onincrement) { + this.props.onincrement(); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..6db9b2fc5bc --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,10 @@ + + +
+ Counter: + +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..4a8ffec4d47 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,17 @@ -import { Component } from "@odoo/owl"; +import { Component, 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, TodoList }; + + setup() { + this.sum = useState({ value: 0 }); + } + + incrementSum() { + this.sum.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..1c1c63f2564 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,15 @@ - - + + +
- hello world + + + "The sum is :" +
+
+
-
diff --git a/awesome_owl/static/src/todo/todoList.xml b/awesome_owl/static/src/todo/todoList.xml new file mode 100644 index 00000000000..b17927f3461 --- /dev/null +++ b/awesome_owl/static/src/todo/todoList.xml @@ -0,0 +1,14 @@ + + Todo Input: +
+ + + +
+
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..ed750cfde13 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,21 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + + static props = { + id: Number, + description: String, + isCompleted: Boolean, + toggleTodo: Function, + removeTodo: Function, + }; + + onToggle() { + this.props.toggleTodo(this.props.id); + } + + onRemove(){ + this.props.removeTodo(this.props.id); + } +} 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..2caf9553de9 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,41 @@ +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.inputRef = useAutofocus("InputValue") + this.todos = useState([]); + this.nextId = 1 + this.inputValue = useState({ text: "" }); + this.removeTodo = this.removeTodo.bind(this); + } + + addTodo(ev) + { + if (ev.keyCode != 13) return + this.todos.push({ + id : this.nextId++, + description : this.inputValue.text, + isCompleted : false + }) + this.inputValue.text = "" + } + + toggleTodo = (id) => { + const todo = this.todos.find(t => t.id == id); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(id) { + const index = this.todos.findIndex(todo => todo.id === id); + if (index !== -1) { + this.todos.splice(index, 1); + } + } +} diff --git a/awesome_owl/static/src/todo/todoitem.xml b/awesome_owl/static/src/todo/todoitem.xml new file mode 100644 index 00000000000..ee3e5838559 --- /dev/null +++ b/awesome_owl/static/src/todo/todoitem.xml @@ -0,0 +1,14 @@ + +
+ + + id: - + description: - + iscompleted: + + +
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..46a5a66101c --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,11 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(refName) { + const ref = useRef(refName); + + onMounted(() => { + ref.el.focus(); + }); + + return ref; +} From 73e51189ab94f0439669dcbe1fad3d65175184b4 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Fri, 8 May 2026 17:54:00 +0530 Subject: [PATCH 18/19] [IMP] awesome_dashboard: complete Owl dashboard with configurable items This commit implements the Awesome Dashboard as part of learning Owl. The main goal was to understand how frontend dashboard is built using Owl instead of using xml for standard list/form/kanban views as these are not suitable when we want to display mixed information like KPIs, charts and quick actions in one place, so a custom dashboard is needed. Explored how components, services and hooks are used together. The dashboard is split into smaller parts so it is easier to follow and not everything is in a single file. Also added the option to add/remove dashboard items. This was mainly to understand how state is handled on the client side. --- awesome_dashboard/__manifest__.py | 5 +- awesome_dashboard/static/src/dashboard.js | 8 -- awesome_dashboard/static/src/dashboard.xml | 8 -- .../static/src/dashboard/dashboard.js | 88 +++++++++++++++++++ .../static/src/dashboard/dashboard.scss | 3 + .../static/src/dashboard/dashboard.xml | 38 ++++++++ .../static/src/dashboard/dashboard_items.js | 67 ++++++++++++++ .../src/dashboard/item/dashboard_item.js | 15 ++++ .../src/dashboard/item/dashboard_item.xml | 8 ++ .../src/dashboard/number_card/number_card.js | 10 +++ .../src/dashboard/number_card/number_card.xml | 9 ++ .../static/src/dashboard/piechart/piechart.js | 37 ++++++++ .../src/dashboard/piechart/piechart.xml | 3 + .../dashboard/piechart_card/piechart_card.js | 14 +++ .../dashboard/piechart_card/piechart_card.xml | 11 +++ .../src/dashboard/statistics_service.js | 21 +++++ .../static/src/dashboard_action.js | 22 +++++ 17 files changed, 350 insertions(+), 17 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js delete mode 100644 awesome_dashboard/static/src/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/item/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/piechart/piechart.js create mode 100644 awesome_dashboard/static/src/dashboard/piechart/piechart.xml create mode 100644 awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.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..5e14e8beec8 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -25,6 +25,9 @@ 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*' + ] }, - 'license': 'AGPL-3' + 'license': 'AGPL-3', } diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index c4fb245621b..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..d99a6f632f1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,88 @@ +import { _t } from "@web/core/l10n/translation"; +import { Component, useState } from "@odoo/owl"; +import { Layout } from "@web/search/layout"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./item/dashboard_item"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; + + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem}; + setup(){ + this.action = useService("action"); + this.statistics = useService("awesome_dashboard.statistics"); + this.dialog = useService("dialog"); + this.results = useState(this.statistics.stats); + this.items = registry.category("awesome_dashboard").getAll(); + this.state = useState({ + disabledItems: browser.localStorage.getItem("disabledDashboardItems")?.split(",") || [] + }); + } + + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }) + } + updateConfiguration(newDisabledItems) { + this.state.disabledItems = newDisabledItems; + } + openCustomers() { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: "res.partner", + name: _t("Customers"), + views: [ + [false, "kanban"], + ], + }); + } + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: "crm.lead", + name: _t("Leads"), + views: [ + [false, "list"], + [false, "form"], + ], + }); + } +} + +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); + } +} + +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..90e1493325f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: grey; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..006b541538c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + +
+ + + + + +
+
+
+ + + 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 new file mode 100644 index 00000000000..9396b035122 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,67 @@ +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./piechart_card/piechart_card"; +import { registry } from "@web/core/registry"; +import { _t } from "@web/core/l10n/translation"; + + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (data) => ({ + title: _t("Average amount of t-shirt by order this month"), + value: data.average_quantity, + }) + }, + { + id: "average_time", + description: "Average time for an order", + Component: NumberCard, + props: (data) => ({ + title: _t("Average time for an order to go from 'new' to 'sent' or 'cancelled'"), + value: data.average_time, + }) + }, + { + id: "number_new_orders", + description: "New orders this month", + Component: NumberCard, + props: (data) => ({ + title: _t("Number of new orders this month"), + value: data.nb_new_orders, + }) + }, + { + id: "cancelled_orders", + description: "Cancelled orders this month", + Component: NumberCard, + props: (data) => ({ + title: _t("Number of cancelled orders this month"), + value: data.nb_cancelled_orders, + }) + }, + { + id: "amount_new_orders", + description: "amount orders this month", + Component: NumberCard, + props: (data) => ({ + title: _t("Total amount of new orders this month"), + value: data.total_amount, + }) + }, + { + id: "pie_chart", + description: "Shirt orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: _t("Shirt orders by size"), + data: data.orders_by_size, + }) + } +] + +items.forEach(item => { + registry.category("awesome_dashboard").add(item.id, item); +}); diff --git a/awesome_dashboard/static/src/dashboard/item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/item/dashboard_item.js new file mode 100644 index 00000000000..5dc7b0866aa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/item/dashboard_item.js @@ -0,0 +1,15 @@ +import { Component } from "@odoo/owl"; + + +export class DashboardItem extends Component{ + static template="awesome_dashboard.DashboardItem" + + static props = { + size: { type: Number, optional: true }, + slots: { type: Object, optional: true }, + }; + get width() { + const size = this.props.size || 1; + return `width: ${18 * size}rem;`; + } +} diff --git a/awesome_dashboard/static/src/dashboard/item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/item/dashboard_item.xml new file mode 100644 index 00000000000..f58d2a1203f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/item/dashboard_item.xml @@ -0,0 +1,8 @@ + + + +
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..5c3344d6277 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl"; + + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { type: String }, + value: { type: Number }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..cd2c0143bce --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,9 @@ + + + +
+
+

+

+ + diff --git a/awesome_dashboard/static/src/dashboard/piechart/piechart.js b/awesome_dashboard/static/src/dashboard/piechart/piechart.js new file mode 100644 index 00000000000..9ec803cd839 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.js @@ -0,0 +1,37 @@ +import { Component, onWillStart, onMounted, useRef } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + onMounted(() => { + this.renderChart(); + }); + } + + renderChart() { + const ctx = this.canvasRef.el.getContext("2d"); + const data = this.props.data || {}; + new Chart(ctx, { + type: "pie", + data: { + labels: ["S", "M", "XL"], + datasets: [ + { + data: [ + data.s || 0, + data.m || 0, + data.xl || 0, + ], + }, + ], + }, + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard/piechart/piechart.xml b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml new file mode 100644 index 00000000000..0b1834f0b4e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml @@ -0,0 +1,3 @@ + + + diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js new file mode 100644 index 00000000000..2c81708847c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js @@ -0,0 +1,14 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "../piechart/piechart"; + + +export class PieChartCard extends Component { + static components = { PieChart }; + static template = "awesome_dashboard.PieChartCard"; + + static components = { PieChart }; + static props = { + title: { type: String }, + values: { type: Object }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml new file mode 100644 index 00000000000..7116b6e20b0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml @@ -0,0 +1,11 @@ + + + +
+
+
+ +
+
+
+
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..14b7c87d01e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,21 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + + +const statisticsService = { + start() { + const stats = reactive({}); + async function loadStatistics() { + const result = await rpc("/awesome_dashboard/statistics"); + Object.assign(stats, result); + } + loadStatistics(); + setInterval(loadStatistics, 600000); + return { + stats, + }; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..0767cf3fbf1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,22 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; +import { Component, xml } from "@odoo/owl"; + + +class AwesomeDashboardAction extends Component { + static components = { LazyComponent }; + + static template = xml` + + `; +} + +registry.category("actions").add( + "awesome_dashboard.dashboard", + AwesomeDashboardAction +); From 73a3fb90f401fb9890cd0951b0138addde718ee4 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Fri, 15 May 2026 10:26:39 +0530 Subject: [PATCH 19/19] [ADD] sale_rental_deposit: introduce deposit mechanism for rental products Rental workflows currently lack a way to enforce or manage security deposits, which are a common business requirement for renting high-value or damage-sensitive products. Without this, users must rely on manual workarounds such as adding extra order lines or handling deposits outside the system, leading to: - higher risk of human error, - lack of traceability between rented product and its deposit, - poor user experience on online product page(no visibility of required deposit). This change introduces a structured and configurable deposit mechanism to standardize how deposits are handled across the system. A dedicated deposit product can now be defined in settings, ensuring consistency in accounting and reuse across all rental operations. At the product level, a deposit requirement and per-unit amount can be configured, allowing flexibility depending on the nature of the item. The system automatically manages deposit lines when rental products are added to orders, ensuring that: - deposits are always applied when required, - amounts stay synchronized with product quantities, The same logic is extended to the website to guarantee consistency between backend and frontend flows, and to provide transparency to customers by displaying deposit information before checkout. --- sale_renting_deposit/__init__.py | 1 + sale_renting_deposit/__manifest__.py | 16 ++++++ sale_renting_deposit/models/__init__.py | 3 ++ .../models/product_template.py | 8 +++ .../models/res_config_settings.py | 11 ++++ .../models/sale_order_lines.py | 54 +++++++++++++++++++ .../static/src/js/deposit_rental.js | 18 +++++++ .../views/product_template_views.xml | 16 ++++++ .../views/res_config_settings_views.xml | 19 +++++++ .../views/website_sale_view.xml | 27 ++++++++++ 10 files changed, 173 insertions(+) create mode 100644 sale_renting_deposit/__init__.py create mode 100644 sale_renting_deposit/__manifest__.py create mode 100644 sale_renting_deposit/models/__init__.py create mode 100644 sale_renting_deposit/models/product_template.py create mode 100644 sale_renting_deposit/models/res_config_settings.py create mode 100644 sale_renting_deposit/models/sale_order_lines.py create mode 100644 sale_renting_deposit/static/src/js/deposit_rental.js create mode 100644 sale_renting_deposit/views/product_template_views.xml create mode 100644 sale_renting_deposit/views/res_config_settings_views.xml create mode 100644 sale_renting_deposit/views/website_sale_view.xml diff --git a/sale_renting_deposit/__init__.py b/sale_renting_deposit/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_renting_deposit/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_renting_deposit/__manifest__.py b/sale_renting_deposit/__manifest__.py new file mode 100644 index 00000000000..8a1eb9810d1 --- /dev/null +++ b/sale_renting_deposit/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'Renting Deposit', + 'author': "Ayush Khubchandani (aykhu)", + 'license': 'LGPL-3', + 'depends': ['sale_renting', 'website_sale'], + 'data': [ + 'views/product_template_views.xml', + 'views/res_config_settings_views.xml', + 'views/website_sale_view.xml', + ], + 'assets': { + 'web.assets_frontend': [ + 'sale_renting_deposit/static/src/js/deposit_rental.js', + ], + }, +} diff --git a/sale_renting_deposit/models/__init__.py b/sale_renting_deposit/models/__init__.py new file mode 100644 index 00000000000..ab748108c8d --- /dev/null +++ b/sale_renting_deposit/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_template +from . import res_config_settings +from . import sale_order_lines diff --git a/sale_renting_deposit/models/product_template.py b/sale_renting_deposit/models/product_template.py new file mode 100644 index 00000000000..d0a3bb1886c --- /dev/null +++ b/sale_renting_deposit/models/product_template.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + deposit_required = fields.Boolean(help="Enable if this product requires deposit.") + deposit_amount = fields.Float(help="This specifies deposit for 1 unit of this product.") diff --git a/sale_renting_deposit/models/res_config_settings.py b/sale_renting_deposit/models/res_config_settings.py new file mode 100644 index 00000000000..41e07de379d --- /dev/null +++ b/sale_renting_deposit/models/res_config_settings.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + deposit_product_id = fields.Many2one( + "product.product", + string="Deposit Product", + config_parameter="sale_renting.deposit_product_id" + ) diff --git a/sale_renting_deposit/models/sale_order_lines.py b/sale_renting_deposit/models/sale_order_lines.py new file mode 100644 index 00000000000..7392904e56c --- /dev/null +++ b/sale_renting_deposit/models/sale_order_lines.py @@ -0,0 +1,54 @@ +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + is_deposit_line = fields.Boolean(default=False) + deposit_origin_line_id = fields.Many2one('sale.order.line', ondelete='cascade') + + def _get_line_header(self): + if self.is_deposit_line and self.name: + return self.name + return super()._get_line_header() + + @api.model_create_multi + def create(self, vals_list): + order_lines = super().create(vals_list) + for line in order_lines: + if (line.product_id.deposit_required and not line.is_deposit_line): + deposit_product_param = self.env["ir.config_parameter"].sudo().get_param( + "sale_renting.deposit_product_id" + ) + deposit_product_id = int(deposit_product_param) + deposit_amount = line.product_id.deposit_amount + self.create({ + 'order_id': line.order_id.id, + 'product_id': deposit_product_id, + 'product_uom_qty': line.product_uom_qty, + 'price_unit': deposit_amount, + 'name': f"Deposit for {line.product_id.name}", + 'is_deposit_line': True, + 'deposit_origin_line_id': line.id, + }) + return order_lines + + def write(self, vals): + res = super().write(vals) + deposit_lines = self.env['sale.order.line'].search( + [('deposit_origin_line_id', 'in', self.ids)] + ) + for line in self: + if line.is_deposit_line: + continue + deposit_line = deposit_lines.filtered( + lambda l: l.deposit_origin_line_id == line + ) + if not deposit_line: + continue + deposit_line.write({ + 'product_uom_qty': line.product_uom_qty, + 'price_unit': line.product_id.deposit_amount, + 'name': f"Deposit for {line.product_id.name}", + }) + return res diff --git a/sale_renting_deposit/static/src/js/deposit_rental.js b/sale_renting_deposit/static/src/js/deposit_rental.js new file mode 100644 index 00000000000..36bda5c2001 --- /dev/null +++ b/sale_renting_deposit/static/src/js/deposit_rental.js @@ -0,0 +1,18 @@ +document.addEventListener('change', function (ev) { + const input = ev.target; + if (!input.matches('.js_main_product input[name="add_qty"]')) { + return; + } + const productEl = input.closest('.js_product'); + const depositEl = productEl?.querySelector('.o_deposit_wrapper'); + if (!depositEl) { + return; + } + const depositUnit = parseFloat(depositEl.dataset.depositUnit) || 0; + const quantity = parseFloat(input.value) || 0; + const total = (depositUnit * quantity).toFixed(2); + const target = depositEl.querySelector('.o_deposit_amount_value'); + if (target) { + target.textContent = total; + } +}); diff --git a/sale_renting_deposit/views/product_template_views.xml b/sale_renting_deposit/views/product_template_views.xml new file mode 100644 index 00000000000..e681c1ff387 --- /dev/null +++ b/sale_renting_deposit/views/product_template_views.xml @@ -0,0 +1,16 @@ + + + + + product.template.inherit.stock.rental + product.template + + + + + + + + + + diff --git a/sale_renting_deposit/views/res_config_settings_views.xml b/sale_renting_deposit/views/res_config_settings_views.xml new file mode 100644 index 00000000000..b3954d974aa --- /dev/null +++ b/sale_renting_deposit/views/res_config_settings_views.xml @@ -0,0 +1,19 @@ + + + + + res.config.settings.view.form.inherit.rental + res.config.settings + + + + +
+
+
+
+
+ +
diff --git a/sale_renting_deposit/views/website_sale_view.xml b/sale_renting_deposit/views/website_sale_view.xml new file mode 100644 index 00000000000..6e1238590cf --- /dev/null +++ b/sale_renting_deposit/views/website_sale_view.xml @@ -0,0 +1,27 @@ + + + + + +