From 90c491a934d4ce585ca3e125e6b2db522285b65d Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Thu, 3 Jul 2025 10:37:53 +0530 Subject: [PATCH 01/10] [ADD] estate: initial module creation Created a new 'estate' module as per Chapters 2 and 3. Defined the module structure, added the business object 'estate.property' in a Python class, and configured the __manifest__.py and __init__.py to make the app visible in the apps page. --- estate/__init__.py | 1 + estate/__manifest__.py | 14 ++++++++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 23 +++++++++++++++++++++++ 4 files changed, 39 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..e65d7ac5523 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': "Estate", + 'version': '1.0', + 'license': 'LGPL-3', + 'depends': ['base'], + 'author': "Kalpan Desai", + 'category': 'Estate/sales', + 'description': """ + Module specifically designed for real estate business case. + """, + 'installable': True, + 'application': True, + +} \ No newline at end of file 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..cbbaf7c42a5 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,23 @@ +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + + name = fields.Char('Name', required=True) + description = fields.Text('Description') + postcode = fields.Char('Postcode') + date_availability = fields.Date('Available From') + expected_price = fields.Float('Expected Price', required=True) + selling_price = fields.Float('Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer('Bedrooms', default=2) + living_area = fields.Integer('Living Area (sqm)') + facades = fields.Integer('Facades') + garage = fields.Boolean('Garage') + garden = fields.Boolean('Garden') + garden_area = fields.Integer('Garden Area (sqm)') + garden_orientation = fields.Selection( + [('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + string='Garden Orientation' + ) From c8c44a504a95defacc975458913e6d06ca138b81 Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Fri, 4 Jul 2025 08:28:42 +0530 Subject: [PATCH 02/10] [ADD] estate: added access rights, field attributes and custom Views Applied the security access to base users group using CSV. Started working on UI: Create the estate_property_views.xml file in the Views folder Created an action for the model estate.property. Created the estate_menus.xml Created the three levels of menus for the estate.property Added the appropriate default attributes so that: the default number of bedrooms is 2 the default availability date is in 3 months Added active and state field. Defined a custom list view for the estate.property Defined a custom form view for the estate.property Defined a custom search view for the estate.property --- .gitignore | 3 + estate/__manifest__.py | 9 ++- estate/models/estate_property.py | 14 ++++- estate/security/ir.model.access.csv | 2 + estate/views/estate_menus.xml | 13 +++++ estate/views/estate_property_views.xml | 81 ++++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/.gitignore b/.gitignore index b6e47617de1..802d4df7341 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +#VS CODE +.vscode/ \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py index e65d7ac5523..0598c140183 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,5 +10,10 @@ """, 'installable': True, 'application': True, - -} \ No newline at end of file + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml' + ] + +} diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index cbbaf7c42a5..d8d005736f0 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,14 +1,18 @@ from odoo import fields, models +from dateutil.relativedelta import relativedelta class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property" - name = fields.Char('Name', required=True) + name = fields.Char('Name', required=True, default='Unknown Property') description = fields.Text('Description') postcode = fields.Char('Postcode') - date_availability = fields.Date('Available From') + date_availability = fields.Date( + "Date Availability", + default=lambda self: fields.Date.to_string(fields.Date.context_today(self) + relativedelta(months=3)) + ) expected_price = fields.Float('Expected Price', required=True) selling_price = fields.Float('Selling Price', readonly=True, copy=False) bedrooms = fields.Integer('Bedrooms', default=2) @@ -21,3 +25,9 @@ class EstateProperty(models.Model): [('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], string='Garden Orientation' ) + active = fields.Boolean('Active', default=True) + state = fields.Selection( + [('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), ('canceled', 'Canceled')], + string='Status', default='new', required=True, copy=False + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..45a7de697cd --- /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_base_group,estate.property.basegroup,model_estate_property,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..7545dc3dd2f --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..d0f783591ee --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,81 @@ + + + + Properties + estate.property + list,form + [('active', '=', True)] + + + estate.property.list.view + estate.property + + + + + + + + + + + + + + + estate.property.search.view + estate.property + + + + + + + + + + + + + + + + + + estate.property.properties.form.view + estate.property + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
From a152e620841f41b7f3d9cc003b3f1e7c7e1639ae Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Mon, 7 Jul 2025 10:24:42 +0530 Subject: [PATCH 03/10] [ADD] estate: added property type, Tags, offers and States MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added Property type model, created view and added it to menu Added filed to Buyer and salesperson field to property model Added Property tag model, created view and added it to menu Created many2many relation between property tags and property model Added Property offer model, created view and added property form page. Added the total_area field to estate.property. It is defined as the sum of the living_area and the garden_area. Added the best_price field to estate.property. It is defined as the highest (i.e. maximum) of the offers’ price. Added validity and date_deadline to property offer Defined inverse function so that the user can set either the date or the validity. Created an onchange in the estate.property model in order to set values for the garden area (10) and orientation (North) when garden is set to True. When unset, clear the fields. Added the buttons ‘Cancel’ and ‘Sold’ to the estate.property model. A cancelled property cannot be set as sold, and a sold property cannot be cancelled. Added the buttons ‘Accept’ and ‘Refuse’ to the estate.property.offer model. When an offer is accepted, set the buyer and the selling price for the corresponding property. --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 3 ++ estate/models/estate_property.py | 47 ++++++++++++++++++- estate/models/estate_property_offer.py | 49 ++++++++++++++++++++ estate/models/estate_property_tag.py | 9 ++++ estate/models/estate_property_type.py | 12 +++++ estate/security/ir.model.access.csv | 3 ++ estate/views/estate_menus.xml | 34 ++++++++++---- estate/views/estate_property_offer_views.xml | 43 +++++++++++++++++ estate/views/estate_property_tag_views.xml | 32 +++++++++++++ estate/views/estate_property_type_views.xml | 41 ++++++++++++++++ estate/views/estate_property_views.xml | 45 ++++++++++++++---- 12 files changed, 301 insertions(+), 20 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 0598c140183..9ce44e44f33 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -13,6 +13,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 d8d005736f0..15aec89cbc8 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ -from odoo import fields, models +from odoo import api, fields, models +from odoo.exceptions import UserError from dateutil.relativedelta import relativedelta @@ -31,3 +32,47 @@ class EstateProperty(models.Model): ('sold', 'Sold'), ('canceled', 'Canceled')], string='Status', default='new', required=True, copy=False ) + property_type_id = fields.Many2one("estate.property.type", string="Property Type", required=True) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user) + tags_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + total_area = fields.Float(compute="_compute_total_area", string="Total Area", readonly=True) + best_price = fields.Float(compute="_compute_best_price", string="Best Price", readonly=True) + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + prices = record.offer_ids.mapped("price") + record.best_price = max(prices, default=0) + + @api.onchange("garden") + def _onchange_garden(self): + for record in self: + if self.garden: + record.garden_area = 10 + record.garden_orientation = "north" + else: + record.garden_area = 0 + record.garden_orientation = False + + def action_sold(self): + for record in self: + if record.state != "canceled": + record.state = "sold" + else: + raise UserError("Canceled properties can't be sold") + return True + + def action_cancel(self): + for record in self: + if record.state != "sold": + record.state = "canceled" + else: + raise UserError("Sold properties can't be sold") + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..dbd81f27e3f --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,49 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Property Offer" + _order = "price asc" + + price = fields.Float(string="Price", required=True) + status = fields.Selection( + string="Type", + selection=[("accepted", "Accepted"), ("refused", "Refused")], + copy=False, + ) + partner_id = fields.Many2one("res.partner", string='salesperson', required=True) + property_id = fields.Many2one("estate.property", required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_date_deadline", inverse="_inverse_date_deadline" + ) + + + @api.depends("validity") + def _compute_date_deadline(self): + for record in self: + if not record.create_date: + today = fields.Datetime.today() + record.date_deadline = base_date + timedelta(days=record.validity) + else: + record.date_deadline = record.create_date + timedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days + + def action_accept_offer(self): + for record in self: + record.status = "accepted" + if record.property_id: + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + record.property_id.state = "offer_accepted" + return True + + def action_refuse_offer(self): + for record in self: + record.status = "refused" + return True diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..2f61a84c8f4 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Property Tags" + _order = "name" + + 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..6bc2926bac7 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Types of properties available in the estate module." + + + name = fields.Char('Name', required=True) + property_ids = fields.One2many( + "estate.property", "property_type_id", string="Property" + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 45a7de697cd..81bca172f47 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_base_group,estate.property.basegroup,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type_base_group,estate.property.type.basegroup,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag_base_group,estate.property.tag.basegroup,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer_base_group,estate.property.offer.basegroup,model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 7545dc3dd2f..6132d69745f 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,13 +1,29 @@ - + - - - + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..1d0229cbfb2 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,43 @@ + + + + Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + + estate.property.offer.list + estate.property.offer + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e4a71e1a8a7..d063c3ef7e8 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,12 +5,17 @@ estate.property list,form [('active', '=', True)] + {'search_default_state': True} estate.property.list.view estate.property - + + @@ -65,7 +70,7 @@ - + @@ -78,8 +83,8 @@ - - + + @@ -88,7 +93,7 @@ - + From bd46021bff84f3f31c6799f51c8af356d08bafbd Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Wed, 9 Jul 2025 10:21:57 +0530 Subject: [PATCH 05/10] [ADD] estate: added invoicing and user Category Added Kanban View to the property page Added automated invoicing for the sold property added user types in module defined in user defination --- estate/__manifest__.py | 5 +- estate/models/__init__.py | 1 + estate/models/estate_property.py | 12 ++++- estate/models/estate_property_offer.py | 25 +++++----- estate/models/estate_property_type.py | 1 - estate/models/estate_res_user.py | 8 ++++ estate/views/estate_property_offer_views.xml | 2 +- estate/views/estate_property_tag_views.xml | 2 +- estate/views/estate_property_type_views.xml | 2 +- estate/views/estate_property_views.xml | 48 +++++++++++++++++--- estate/views/estate_res_user_views.xml | 15 ++++++ estate_account/__init__.py | 1 + estate_account/__manifest__.py | 21 +++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 32 +++++++++++++ estate_account/security/ir.model.access.csv | 1 + 16 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 estate/models/estate_res_user.py create mode 100644 estate/views/estate_res_user_views.xml 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 create mode 100644 estate_account/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9ce44e44f33..000bf853601 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -13,10 +13,11 @@ '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' + 'views/estate_res_user_views.xml', + 'views/estate_menus.xml', ] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..ff7189569b6 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 estate_res_user diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 97d4c1c7c5a..79da56d3a05 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -3,12 +3,11 @@ from odoo.exceptions import UserError, ValidationError from odoo.tools import float_utils, _ + class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property" _order = "id desc" - - name = fields.Char('Name', required=True, default='Unknown Property') description = fields.Text('Description') postcode = fields.Char('Postcode') @@ -42,6 +41,10 @@ class EstateProperty(models.Model): total_area = fields.Float(compute="_compute_total_area", string="Total Area", readonly=True) best_price = fields.Float(compute="_compute_best_price", string="Best Price", readonly=True) + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must strictly be Positive.'), + ('check_selling_price', 'CHECK(selling_price > 0)', 'The selling price must strictly be positive.'), + ] @api.depends("garden_area", "living_area") def _compute_total_area(self): @@ -88,3 +91,8 @@ def _check_price(self): if float_utils.float_compare(record.selling_price, record.expected_price * 0.9, precision_rounding=3) == -1: raise ValidationError(_('The selling cannot be lower than 90% of the expected price.')) + + @api.ondelete(at_uninstall=False) + def _unlink_if_state_check(self): + if any(record.state not in ('new', 'canceled') for record in self): + raise UserError(_("You cannot delete a property that is not in the 'New' or 'Canceled' state.")) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index b36186048da..4464a200c21 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -24,13 +24,13 @@ class EstatePropertyOffer(models.Model): _sql_constraints = [ ("check_price", "CHECK(price > 0)", "The price must be strictly positive.") ] - + @api.depends("validity") def _compute_date_deadline(self): for record in self: if not record.create_date: today = fields.Datetime.today() - record.date_deadline = base_date + timedelta(days=record.validity) + record.date_deadline = today + timedelta(days=record.validity) else: record.date_deadline = record.create_date + timedelta(days=record.validity) @@ -38,22 +38,12 @@ def _inverse_date_deadline(self): for record in self: record.validity = (record.date_deadline - record.create_date.date()).days - def action_accept_offer(self): - for record in self: - record.status = "accepted" - if record.property_id: - 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_accept_offer(self): for offer in self: if offer.property_id.state in {'accepted', 'sold'}: - raise UserError(_('An offer has already been accepted for this property.')) + raise UserError('An offer has already been accepted for this property.') offer.write({'status': 'accepted'}) - offer.property_id.write({ 'selling_price': offer.price, 'buyer_id': offer.partner_id.id, @@ -66,3 +56,12 @@ def action_refuse_offer(self): for record in self: record.status = "refused" return True + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + property = self.env["estate.property"].browse(vals["property_id"]) + property.state = "offer_received" + if property.best_price > vals["price"]: + raise UserError("The offer must be higher than the existing offer") + return super().create(vals_list) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 3a426da80f5..2fc65e7b714 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -16,7 +16,6 @@ class EstatePropertyType(models.Model): ('check_property_type_name', 'UNIQUE(name)', 'A Type must be unique.'), ] - @api.depends('offer_ids') def compute_offer_count(self): for record in self: diff --git a/estate/models/estate_res_user.py b/estate/models/estate_res_user.py new file mode 100644 index 00000000000..107f5957082 --- /dev/null +++ b/estate/models/estate_res_user.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class User(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", inverse_name="salesperson_id") + domain = ["|", ("state", "=", "new"), ("state", "=", "offer_received")] diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 647c0eb6285..861ee7760c7 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -41,4 +41,4 @@ - \ No newline at end of file + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index 5a5a93c9377..e5aa8ab8f1a 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -29,4 +29,4 @@ - \ No newline at end of file + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 8f2095c2e8c..0a6913b7ccc 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -49,4 +49,4 @@ - \ No newline at end of file + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d063c3ef7e8..2bb443be00a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,12 +1,5 @@ - - Properties - estate.property - list,form - [('active', '=', True)] - {'search_default_state': True} - estate.property.list.view estate.property @@ -108,4 +101,45 @@ + + estate.property.kanban + estate.property + + + + +
+ +
+ Expected Price: + +
+
+ Best Price: + +
+
+ Selling Price: + +
+ +
+
+
+
+
+
+ + + Properties + estate.property + list,form,kanban + [('active', '=', True)] + {'search_default_state': True} + +
diff --git a/estate/views/estate_res_user_views.xml b/estate/views/estate_res_user_views.xml new file mode 100644 index 00000000000..8b2cc83490c --- /dev/null +++ b/estate/views/estate_res_user_views.xml @@ -0,0 +1,15 @@ + + + + estate.res.user.form.inherit + res.users + + + + + + + + + + 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..b45806be921 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': "Estate_Account", + 'version': '1.0', + 'license': 'LGPL-3', + 'depends': ['estate', 'account'], + 'author': "Kalpan Desai", + 'category': 'Estate/Accounting', + 'description': """ + Module specifically designed for real estate accounting case. + This module extends the estate module to include accounting features. + It allows users to manage financial transactions related to properties, + such as tracking payments, managing invoices, and handling financial reports. + """, + 'installable': True, + 'application': True, + + 'data': [ + 'security/ir.model.access.csv', + ] + +} 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..a93f2991492 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,32 @@ +from odoo import Command, models + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold(self): + # print("Log by kdes inside Child model", flush=True) + self.ensure_one() + self.env["account.move"].create( + { + "partner_id": self.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create( + { + "name": self.name, + "quantity": 1, + "price_unit": 0.06 * self.selling_price, + } + ), + Command.create( + { + "name": "administrative fees", + "quantity": 1, + "price_unit": 100.00, + } + ), + ], + } + ) + return super().action_sold() diff --git a/estate_account/security/ir.model.access.csv b/estate_account/security/ir.model.access.csv new file mode 100644 index 00000000000..97dd8b917b8 --- /dev/null +++ b/estate_account/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink From 606c43e93506ac02583f500ed4ada868ecbfd8ec Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Wed, 16 Jul 2025 11:13:03 +0530 Subject: [PATCH 06/10] [ADD] estate: added security right per user Managers could see all properties Salesperson could see properties assigned to them and those which are not assigned to any user User can only view properties that belongs to their company added default demo data for property type and property --- estate/__manifest__.py | 7 ++++- estate/demo/estate.property.type.csv | 4 +++ estate/demo/estate.property.xml | 39 ++++++++++++++++++++++++ estate/models/estate_property.py | 1 + estate/security/ir.model.access.csv | 12 +++++--- estate/security/security.xml | 44 ++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 estate/demo/estate.property.type.csv create mode 100644 estate/demo/estate.property.xml create mode 100644 estate/security/security.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 000bf853601..9bb82078e76 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,6 +11,7 @@ 'installable': True, 'application': True, 'data': [ + 'security/security.xml', 'security/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_property_offer_views.xml', @@ -18,6 +19,10 @@ 'views/estate_property_tag_views.xml', 'views/estate_res_user_views.xml', 'views/estate_menus.xml', - ] + ], + 'demo': [ + 'demo/estate.property.type.csv', + 'demo/estate.property.xml', + ], } diff --git a/estate/demo/estate.property.type.csv b/estate/demo/estate.property.type.csv new file mode 100644 index 00000000000..8490d446c15 --- /dev/null +++ b/estate/demo/estate.property.type.csv @@ -0,0 +1,4 @@ +"id","name","sequence","offer_count" +2,"Commercial",1,0 +3,"Industrial",1,0 +4,"Land",1,0 \ No newline at end of file diff --git a/estate/demo/estate.property.xml b/estate/demo/estate.property.xml new file mode 100644 index 00000000000..f630103b00b --- /dev/null +++ b/estate/demo/estate.property.xml @@ -0,0 +1,39 @@ + + + Residential + + + + Big Villa + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 1500000 + 6 + 100 + 4 + 1 + 1 + 100000 + south + + + + + Trailer home + canceled + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 95000 + 1 + 10 + 4 + 0 + 1 + + + diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 79da56d3a05..2dce6e2be45 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -40,6 +40,7 @@ class EstateProperty(models.Model): offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") total_area = fields.Float(compute="_compute_total_area", string="Total Area", readonly=True) best_price = fields.Float(compute="_compute_best_price", string="Best Price", readonly=True) + company_id = fields.Many2one("res.company", string="Company", default=lambda self: self.env.company) _sql_constraints = [ ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must strictly be Positive.'), diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 81bca172f47..019560224e9 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,9 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property_base_group,estate.property.basegroup,model_estate_property,base.group_user,1,1,1,1 -access_estate_property_type_base_group,estate.property.type.basegroup,model_estate_property_type,base.group_user,1,1,1,1 -access_estate_property_tag_base_group,estate.property.tag.basegroup,model_estate_property_tag,base.group_user,1,1,1,1 -access_estate_property_offer_base_group,estate.property.offer.basegroup,model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property_manager_group,estate.property.managergroup,model_estate_property,estate_group_manager,1,1,1,1 +access_estate_property_type_manager_group,estate.property.type.managergroup,model_estate_property_type,estate_group_manager,1,1,1,1 +access_estate_property_tag_manager_group,estate.property.tag.managergroup,model_estate_property_tag,estate_group_manager,1,1,1,1 +access_estate_property_offer_manager_group,estate.property.offer.managergroup,model_estate_property_offer,estate_group_manager,1,1,1,1 +access_estate_property_base_group,estate.property.basegroup,model_estate_property,estate_group_user,1,1,1,0 +access_estate_property_type_base_group,estate.property.type.basegroup,model_estate_property_type,estate_group_user,1,0,0,0 +access_estate_property_tag_base_group,estate.property.tag.basegroup,model_estate_property_tag,estate_group_user,1,0,0,0 +access_estate_property_offer_base_group,estate.property.offer.basegroup,model_estate_property_offer,estate_group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/security/security.xml b/estate/security/security.xml new file mode 100644 index 00000000000..144e2765ee6 --- /dev/null +++ b/estate/security/security.xml @@ -0,0 +1,44 @@ + + + Real Estate Brokerage + 10 + + + + Agent + + + + + Manager + + + + + + Agent: See/modify own or unassigned properties in own company + + + + + + + [ + '&', + ('company_id', '=', user.company_id.id), + '|', + ('salesperson_id', '=', user.id), + ('salesperson_id', '=', False) + ] + + + + Manager: Full access to all properties of all companies + + + + + + + + \ No newline at end of file From 193a95238745246d11b5773a6c1c2ab52ccf3f02 Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Wed, 16 Jul 2025 12:40:38 +0530 Subject: [PATCH 07/10] [IMP] estate: added report feature added report generation feature for the properties added property details in report and added offers to the report added demo data for ease commented out last demo data files in manifest file as it was causing replication issues --- estate/__manifest__.py | 7 +- estate/data/estate_demo.xml | 122 ++++++++++++++++++++++++++ estate/report/estate_report_views.xml | 26 ++++++ estate/report/estate_reports.xml | 101 +++++++++++++++++++++ 4 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 estate/data/estate_demo.xml create mode 100644 estate/report/estate_report_views.xml create mode 100644 estate/report/estate_reports.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 9bb82078e76..5fff1410520 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -19,10 +19,13 @@ 'views/estate_property_tag_views.xml', 'views/estate_res_user_views.xml', 'views/estate_menus.xml', + 'report/estate_reports.xml', + 'report/estate_report_views.xml', ], 'demo': [ - 'demo/estate.property.type.csv', - 'demo/estate.property.xml', + 'data/estate_demo.xml', + # 'demo/estate.property.type.csv', + # 'demo/estate.property.xml', ], } diff --git a/estate/data/estate_demo.xml b/estate/data/estate_demo.xml new file mode 100644 index 00000000000..df6f4150c9a --- /dev/null +++ b/estate/data/estate_demo.xml @@ -0,0 +1,122 @@ + + + + + Residential + + + + Commercial + + + + Industrial + + + + Land + + + + Big Villa + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 6 + 100 + 4 + True + True + 100000 + south + + + + + + Trailer Home + canceled + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 120000 + 1 + 10 + 4 + False + True + + + + + + International Space Station + new + Aliens sometimes come visit + ---- + 2030-12-31 + 45890000 + + + + + + Cozy Cabin + sold + Small cabin by lake + 10000 + 2020-01-01 + 80000 + 1 + 10 + 4 + False + True + + + + + + 10000 + 14 + + + + + + + 1500000 + 14 + + + + + + + 1500001 + 14 + + + + + + + 60000 + 14 + + + + + + + 75000 + 14 + + + + + + \ No newline at end of file diff --git a/estate/report/estate_report_views.xml b/estate/report/estate_report_views.xml new file mode 100644 index 00000000000..a9cb7515a73 --- /dev/null +++ b/estate/report/estate_report_views.xml @@ -0,0 +1,26 @@ + + + + + Property Offers + estate.property + qweb-pdf + estate.report_property + estate.report_property + 'Property Offers' + + + + + + + Property Offers + res.users + qweb-pdf + estate.report_salesman_properties + estate.report_salesman_properties + 'Salesman Property Offers' + + + + \ No newline at end of file diff --git a/estate/report/estate_reports.xml b/estate/report/estate_reports.xml new file mode 100644 index 00000000000..3e9b4678b6c --- /dev/null +++ b/estate/report/estate_reports.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + From e1c17d9cf2ee729c9e13d8243c6982681e9c70a5 Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Thu, 17 Jul 2025 12:06:06 +0530 Subject: [PATCH 08/10] [ADD] awsome_owl: added counter Card and ToDo components Implemented basic Counter component with useState and increment method Extracted Counter as reusable sub-component with own template and files Created Card component with title and content props, styled with Bootstrap Enabled rendering of safe HTML content using t-out and markup function Added props validation for Card and Counter components Implemented parent-child communication: Counter triggers onChange callback to update Playground sum Built TodoList and TodoItem components with hardcoded todos and t-foreach rendering Added dynamic classes to TodoItem based on isCompleted state Enabled adding todos via input and keyup event with unique IDs Used t-ref and useRef with onMounted to autofocus input, extracted useAutofocus hook Implemented toggling todo completion state with callback prop toggleState Added removeTodo callback and delete functionality in TodoItem with click handler Refactored Card component to use slots for arbitrary content, including nested components Added toggle button to Card to show/hide content with local state and conditional rendering --- awesome_owl/static/src/card/card.js | 22 ++++++++++ awesome_owl/static/src/card/card.xml | 16 +++++++ awesome_owl/static/src/counter/counter.js | 21 ++++++++++ awesome_owl/static/src/counter/counter.xml | 9 ++++ awesome_owl/static/src/playground.js | 22 +++++++++- awesome_owl/static/src/playground.xml | 9 +++- awesome_owl/static/src/todo_list/todo_item.js | 22 ++++++++++ .../static/src/todo_list/todo_item.xml | 13 ++++++ awesome_owl/static/src/todo_list/todo_list.js | 42 +++++++++++++++++++ .../static/src/todo_list/todo_list.xml | 12 ++++++ awesome_owl/static/src/utils.js | 10 +++++ 11 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml create mode 100644 awesome_owl/static/src/todo_list/todo_item.js create mode 100644 awesome_owl/static/src/todo_list/todo_item.xml create mode 100644 awesome_owl/static/src/todo_list/todo_list.js create mode 100644 awesome_owl/static/src/todo_list/todo_list.xml create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..1b4ae7f24fa --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,22 @@ +/** @odoo-module */ + +import { Component, useState} from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title: String, + slots: { + type: Object, + shape: { + default: true + }, + } + }; + setup() { + this.state = useState({ isOpen: true }); + } + toggleContent() { + this.state.isOpen = !this.state.isOpen; + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..6f7cee439f1 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,16 @@ + + + +
+
+
+ + +
+

+ +

+
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..1c8c9bc5b21 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,21 @@ +/** @odoo-module */ + +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.Counter"; + static props = { + onChange: { type: Function, optional: true }, + }; + setup() { + this.state = useState({ value: 1 }); + } + + increment() { + this.state.value = this.state.value + 1; + if (this.props.onChange) { + this.props.onChange(); + } + + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..eb55e850b3c --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,9 @@ + + + +
+ Counter : + +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..25115152888 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,27 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, markup , useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo_list/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + static props = { }; + static components = { + Counter, + Card, + TodoList, + }; + + setup() { + this.str1 = "
some content
"; + this.str2 = markup("
some content
"); + this.sum = useState({ + value: 2, + }); + } + incrementSum() { + this.sum.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..7473e4f0a87 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,9 +2,16 @@ -
+
hello world + + +
Card 1 Content
+ +
The sum is:
+ + diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js new file mode 100644 index 00000000000..d140e1d821b --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,22 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + static props = { + todo: { + type: Object, + shape: { id: Number, description: String, isCompleted: Boolean } + }, + toggleState: Function, + removeTodo: Function, + }; + + onChange() { + this.props.toggleState(this.props.todo.id); + } + onRemove() { + this.props.removeTodo(this.props.todo.id); + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml new file mode 100644 index 00000000000..1abdb31910d --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,13 @@ + + + +
+ + +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..5ef8a810a36 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,42 @@ +/** @odoo-module */ + +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 }; + static props = { }; + setup() { + this.nextId = 1; + this.todos = useState([]); + useAutofocus("todoInput"); + } + + addTodo(event) { + if (event.key === "Enter" && event.target.value!== "") { + this.todos.push({ + id: this.nextId++, + description: event.target.value, + isCompleted: false, + }); + event.target.value = ""; + } + } + + toggleTodo(id) { + const todo = this.todos.find(todo => todo.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); + } + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml new file mode 100644 index 00000000000..578a5ba00f4 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,12 @@ + + + +
+

Todo List

+ + +
+
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..df699bf9595 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,10 @@ +/** @odoo-module */ + +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(refName) { + const ref = useRef(refName); + onMounted(() => { + ref.el.focus(); + }); +} \ No newline at end of file From ddb633a55feca055ddeb5df2a07dadf6e08aa05a Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Thu, 17 Jul 2025 18:34:36 +0530 Subject: [PATCH 09/10] [ADD] awsome_dashboard: implement features with layout, services, and charts Integrated component from @web/search/layout for consistent UI structure Applied class with custom background via new Added control panel buttons for: Navigating to Customers via action XML ID Navigating to Leads via dynamic CRM model action Created reusable component with optional prop for flexible layout Used to fetch data from Displayed key metrics: new orders, total amount, avg t-shirt/order, cancelled orders, avg processing time Created a persistent service using for cached data loading Updated Dashboard to consume statistics service with and reactive state Added component with lazy-loaded Chart.js to visualize t-shirt sales by size Enabled auto-refresh of statistics every 10s (simulating real-time data) Used a shared reactive object in the service for live updates in Dashboard --- awesome_dashboard/static/src/dashboard.js | 40 ++++++++++++++++- awesome_dashboard/static/src/dashboard.scss | 3 ++ awesome_dashboard/static/src/dashboard.xml | 43 ++++++++++++++++++- .../static/src/dashboard_item.js | 20 +++++++++ .../static/src/dashboard_item.xml | 10 +++++ .../static/src/pie_chart/pie_chart.js | 43 +++++++++++++++++++ .../static/src/pie_chart/pie_chart.xml | 10 +++++ .../static/src/statistics_service.js | 26 +++++++++++ 8 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/pie_chart/pie_chart.js create mode 100644 awesome_dashboard/static/src/pie_chart/pie_chart.xml create mode 100644 awesome_dashboard/static/src/statistics_service.js diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 637fa4bb972..2d8e8d05c03 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,10 +1,46 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, onWillStart, useState } from "@odoo/owl"; import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item"; +import { rpc } from "@web/core/network/rpc"; +import { PieChart } from "./pie_chart/pie_chart"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; -} + static components = { + Layout, + DashboardItem, + PieChart, + }; + + setup() { + this.action = useService("action"); + + this.statistics = useState(useService("awesome_dashboard.statistics")); + this.display = { + controlPanel: {}, + }; + + } + + openCustomerView() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "All leads", + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"] + ], + }); + } +} registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss new file mode 100644 index 00000000000..769fc1e72f9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: gray; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 1a2ac9a2fed..ec455baafd7 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -2,7 +2,48 @@ - hello dashboard + + + + + +
+ + Average amount of t-shirt by order this month +
+ +
+
+ + Average time for an order to go from 'new' to 'sent' or 'cancelled' +
+ +
+
+ + Number of new orders this month +
+ +
+
+ + Number of cancelled orders this month +
+ +
+
+ + Total amount of new orders this month +
+ +
+
+ + Shirt orders by size + + +
+
diff --git a/awesome_dashboard/static/src/dashboard_item.js b/awesome_dashboard/static/src/dashboard_item.js new file mode 100644 index 00000000000..6bf12fa64c9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item.js @@ -0,0 +1,20 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem" + static props = { + slots: { + type: Object, + shape: { + default: Object + }, + }, + size: { + type: Number, + default: 1, + optional: true, + }, + }; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_item.xml b/awesome_dashboard/static/src/dashboard_item.xml new file mode 100644 index 00000000000..d023340bb59 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.js b/awesome_dashboard/static/src/pie_chart/pie_chart.js new file mode 100644 index 00000000000..29de2774d35 --- /dev/null +++ b/awesome_dashboard/static/src/pie_chart/pie_chart.js @@ -0,0 +1,43 @@ +/** @odoo-module */ + +import { loadJS } from "@web/core/assets"; +import { getColor } from "@web/core/colors/colors"; +import { Component, onWillStart, useRef, onMounted, onWillUnmount } from "@odoo/owl"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + label: String, + data: Object, + }; + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"])); + onMounted(() => { + this.renderChart(); + }); + onWillUnmount(() => { + this.chart.destroy(); + }); + } + + renderChart() { + const labels = Object.keys(this.props.data); + const data = Object.values(this.props.data); + const color = labels.map((_, index) => getColor(index)); + this.chart = new Chart(this.canvasRef.el, { + type: "pie", + data: { + labels: labels, + datasets: [ + { + label: this.props.label, + data: data, + backgroundColor: color, + }, + ], + }, + }); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..18416e9a223 --- /dev/null +++ b/awesome_dashboard/static/src/pie_chart/pie_chart.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js new file mode 100644 index 00000000000..b33b7feb258 --- /dev/null +++ b/awesome_dashboard/static/src/statistics_service.js @@ -0,0 +1,26 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { memoize } from "@web/core/utils/functions"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +const statisticsService = { + // dependencies: ["rpc"], + + start(env) { + const statistics = reactive({ isReady: false }); + + async function loadData() { + const updates = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, updates, { isReady: true }); + } + + setInterval(loadData, 10*60*1000); + loadData(); + + return statistics; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); \ No newline at end of file From 11b45754a919d3d3fae68d4e5069a71b7a78e270 Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Fri, 18 Jul 2025 17:46:33 +0530 Subject: [PATCH 10/10] [IMP] awesome_dashboard: implement dynamic and extensible dashboard Update statistics service to reload data reactively every 10 seconds (for testing) Use reactive object for statistics to enable live dashboard updates Modify Dashboard component to consume reactive statistics via useState Implement lazy loading of dashboard assets and component using LazyComponent and asset bundles Refactor dashboard to be generic, rendering dashboard items dynamically from a registry Create NumberCard and PieChartCard components for dashboard items Register dashboard items in awesome_dashboard registry for easy extensibility Add dashboard customization UI with a settings dialog and checkboxes for enabling/disabling items Persist user dashboard configuration in localStorage to maintain preferences across sessions Filter dashboard items displayed based on user configuration --- awesome_dashboard/__manifest__.py | 4 + awesome_dashboard/static/src/dashboard.js | 46 -------- .../static/src/dashboard/dashboard.js | 100 ++++++++++++++++++ .../static/src/{ => dashboard}/dashboard.scss | 0 .../static/src/{ => dashboard}/dashboard.xml | 35 +++++- .../dashboard_item}/dashboard_item.js | 0 .../dashboard_item}/dashboard_item.xml | 0 .../static/src/dashboard/dashboard_items.js | 67 ++++++++++++ .../src/dashboard/number_card/number_card.js | 15 +++ .../src/dashboard/number_card/number_card.xml | 10 ++ .../{ => dashboard}/pie_chart/pie_chart.js | 0 .../{ => dashboard}/pie_chart/pie_chart.xml | 0 .../pie_chart_card/pie_chart_card.js | 17 +++ .../pie_chart_card/pie_chart_card.xml | 7 ++ .../src/{ => dashboard}/statistics_service.js | 1 - .../static/src/dashboard_loader.js | 15 +++ 16 files changed, 268 insertions(+), 49 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js rename awesome_dashboard/static/src/{ => dashboard}/dashboard.scss (100%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard.xml (61%) rename awesome_dashboard/static/src/{ => dashboard/dashboard_item}/dashboard_item.js (100%) rename awesome_dashboard/static/src/{ => dashboard/dashboard_item}/dashboard_item.xml (100%) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.xml rename awesome_dashboard/static/src/{ => dashboard}/pie_chart/pie_chart.js (100%) rename awesome_dashboard/static/src/{ => dashboard}/pie_chart/pie_chart.xml (100%) create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml rename awesome_dashboard/static/src/{ => dashboard}/statistics_service.js (92%) create mode 100644 awesome_dashboard/static/src/dashboard_loader.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..9cb889db903 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -24,6 +24,10 @@ 'assets': { 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', + ('remove', 'awesome_dashboard/static/src/dashboard/**/*'), + ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*' ], }, 'license': 'AGPL-3' diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index 2d8e8d05c03..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,46 +0,0 @@ -/** @odoo-module **/ - -import { Component, onWillStart, useState } from "@odoo/owl"; -import { registry } from "@web/core/registry"; -import { Layout } from "@web/search/layout"; -import { useService } from "@web/core/utils/hooks"; -import { DashboardItem } from "./dashboard_item"; -import { rpc } from "@web/core/network/rpc"; -import { PieChart } from "./pie_chart/pie_chart"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; - - static components = { - Layout, - DashboardItem, - PieChart, - }; - - setup() { - this.action = useService("action"); - - this.statistics = useState(useService("awesome_dashboard.statistics")); - this.display = { - controlPanel: {}, - }; - - } - - openCustomerView() { - this.action.doAction("base.action_partner_form"); - } - - openLeads() { - this.action.doAction({ - type: "ir.actions.act_window", - name: "All leads", - res_model: "crm.lead", - views: [ - [false, "list"], - [false, "form"] - ], - }); - } -} -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..ac911ac01aa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,100 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; +// import { PieChart } from "./pie_chart/pie_chart"; +// import { items } from "./dashboard_items"; +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 = useState(useService("awesome_dashboard.statistics")); + this.dialog = useService("dialog"); + this.display = { + controlPanel: {}, + }; + 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; + } + + openCustomerView() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "All leads", + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"] + ], + }); + } +} + +class ConfigurationDialog extends Component { + static template = "awesome_dashboard.ConfigurationDialog"; + static components = { Dialog, CheckBox }; + static props = ["close", "items", "disabledItems", "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("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss similarity index 100% rename from awesome_dashboard/static/src/dashboard.scss rename to awesome_dashboard/static/src/dashboard/dashboard.scss diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml similarity index 61% rename from awesome_dashboard/static/src/dashboard.xml rename to awesome_dashboard/static/src/dashboard/dashboard.xml index ec455baafd7..347363ca50e 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -7,8 +7,15 @@ + + + + +
- + + + + + + + + +
+ + + Which cards do you whish to see ? + + + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js similarity index 100% rename from awesome_dashboard/static/src/dashboard_item.js rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js diff --git a/awesome_dashboard/static/src/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml similarity index 100% rename from awesome_dashboard/static/src/dashboard_item.xml rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml 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..273dd577643 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,67 @@ + /** @odoo-module */ + +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; +import { registry } from "@web/core/registry"; + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity, + }) + }, + { + id: "average_time", + description: "Average time for an order", + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time, + }) + }, + { + id: "number_new_orders", + description: "New orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders, + }) + }, + { + id: "cancelled_orders", + description: "Cancelled orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders, + }) + }, + { + id: "amount_new_orders", + description: "amount orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount, + }) + }, + { + id: "pie_chart", + description: "Shirt orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "Shirt orders by size", + values: data.orders_by_size, + }) + } +] + +items.forEach(item => { + registry.category("awesome_dashboard").add(item.id, item); +}); 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..cc5e2d439d7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,15 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { + type: String, + }, + value: { + type: Number, + } + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..ec9361a9ebf --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,10 @@ + + + + + +
+ +
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js similarity index 100% rename from awesome_dashboard/static/src/pie_chart/pie_chart.js rename to awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml similarity index 100% rename from awesome_dashboard/static/src/pie_chart/pie_chart.xml rename to awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js new file mode 100644 index 00000000000..62c6f15a21f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js @@ -0,0 +1,17 @@ +/** odoo-module */ + +import { Component } from "@odoo/owl"; +import { PieChart } from "../pie_chart/pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PieChart } + static props = { + title: { + type: String, + }, + values: { + type: Object, + }, + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml new file mode 100644 index 00000000000..b8c94bebb64 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js similarity index 92% rename from awesome_dashboard/static/src/statistics_service.js rename to awesome_dashboard/static/src/dashboard/statistics_service.js index b33b7feb258..f398af65caa 100644 --- a/awesome_dashboard/static/src/statistics_service.js +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -1,7 +1,6 @@ /** @odoo-module */ import { registry } from "@web/core/registry"; -import { memoize } from "@web/core/utils/functions"; import { rpc } from "@web/core/network/rpc"; import { reactive } from "@odoo/owl"; diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..de5ebe1de75 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,15 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; +import { Component, xml } from "@odoo/owl"; + +class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; + +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); \ No newline at end of file