From 291e43d840dd057ae139d30cfe5e7141adb2b17f Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Thu, 24 Jul 2025 11:24:34 +0530 Subject: [PATCH 01/12] [ADD] estate: create a real estate app with basic models using Object relational mapping. Key changes include: - Defined core model (estate.property) - Created `__init__.py` and `__manifest__.py` - Added name and required dependencies of our module in `__manifest__.py` - Added field declarations for estate.property This commit lays the foundation for a functional real estate management system in Odoo. --- estate/__init__.py | 1 + estate/__manifest__.py | 11 +++++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 27 +++++++++++++++++++++++++++ 4 files changed, 40 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..9a7e03eded3 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..0592aa46c1c --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,11 @@ +{ + 'name': "Real Estate", + 'description': """Real Estate Application.""", + 'summary': """Real Estate Application for beginner.""", + 'depends': ['base'], + 'author': "Aaryan Parpyani (aarp)", + 'category': "Tutorials/RealEstate", + 'version': '1.0', + 'application': True, + 'installable': 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..63266319a01 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,27 @@ +from odoo import fields, models + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Test" + + name = fields.Char(string="Title") + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float() + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + [ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ], + string="Garden Orientation" + ) \ No newline at end of file From 066933b6dfca816c870dc24fe15dac6b8061acb5 Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Thu, 24 Jul 2025 12:23:01 +0530 Subject: [PATCH 02/12] [IMP] estate: implement the basic `estate.property` with required field declarations. - Added `__manifest__.py` with module metadata. - Created the `models` directory and added `__init__.py` to register models. - Implemented the basic `estate.property` model with fields such as: - name, description, postcode - expected_price, selling_price - bedrooms, living_area, facades, garage - garden, garden_area, garden_orientation, date_availability This serves as the foundational structure for the Real Estate application as part of the Odoo tutorials. --- estate/models/estate_property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 63266319a01..816566c4744 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -12,7 +12,7 @@ class EstateProperty(models.Model): selling_price = fields.Float() bedrooms = fields.Integer() living_area = fields.Integer() - facades = fields.Integer() + facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() From 163a7e8157be5f31d91d3399eb434de83de3ea4b Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Thu, 24 Jul 2025 18:49:31 +0530 Subject: [PATCH 03/12] [IMP] estate: create a real estate app with basic models, views, and security rules Key changes include: - Defined core models (estate.property, estate.property.type, estate.property.tag). - Added fields declarations for estate.property, estate.property.type and estate.property.tag. - Set up basic security access and created model-specific access control CSV. - Configured menus, actions, and views (list, form, search). - Linked tags and property types using relational fields. - Applied domain filters and default values for a cleaner UI. --- estate/__manifest__.py | 5 ++ estate/models/estate_property.py | 27 +++++-- estate/security/ir.model.access.csv | 2 + estate/static/description/city.png | Bin 0 -> 7097 bytes estate/views/estate_menus_views.xml | 7 ++ estate/views/estate_property_views.xml | 105 +++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/static/description/city.png create mode 100644 estate/views/estate_menus_views.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 0592aa46c1c..c1144e3e6b0 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,4 +8,9 @@ 'version': '1.0', 'application': True, 'installable': True, + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_menus_views.xml', + 'views/estate_property_views.xml', + ] } \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 816566c4744..4447769bd80 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,16 +1,18 @@ from odoo import fields, models +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta class EstateProperty(models.Model): _name = "estate.property" _description = "Estate Test" - name = fields.Char(string="Title") - description = fields.Text() + name = fields.Char(string="Name", required=True) + description = fields.Char() postcode = fields.Char() - date_availability = fields.Date() - expected_price = fields.Float() - selling_price = fields.Float() - bedrooms = fields.Integer() + date_availability = fields.Date(String="Available From", copy=False, default=lambda self: (datetime.today() + relativedelta(months=3)).date()) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() @@ -24,4 +26,17 @@ class EstateProperty(models.Model): ('west', 'West') ], string="Garden Orientation" + ) + state = fields.Selection( + [ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], + string="Status", + required=True, + copy=False, + default='new' ) \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..ab63520e22b --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/static/description/city.png b/estate/static/description/city.png new file mode 100644 index 0000000000000000000000000000000000000000..f95f98bf3d29fa71589fff3d28c48cede03531c4 GIT binary patch literal 7097 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 WP8m;}RhJiyr)+Wfsb~}J_ + + + + + + \ 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..2cf466acfaf --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,105 @@ + + + + + estate.property + + + + + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + + + + + + + + Estate Entry + estate.property + list,form + +
+
From f992186f7c4a2e585474150783a666f3dfc57499 Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Thu, 24 Jul 2025 19:36:09 +0530 Subject: [PATCH 04/12] [IMP] estate: fix linting issues and improve code formatting as per Odoo guidelines This commit resolves multiple formatting and linting issues to align with Odoo's official coding guidelines: - Added missing newline at end of files (`__init__.py`, `__manifest__.py`, `estate_property.py`). - Removed unused import `timedelta` from `datetime`. - Added an extra blank line before class definition `EstateProperty` in file `estate_property.py`. - Removed trailing whitespace from lines 5 and 17 in `estate_property.py`. - Ensured proper indentation and closing bracket formatting. These improvements enhance code readability, maintainability, and compliance with Odoo community standards. --- estate/__init__.py | 2 +- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c1144e3e6b0..6750546a33e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -13,4 +13,4 @@ 'views/estate_menus_views.xml', 'views/estate_property_views.xml', ] -} \ No newline at end of file +} diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4447769bd80..3247f4fd9a9 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,20 +1,20 @@ from odoo import fields, models -from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta -class EstateProperty(models.Model): + +class EstateProperty(models.Model): _name = "estate.property" _description = "Estate Test" name = fields.Char(string="Name", required=True) description = fields.Char() postcode = fields.Char() - date_availability = fields.Date(String="Available From", copy=False, default=lambda self: (datetime.today() + relativedelta(months=3)).date()) + date_availability = fields.Date(String="Available From", copy=False, default=fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) living_area = fields.Integer() - facades = fields.Integer() + facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() @@ -39,4 +39,4 @@ class EstateProperty(models.Model): required=True, copy=False, default='new' - ) \ No newline at end of file + ) From 0f74440b3deec130fae915b098c3fab3b0305d48 Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Mon, 28 Jul 2025 15:19:10 +0530 Subject: [PATCH 05/12] [IMP] estate: implement only one offer can be accepted and create invoice on property sold, do code cleanup. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit improves the usability of the Real Estate module by: - Making list views for Offers and Tags editable for quick updates. - Adding row coloring: refused offers appear red, accepted ones green, and the status column has been removed for visual clarity. - Enabling the ‘Available’ filter by default in property list action to surface active listings. - Modifying the living_area search input to include properties with an area greater than or equal to the entered value. - Introducing a stat button on the property type form that shows the number of linked offers. - Introduced kanban view for the properties that are on sale, grouped by property type. When a property is marked as sold, automatically create a draft customer invoice containing: - Selling price of the property. - 6% commission on the propertly selling price. - Flat $100 Administrative fees. Refactored `estate` module: - Resolved linting and indentation error. - Corrected XML ID and name for xml files according to Odoo coding guidelines. - Replaced deprecated 'kanban-box' with 'card' template. - Deleted unnecessary files. - Removed unnecessary docstrings and code comments. - Implemented the logic, that if one offer is accepted for the property then others are automatically marked as refused. - Replaced direct field assignments with `write()` method for multiple fields. - Corrected typo errors in the files. - Removed dead code that wasn't necessary. --- estate/__manifest__.py | 19 ++- estate/models/__init__.py | 4 + estate/models/estate_property.py | 114 ++++++++++++++++- estate/models/estate_property_offer.py | 128 +++++++++++++++++++ estate/models/estate_property_tag.py | 18 +++ estate/models/estate_property_type.py | 38 ++++++ estate/models/inherited_user.py | 10 ++ estate/security/ir.model.access.csv | 5 +- estate/views/estate_menus_views.xml | 14 +- estate/views/estate_property_offer_views.xml | 51 ++++++++ estate/views/estate_property_tags_views.xml | 20 +++ estate/views/estate_property_type_views.xml | 64 ++++++++++ estate/views/estate_property_views.xml | 120 +++++++++++++---- estate/views/res_users_views.xml | 14 ++ estate_account/__init__.py | 1 + estate_account/__manifest__.py | 10 ++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 47 +++++++ 18 files changed, 636 insertions(+), 42 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/models/inherited_user.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tags_views.xml create mode 100644 estate/views/estate_property_type_views.xml create mode 100644 estate/views/res_users_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 diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6750546a33e..b194a41d05c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,16 +1,21 @@ { - 'name': "Real Estate", - 'description': """Real Estate Application.""", - 'summary': """Real Estate Application for beginner.""", + 'name': 'Real Estate', + 'description': 'Real Estate Application.', + 'summary': 'Real Estate Application for beginner.', 'depends': ['base'], - 'author': "Aaryan Parpyani (aarp)", - 'category': "Tutorials/RealEstate", + 'author': 'Aaryan Parpyani (aarp)', + 'category': 'Tutorials/RealEstate', 'version': '1.0', 'application': True, 'installable': True, + 'license': 'LGPL-3', 'data': [ - 'security/ir.model.access.csv', - 'views/estate_menus_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tags_views.xml', + 'views/res_users_views.xml', 'views/estate_property_views.xml', + 'views/estate_menus_views.xml', + 'security/ir.model.access.csv', ] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..b8a9f7e0dfd 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,5 @@ from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import inherited_user diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3247f4fd9a9..53f2f5fd8d3 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,15 +1,31 @@ -from odoo import fields, models +# imports of python lib from dateutil.relativedelta import relativedelta +# imports of odoo +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero + class EstateProperty(models.Model): - _name = "estate.property" - _description = "Estate Test" + # === Private attributes === + _name = 'estate.property' + _description = 'Estate Test' + _order = 'id desc' + + # SQL Constraints + _sql_constraints = [ + ('check_expected_price_positive', 'CHECK(expected_price > 0)', 'Expected price must be strictly positive.'), + ('check_selling_price_positive', 'CHECK(selling_price > 0)', 'Selling price must be strictly positive.'), + ] - name = fields.Char(string="Name", required=True) + # --------------------- + # Fields declaration + # --------------------- + name = fields.Char(string='Name', required=True) description = fields.Char() postcode = fields.Char() - date_availability = fields.Date(String="Available From", copy=False, default=fields.Date.today() + relativedelta(months=3)) + date_availability = fields.Date(string='Available From', copy=False, default=fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) @@ -18,6 +34,9 @@ class EstateProperty(models.Model): garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() + active = fields.Boolean(default=True) + + # Selection fields garden_orientation = fields.Selection( [ ('north', 'North'), @@ -25,7 +44,7 @@ class EstateProperty(models.Model): ('east', 'East'), ('west', 'West') ], - string="Garden Orientation" + string='Garden Orientation' ) state = fields.Selection( [ @@ -35,8 +54,89 @@ class EstateProperty(models.Model): ('sold', 'Sold'), ('cancelled', 'Cancelled') ], - string="Status", + string='Status', required=True, copy=False, default='new' ) + + # Many2one fields + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + salesman_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) + buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) + + # Many2many fields + tag_ids = fields.Many2many('estate.property.tag', string='Tags') + + # One2many fields + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + + # Computed fields + total_area = fields.Integer(string='Total Area', compute='_compute_total_area') + best_price = fields.Float(string='Best Offer', compute='_compute_best_price') + + # ----------------------- + # Compute methods + # ----------------------- + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for record in self: + record.total_area = (record.living_area) + (record.garden_area) + + @api.depends('offer_ids.price') + def _compute_best_price(self): + for record in self: + if record.offer_ids: + record.best_price = max(record.offer_ids.mapped('price')) + else: + record.best_price = 0 + + # ---------------------------------- + # Constraints and onchange methods + # ---------------------------------- + @api.constrains('expected_price', 'selling_price') + def _check_selling_price(self): + for property in self: + if not float_is_zero(property.selling_price, precision_digits=2): + min_allowed = 0.9 * property.expected_price + if float_compare(property.selling_price, min_allowed, precision_digits=2) < 0: + raise ValidationError('Selling price cannot be lower than 90% of the expected price.') + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.write({ + 'garden_area': 10, + 'garden_orientation': 'north' + }) + else: + self.write({ + 'garden_area': 0, + 'garden_orientation': False + }) + + # ---------------------- + # CRUD methods + # ----------------------- + @api.ondelete(at_uninstall=False) + def _unlink_except_new_or_cancelled(self): + for record in self: + if record.state not in ['new', 'cancelled']: + raise UserError('You can only delete properties that are in New or Cancelled state.') + + # ---------------------- + # Action methods + # ---------------------- + def action_mark_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError('Cancelled properties cannot be marked as sold.') + record.state = 'sold' + return True + + def action_mark_cancelled(self): + for record in self: + if record.state == 'sold': + raise UserError('Sold properties cannot be marked as cancelled.') + record.state = 'cancelled' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..360a8c02511 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,128 @@ +# imports of python lib +from datetime import timedelta + +# imports of odoo +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare + + +class EstatePropertyOffer(models.Model): + # === Private attributes === + _name = 'estate.property.offer' + _description = 'Offers for all the property listings.' + _order = 'price desc' + + # SQL Constraints + _sql_constraints = [ + ('check_price_positive', 'CHECK(price > 0)', 'Offer price must be strictly positive.'), + ] + + # ------------------------ + # Fields declaration + # ------------------------- + price = fields.Float(string='Price') + validity = fields.Integer(default=7) + + # Selection fields + status = fields.Selection( + [ + ('accepted', 'Accepted'), + ('refused', 'Refused') + ], + string='Status', + copy=False + ) + + # Computed fields + date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline', store=True) + + # Many2one fields + partner_id = fields.Many2one('res.partner', required=True) + property_id = fields.Many2one('estate.property', string='Property Name', required=True) + property_type_id = fields.Many2one(related='property_id.property_type_id', store=True) + + # -------------------- + # Compute methods + # -------------------- + @api.depends('create_date', 'validity', 'date_deadline') + def _compute_date_deadline(self): + for offer in self: + base_date = (offer.create_date or fields.Datetime.now()).date() + offer.date_deadline = base_date + timedelta(days=offer.validity or 0) + + def _inverse_date_deadline(self): + for offer in self: + base_date = (offer.create_date or fields.Datetime.now()).date() + if offer.date_deadline: + offer.validity = (offer.date_deadline - base_date).days + + # --------------------- + # CRUD methods + # --------------------- + @api.model_create_multi + def create(self, vals_list): + """Overrided the default create method to enforce business rules when creating an offer. + + Logic implemented: + 1. Checks if the offer amount is lower than existing offer for the property. + - If so raises a UserError to prevent the creation of the offer. + 2. If the offer is valid, updates the related property's state to 'offer_received'. + + Args: + vals (dict): The values used to create the new estate.property.offer record. + + Returns: + recordset: the newly created estate.property.offer record. + + Raises: + UserError: If the offer amount is lower than an existing offer for the property. + """ + for vals in vals_list: + property_id = vals.get('property_id') + offer_price = vals.get('price', 0.0) + if not property_id or not offer_price: + raise UserError('Both Property and Price must be provided.') + + property_obj = self.env['estate.property'].browse(property_id) + for offer in property_obj.offer_ids: + if float_compare(offer_price, offer.price, precision_rounding=0.01) < 0: + raise UserError('You cannot create an offer with an amount lower than existing offer.') + + if property_obj.state == 'new': + property_obj.state = 'offer_received' + + return super().create(vals_list) + + # ------------------- + # Action methods + # ------------------- + def action_accept(self): + """Accept the offer and update the related property accordingly. + + - Sets the offer's status to 'accepted'. + - Sets all the offer's status to 'refused'. + - Updates the property's selling price and buyer. + - Updates the property's state to 'offer_accepted'. + + Raises: + UserError: If the property is already marked as 'sold'. + """ + for offer in self: + if offer.property_id.state == 'sold': + raise UserError('You cannot accept an offer for a sold property.') + + offer.status = 'accepted' + (offer.property_id.offer_ids - offer).write({'status': 'refused'}) + property = offer.property_id + property.write({ + 'selling_price': offer.price, + 'buyer_id': offer.partner_id, + 'state': 'offer_accepted' + }) + return True + + def action_refuse(self): + for offer in self: + offer.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..fb8de7ac06e --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,18 @@ +# imports of odoo +from odoo import fields, models + + +class EstatePropertyType(models.Model): + # === Private attributes === + _name = 'estate.property.tag' + _description = 'Property tags to represent the property.' + _order = 'name' + + # SQL Constraints + _sql_constraints = [ + ('unique_tag_name', 'UNIQUE(name)', 'Tag name must be unique.'), + ] + + # === Fields declaration === + name = fields.Char('Property Tag', required=True) + color = fields.Integer(string="Color") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..50dc002762a --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,38 @@ +# imports of odoo +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + # === Private attributes === + _name = 'estate.property.type' + _description = 'Property types for available in the business.' + _order = 'sequence, name' + + # SQL Constraints + _sql_constraints = [ + ('unique_type_name', 'UNIQUE(name)', 'Type name must be unique.') + ] + + # === Fields declaration === + name = fields.Char(required=True) + sequence = fields.Integer('Sequence', default=10) + + # One2many fields + property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties') + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers') + + # Compute fields + offer_count = fields.Integer(string='Number of Offers', compute='_compute_offer_count') + + # === Compute methods === + @api.depends('offer_ids') + def _compute_offer_count(self): + '''Compute the number of offers linked to each property type. + + This method calculates the total number of related offers (`offer_ids`) + for each type in the `estate.property.type` model and stores the count + in the `offer_count` field. + ''' + for record in self: + record.offer_count = len(record.offer_ids) if hasattr( + record, 'offer_ids') else 0 diff --git a/estate/models/inherited_user.py b/estate/models/inherited_user.py new file mode 100644 index 00000000000..33b3eaca2ac --- /dev/null +++ b/estate/models/inherited_user.py @@ -0,0 +1,10 @@ +# Imports of odoo +from odoo import fields, models + + +class InheritedUser(models.Model): + # === Private attributes === + _inherit = 'res.users' + + # === Fields declaration === + property_ids = fields.One2many('estate.property', 'salesman_id', string="Properties") 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_views.xml b/estate/views/estate_menus_views.xml index 17238219427..dc23c2d6e9d 100644 --- a/estate/views/estate_menus_views.xml +++ b/estate/views/estate_menus_views.xml @@ -1,7 +1,13 @@ - - - + + + + + + + + + - \ 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..5bc3068ab55 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,51 @@ + + + + + estate.property.offer.view.list + estate.property.offer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + estate.property.type.action + estate.property.type + list,form + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 2cf466acfaf..f870f3b85a1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,34 +1,79 @@ + + + estate.property.view.kanban + estate.property + + + + + +
+

+ +

+
+ Expected Price: +
+
+ Best Price: +
+
+ Selling Price: +
+
+ +
+
+
+
+
+
+
+ - + + estate.property.view.list estate.property - + + - + - - - - - + + + + + + + - - estate.property.form + + estate.property.view.form estate.property
+ +
+
+

@@ -38,17 +83,22 @@ + + + + + @@ -57,10 +107,25 @@ - - + + + + + + + + + + + + + + + + + @@ -68,38 +133,47 @@ - - estate.property.search + + estate.property.view.search estate.property + - - - - + + + + + + - - + + + + + + - - Estate Entry + + + estate.property.action estate.property - list,form + kanban,list,form + {'search_default_available': True} diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..fb30ea3f0d4 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,14 @@ + + + res.users.view.form.inherit.estate + 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..ae12d94eb6a --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,10 @@ +{ + 'name': 'Real Estate Account', + 'description': 'The module links the estate and accounting apps', + 'categoy': 'Real Estate', + 'depends': ['estate', 'account'], + 'application': True, + 'installable': True, + 'license': 'LGPL-3', + 'author': 'Aaryan Parpyani', +} 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..be5088aa99b --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,47 @@ +# Imports of odoo +from odoo import Command, models + + +class InheritedEstateProperty(models.Model): + # === Private attributes === + _inherit = 'estate.property' + + # === Action methods === + def action_mark_sold(self): + """When a property is marked as sold, + this method creates a customer invoice. + + The invoice contains two lines: + - Selling price of the property. + - 6% of the selling price as commission. + - A flat administrative fee of ₹100. + """ + res = super().action_mark_sold() + + for property in self: + if property.buyer_id: + invoice_vals = { + 'partner_id': super().buyer_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': [ + Command.create({ + 'name': 'Selling Price', + 'quantity': 1, + 'price_unit': property.selling_price, + }), + Command.create({ + 'name': 'Selling Price (6%)', + 'quantity': 1, + 'price_unit': property.selling_price * 0.06, + }), + Command.create({ + 'name': 'Administrative Fees', + 'quantity': 1, + 'price_unit': 100.00, + }) + ] + } + + self.env['account.move'].sudo().with_context( + default_move_type='out_invoice').create(invoice_vals) + return res From 270300f0592b0ba799535afb19e4d7cb318b4080 Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Wed, 30 Jul 2025 19:04:13 +0530 Subject: [PATCH 06/12] [ADD] awesome_owl: implement counter, card components and todo list in playground. - Created `Counter` component in playground. - Extracted `Counter` component from playground into Counter component. - Added ``in template of playground to add two counters. - Created a card component and used t-esc and t-out directives to demonstrate the difference between escaped and safe HTML content using the markup function. - Added props validation to the card component - Implemented parent-child communication using a callback prop (onChange) from the Counter component to the Playground component, updating and displaying the sum in local state whenever counters are incremented. - Built a TodoList with hardcoded todos using useState, rendered each using the TodoItem component with t-foreach, and added prop validation for todo. - Updated TodoItem to conditionally apply Bootstrap classes (text-muted, text-decoration-line-through) using t-att-class based on the todo's completion status. - Enabled dynamic todo creation: replaced hardcoded todos with useState([]), added input field for user to enter a task. - Implemented addTask method to add new todos on Enter key (with unique ID and empty check). --- awesome_owl/static/src/card/card.js | 13 +++++++++++ awesome_owl/static/src/card/card.xml | 15 +++++++++++++ awesome_owl/static/src/counter/counter.js | 25 ++++++++++++++++++++++ awesome_owl/static/src/counter/counter.xml | 11 ++++++++++ awesome_owl/static/src/playground.js | 16 +++++++++++++- awesome_owl/static/src/playground.xml | 24 +++++++++++++++++++-- awesome_owl/static/src/todo/todo_item.js | 15 +++++++++++++ awesome_owl/static/src/todo/todo_item.xml | 9 ++++++++ awesome_owl/static/src/todo/todo_list.js | 23 ++++++++++++++++++++ awesome_owl/static/src/todo/todo_list.xml | 12 +++++++++++ 10 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml create mode 100644 awesome_owl/static/src/todo/todo_item.js create mode 100644 awesome_owl/static/src/todo/todo_item.xml create mode 100644 awesome_owl/static/src/todo/todo_list.js create mode 100644 awesome_owl/static/src/todo/todo_list.xml diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..4cfe21858ff --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,13 @@ +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card" + static props = { + title: { + type: String, + }, + content: { + type: String, + } + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..70996280585 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,15 @@ + + + +
+
+
+ +
+

+ +

+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..6e3503fbaaf --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,25 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.Counter"; + static props = { + title: { + type: String, + }, + onChange: { + type: Function, + optional: true, + } + } + + setup() { + this.state = useState({ value: 1 }); + } + + increment() { + this.state.value++; + if (this.props.onChange) { + this.props.onChange(); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..103aeaa8425 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,11 @@ + + + +
+

+ : +

+ +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..52dba7163e0 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,21 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState, markup } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList } + html = ('

hello world

'); + markup_html = markup(this.html); + + setup() { + this.state = useState({ sum: 2 }); + } + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..8e2f89b33b3 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,8 +2,28 @@ -
- hello world +
+
+
+ + +
+

Sum is:

+
+ + +
+

Cards

+
+ + +
+
+
+ + +
+
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..17fff87b852 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,15 @@ +import { Component, useState } 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, + } + }, + } +} diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml new file mode 100644 index 00000000000..816d58dc9a9 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,9 @@ + + + +
+ . + +
+
+
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..275a3afa171 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,23 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList" + static components = { TodoItem } + id = 1; + + setup () { + this.todos = useState([]); + } + + addTask(event) { + const { target: { value }, keyCode } = event; + if (keyCode != 13 || !value.trim()) return; + this.todos.push({ + id: this.id++, + description: value, + isCompleted: false, + }); + event.target.value = ''; + } +} diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml new file mode 100644 index 00000000000..5134344efc5 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,12 @@ + + + +
+

Todo List

+ + + + +
+
+
From 9cb4c74b5118a9abd83e1aa5c21975ba8c34a4f8 Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Thu, 31 Jul 2025 19:41:39 +0530 Subject: [PATCH 07/12] [IMP] awesome_owl: implement add & delete task functionality in todo list and improve UI for task view. - Focus input field on mount in TodoList component using useAutoFocus() method. - Add checkbox to TodoItem with toggleState callback to update isCompleted status on change - Add removeTodo callback to TodoItem and trigger it via click on remove icon, to delete the task. --- awesome_owl/static/src/todo/todo_item.js | 15 +++++++++++++++ awesome_owl/static/src/todo/todo_item.xml | 10 +++++++--- awesome_owl/static/src/todo/todo_list.js | 15 +++++++++++++++ awesome_owl/static/src/todo/todo_list.xml | 4 ++-- awesome_owl/static/src/utils.js | 8 ++++++++ 5 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js index 17fff87b852..b243f8b9fcc 100644 --- a/awesome_owl/static/src/todo/todo_item.js +++ b/awesome_owl/static/src/todo/todo_item.js @@ -11,5 +11,20 @@ export class TodoItem extends Component { isCompleted: Boolean, } }, + toggleTaskState: { + type: Function, + }, + taskDelete: { + type: Function, + } + + } + + toggleTaskState() { + this.props.toggleTaskState(this.props.todo.id); + } + + taskDelete() { + this.props.taskDelete(this.props.todo.id); } } diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml index 816d58dc9a9..9158219d848 100644 --- a/awesome_owl/static/src/todo/todo_item.xml +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -1,9 +1,13 @@ -
- . - +
+ +
+ . + +
+
diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js index 275a3afa171..17258c9034a 100644 --- a/awesome_owl/static/src/todo/todo_list.js +++ b/awesome_owl/static/src/todo/todo_list.js @@ -1,5 +1,6 @@ import { Component, useState } from "@odoo/owl"; import { TodoItem } from "./todo_item"; +import { useAutoFocus } from "../utils" export class TodoList extends Component { static template = "awesome_owl.TodoList" @@ -8,6 +9,7 @@ export class TodoList extends Component { setup () { this.todos = useState([]); + useAutoFocus(); } addTask(event) { @@ -20,4 +22,17 @@ export class TodoList extends Component { }); event.target.value = ''; } + + toggleTaskState(todo_id) { + const task = this.todos.find((t) => t.id === todo_id); + if (!task) return; + task.isCompleted = !task.isCompleted; + } + + taskDelete(todo_id) { + const taskIndex = this.todos.findIndex((t) => t.id === todo_id) + if (taskIndex >= 0) { + this.todos.splice(taskIndex, 1); + } + } } diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml index 5134344efc5..9e3e9479076 100644 --- a/awesome_owl/static/src/todo/todo_list.xml +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -3,9 +3,9 @@

Todo List

- + - +
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..76d03d95cda --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,8 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export const useAutoFocus = () => { + const inputRef = useRef("focus-input"); + onMounted(() => { + inputRef.el?.focus(); + }) +} \ No newline at end of file From 4a0880f5decca025f8a9180b781f8b69246bfe27 Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Mon, 4 Aug 2025 11:00:36 +0530 Subject: [PATCH 08/12] [IMP] estate, awesome_owl: mark property as sold only if accepted offers exist, remove unnecessary comments and fix linting. - Added logic to check if any offers are present before marking the property as sold. - Added logic to check if one of the offers is accepted before marking property as sold - Removed unnecessary comments from files. - Fixed linting issues from `utils.js` --- awesome_owl/static/src/utils.js | 2 +- estate/models/estate_property.py | 21 +++++++-------------- estate/models/estate_property_offer.py | 10 ---------- estate/models/estate_property_tag.py | 4 +--- estate/models/estate_property_type.py | 6 ------ 5 files changed, 9 insertions(+), 34 deletions(-) diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js index 76d03d95cda..c515d587036 100644 --- a/awesome_owl/static/src/utils.js +++ b/awesome_owl/static/src/utils.js @@ -5,4 +5,4 @@ export const useAutoFocus = () => { onMounted(() => { inputRef.el?.focus(); }) -} \ No newline at end of file +} diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 53f2f5fd8d3..91a18f3e0a0 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,7 +8,6 @@ class EstateProperty(models.Model): - # === Private attributes === _name = 'estate.property' _description = 'Estate Test' _order = 'id desc' @@ -19,9 +18,6 @@ class EstateProperty(models.Model): ('check_selling_price_positive', 'CHECK(selling_price > 0)', 'Selling price must be strictly positive.'), ] - # --------------------- - # Fields declaration - # --------------------- name = fields.Char(string='Name', required=True) description = fields.Char() postcode = fields.Char() @@ -35,8 +31,6 @@ class EstateProperty(models.Model): garden = fields.Boolean() garden_area = fields.Integer() active = fields.Boolean(default=True) - - # Selection fields garden_orientation = fields.Selection( [ ('north', 'North'), @@ -59,19 +53,11 @@ class EstateProperty(models.Model): copy=False, default='new' ) - - # Many2one fields property_type_id = fields.Many2one('estate.property.type', string='Property Type') salesman_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) - - # Many2many fields tag_ids = fields.Many2many('estate.property.tag', string='Tags') - - # One2many fields offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') - - # Computed fields total_area = fields.Integer(string='Total Area', compute='_compute_total_area') best_price = fields.Float(string='Best Offer', compute='_compute_best_price') @@ -129,8 +115,15 @@ def _unlink_except_new_or_cancelled(self): # ---------------------- def action_mark_sold(self): for record in self: + if not record.offer_ids: + raise UserError("You can mark this property as sold because there are no offers.") if record.state == 'cancelled': raise UserError('Cancelled properties cannot be marked as sold.') + + accepted_offer = record.offer_ids.filtered(lambda o: o.status == 'accepted') + if not accepted_offer: + raise UserError("You must accept an offer before marking the property as sold.") + record.state = 'sold' return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 360a8c02511..a0c637c584b 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -8,7 +8,6 @@ class EstatePropertyOffer(models.Model): - # === Private attributes === _name = 'estate.property.offer' _description = 'Offers for all the property listings.' _order = 'price desc' @@ -18,13 +17,8 @@ class EstatePropertyOffer(models.Model): ('check_price_positive', 'CHECK(price > 0)', 'Offer price must be strictly positive.'), ] - # ------------------------ - # Fields declaration - # ------------------------- price = fields.Float(string='Price') validity = fields.Integer(default=7) - - # Selection fields status = fields.Selection( [ ('accepted', 'Accepted'), @@ -33,11 +27,7 @@ class EstatePropertyOffer(models.Model): string='Status', copy=False ) - - # Computed fields date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline', store=True) - - # Many2one fields partner_id = fields.Many2one('res.partner', required=True) property_id = fields.Many2one('estate.property', string='Property Name', required=True) property_type_id = fields.Many2one(related='property_id.property_type_id', store=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index fb8de7ac06e..9f6520159b7 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -2,8 +2,7 @@ from odoo import fields, models -class EstatePropertyType(models.Model): - # === Private attributes === +class EstatePropertyTag(models.Model): _name = 'estate.property.tag' _description = 'Property tags to represent the property.' _order = 'name' @@ -13,6 +12,5 @@ class EstatePropertyType(models.Model): ('unique_tag_name', 'UNIQUE(name)', 'Tag name must be unique.'), ] - # === Fields declaration === name = fields.Char('Property Tag', required=True) color = fields.Integer(string="Color") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 50dc002762a..7ccda7c88f9 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -3,7 +3,6 @@ class EstatePropertyType(models.Model): - # === Private attributes === _name = 'estate.property.type' _description = 'Property types for available in the business.' _order = 'sequence, name' @@ -13,15 +12,10 @@ class EstatePropertyType(models.Model): ('unique_type_name', 'UNIQUE(name)', 'Type name must be unique.') ] - # === Fields declaration === name = fields.Char(required=True) sequence = fields.Integer('Sequence', default=10) - - # One2many fields property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties') offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers') - - # Compute fields offer_count = fields.Integer(string='Number of Offers', compute='_compute_offer_count') # === Compute methods === From 22e9d197437b41970930894c5654be2bfaf99f50 Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Mon, 4 Aug 2025 15:04:36 +0530 Subject: [PATCH 09/12] [IMP] awesome_owl, awesome_dashboard: implement cards with slots and minimize card content using toggle button. create dashboard using layout component. - Transform the Card component to use a default slot instead of a content prop, include multiple cards with arbitrary content (e.g., Counter), and enforce prop validation for the title. - Add toggle functionality to the Card component using a boolean state, conditionally render its content with t-if, and include a header button to switch between open and closed states. - Imported layout component in dashboard and added dashboard.scss to create a basic dasboard view. --- awesome_dashboard/static/src/dashboard.js | 2 ++ awesome_dashboard/static/src/dashboard.scss | 3 +++ awesome_dashboard/static/src/dashboard.xml | 2 +- awesome_owl/static/src/card/card.js | 16 ++++++++++++---- awesome_owl/static/src/card/card.xml | 15 +++++++++------ awesome_owl/static/src/playground.xml | 13 +++++++++++-- 6 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard.scss diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index 637fa4bb972..37c5f0d6348 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -2,9 +2,11 @@ import { Component } from "@odoo/owl"; import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout }; } 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..44de08a2d91 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: white; +} diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 1a2ac9a2fed..6f5999f9ef7 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -2,7 +2,7 @@ - hello dashboard + diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index 4cfe21858ff..fb210e97744 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -1,4 +1,4 @@ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; export class Card extends Component { static template = "awesome_owl.Card" @@ -6,8 +6,16 @@ export class Card extends Component { title: { type: String, }, - content: { - type: String, - } + slots: { + type: Object, + }, + } + + setup() { + this.state = useState({ isCardOpen: false }) + } + + toggleCard() { + this.state.isCardOpen = !this.state.isCardOpen; } } diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index 70996280585..e2627250a49 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -3,12 +3,15 @@
-
- -
-

- -

+
+
+ +
+ +
+ + +
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 8e2f89b33b3..e21aa15acf1 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -15,8 +15,17 @@

Cards

- - + +

Using Slot for card 1

+
+ +

+ +

+
+ + +
From f31960eb27df2e7943326e9674aaa34356ad53ff Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Tue, 5 Aug 2025 18:54:33 +0530 Subject: [PATCH 10/12] [IMP] awesome_dashboard: implement dashboard layout, memoized RPC, pie chart - Added Customers button to open a kanban view with all the customers. - Added Leads button to open dynamic action on `crm.lead` model with list and form view. - Created a reusable DashboardItem card component with a configurable width based on its size prop, fetch statistics via RPC from `/awesome_dashboard/statistics`, and display multiple metric cards showing key monthly order stats. - Implement and register an `awesome_dashboard.statistics` service that memoizes RPC calls to `/awesome_dashboard/statistics` for consistent cached results, then integrate it into the Dashboard component to load and display the statistics. - Build a PieChart component that loads Chart.js in `onWillStart`, renders a canvas, and draws a pie chart of t-shirt sales per size from `/awesome_dashboard/statistics`, displaying it inside a DashboardItem. --- awesome_dashboard/static/src/dashboard.js | 95 ++++++++++++++++++- awesome_dashboard/static/src/dashboard.xml | 24 ++++- .../src/dashboard_item/dashboard_item.js | 21 ++++ .../src/dashboard_item/dashboard_item.xml | 13 +++ .../static/src/pie_chart/pie_chart.js | 50 ++++++++++ .../static/src/pie_chart/pie_chart.xml | 6 ++ .../static/src/statistics_service.js | 20 ++++ 7 files changed, 226 insertions(+), 3 deletions(-) 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/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 37c5f0d6348..d9f557c3d3d 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,12 +1,103 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; 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 { Component, onWillStart, useState } from "@odoo/owl"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout }; + static components = { Layout, DashboardItem, PieChart }; + + setup() { + this.action = useService("action"); + this.statistics = useService("awesome_dashboard.statistics"); + this.cards = useState([ + { + id: 1, + size: 0, + title: "Number of new orders this month", + value: 0, + }, + { + id: 2, + size: 0, + title: "Total amount of new order this month", + value: 0, + }, + { + id: 3, + size: 0, + title: "Average amount of t-shirt by order this month", + value: 0, + }, + { + id: 4, + size: 0, + title: "Number of cancelled orders this month", + value: 0, + }, + { + id: 5, + size: 0, + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: 0, + } + ]); + + onWillStart(async () => { + this.result = await this.statistics.loadStatistics(); + this.cards = [ + { + id: 1, + size: 2, + title: "Number of new orders this month", + value: this.result.nb_new_orders, + }, + { + id: 2, + size: 2, + title: "Total amount of new order this month", + value: this.result.total_amount, + }, + { + id: 3, + size: 2, + title: "Average amount of t-shirt by order this month", + value: this.result.average_quantity, + }, + { + id: 4, + size: 2, + title: "Number of cancelled orders this month", + value: this.result.nb_cancelled_orders, + }, + { + id: 5, + size: 2, + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: this.result.average_time, + } + ] + }); + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: 'ir.actions.act_window', + name: _t('Leads'), + target: 'current', + res_model: 'crm.lead', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], // [view_id, view_type] + }); + } } registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 6f5999f9ef7..ef3b571f218 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -2,7 +2,29 @@ - + + + + + + +
+
+ + +

+ + + + + +

+ +

T-Shirt orders by size

+ +
+
+
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..fa5819a5111 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js @@ -0,0 +1,21 @@ +/** @odoo-module **/ + +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, + } + } + + setup() { + const size = this.props.size || 1; + this.width = 18*size; + } +} 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..48930548c2d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml @@ -0,0 +1,13 @@ + + + +
+
+ +

+ +

+
+
+
+
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..02ed5780197 --- /dev/null +++ b/awesome_dashboard/static/src/pie_chart/pie_chart.js @@ -0,0 +1,50 @@ +/** @odoo-module **/ + +import { Component, onWillStart, useRef, useEffect } from "@odoo/owl"; +import { loadJS } from '@web/core/assets'; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + + static props = { + tshirtSales: { + type: Object, + } + } + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js") + }); + + useEffect(() => { + this.createChart(); + return () => this.chart?.destroy(); + }, () => []); + } + + getChartConfig() { + if (!this.props.tshirtSales) return {}; + return { + type: 'pie', + data: { + labels: Object.keys(this.props.tshirtSales), + datasets: [{ + data: Object.values(this.props.tshirtSales), + }] + }, + options: { + aspectRatio: 2, + } + } + } + + createChart() { + if (this.chart) { + this.chart.destroy(); + } + const config = this.getChartConfig(); + this.chart = new Chart(this.canvasRef.el, config); + } +} diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..13902aa9695 --- /dev/null +++ b/awesome_dashboard/static/src/pie_chart/pie_chart.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js new file mode 100644 index 00000000000..1b785cb0681 --- /dev/null +++ b/awesome_dashboard/static/src/statistics_service.js @@ -0,0 +1,20 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { memoize } from "@web/core/utils/functions"; + +const loadStatistics = async() => { + const result = await rpc("/awesome_dashboard/statistics"); + return result; +} + +export const loadStatService = { + start () { + return { + loadStatistics: memoize(loadStatistics) + } + } +} + +registry.category("services").add("awesome_dashboard.statistics", loadStatService); From 33668c7ce17e9f6ac6d229595c0d726a153872a7 Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Wed, 6 Aug 2025 19:10:53 +0530 Subject: [PATCH 11/12] [IMP] awesome_dashboard: Implement real life update, lazy loading and generic dashboard. - Convert statistics into a shared reactive object in the service, reload it every 10 minutes, and let any displayed dashboard update instantly via useState. - Bundle all dashboard files together and load them on demand using LazyComponent so the code only loads when the user actually views the dashboard. - Replace hard-coded dashboard content with a configurable items list, using dynamic components and props to support different types of cards like NumberCard and PieChartCard. --- awesome_dashboard/__manifest__.py | 3 + awesome_dashboard/static/src/dashboard.js | 103 ------------------ .../src/dashboard/NumberCard/number_card.js | 14 +++ .../src/dashboard/NumberCard/number_card.xml | 11 ++ .../PieChartCard}/pie_chart.js | 15 ++- .../PieChartCard}/pie_chart.xml | 0 .../static/src/dashboard/dashboard.js | 84 ++++++++++++++ .../static/src/{ => dashboard}/dashboard.scss | 0 .../static/src/{ => dashboard}/dashboard.xml | 17 ++- .../static/src/dashboard/dashboard_items.js | 66 +++++++++++ .../src/dashboard/statistics_service.js | 42 +++++++ .../static/src/dashboard_action.js | 13 +++ .../src/dashboard_item/dashboard_item.js | 2 +- .../static/src/statistics_service.js | 20 ---- 14 files changed, 255 insertions(+), 135 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/NumberCard/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/NumberCard/number_card.xml rename awesome_dashboard/static/src/{pie_chart => dashboard/PieChartCard}/pie_chart.js (78%) rename awesome_dashboard/static/src/{pie_chart => dashboard/PieChartCard}/pie_chart.xml (100%) 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 (62%) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/statistics_service.js create mode 100644 awesome_dashboard/static/src/dashboard_action.js delete mode 100644 awesome_dashboard/static/src/statistics_service.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..f46474bc9fe 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -25,6 +25,9 @@ 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', ], + 'awesome_dashboard.dashboard:': [ + 'awesome_dashboard/static/src/dashboard/**/*' + ], }, 'license': 'AGPL-3' } diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index d9f557c3d3d..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,103 +0,0 @@ -/** @odoo-module **/ - -import { _t } from "@web/core/l10n/translation"; -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 { Component, onWillStart, useState } from "@odoo/owl"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout, DashboardItem, PieChart }; - - setup() { - this.action = useService("action"); - this.statistics = useService("awesome_dashboard.statistics"); - this.cards = useState([ - { - id: 1, - size: 0, - title: "Number of new orders this month", - value: 0, - }, - { - id: 2, - size: 0, - title: "Total amount of new order this month", - value: 0, - }, - { - id: 3, - size: 0, - title: "Average amount of t-shirt by order this month", - value: 0, - }, - { - id: 4, - size: 0, - title: "Number of cancelled orders this month", - value: 0, - }, - { - id: 5, - size: 0, - title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", - value: 0, - } - ]); - - onWillStart(async () => { - this.result = await this.statistics.loadStatistics(); - this.cards = [ - { - id: 1, - size: 2, - title: "Number of new orders this month", - value: this.result.nb_new_orders, - }, - { - id: 2, - size: 2, - title: "Total amount of new order this month", - value: this.result.total_amount, - }, - { - id: 3, - size: 2, - title: "Average amount of t-shirt by order this month", - value: this.result.average_quantity, - }, - { - id: 4, - size: 2, - title: "Number of cancelled orders this month", - value: this.result.nb_cancelled_orders, - }, - { - id: 5, - size: 2, - title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", - value: this.result.average_time, - } - ] - }); - } - - openCustomers() { - this.action.doAction("base.action_partner_form"); - } - - openLeads() { - this.action.doAction({ - type: 'ir.actions.act_window', - name: _t('Leads'), - target: 'current', - res_model: 'crm.lead', - views: [[false, 'kanban'], [false, 'list'], [false, 'form']], // [view_id, view_type] - }); - } -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/NumberCard/number_card.js b/awesome_dashboard/static/src/dashboard/NumberCard/number_card.js new file mode 100644 index 00000000000..fa8f221be2d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/NumberCard/number_card.js @@ -0,0 +1,14 @@ +import { Component } from '@odoo/owl' + +export class NumberCard extends Component { + static template = "awesome_dashboard.numberCard" + static components = {} + static props = { + title: { + type: String + }, + value: { + type: Number | String + } + } +} diff --git a/awesome_dashboard/static/src/dashboard/NumberCard/number_card.xml b/awesome_dashboard/static/src/dashboard/NumberCard/number_card.xml new file mode 100644 index 00000000000..7de29aff7f9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/NumberCard/number_card.xml @@ -0,0 +1,11 @@ + + + +

+ +

+
+ +
+
+
diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/PieChartCard/pie_chart.js similarity index 78% rename from awesome_dashboard/static/src/pie_chart/pie_chart.js rename to awesome_dashboard/static/src/dashboard/PieChartCard/pie_chart.js index 02ed5780197..97608a93fb4 100644 --- a/awesome_dashboard/static/src/pie_chart/pie_chart.js +++ b/awesome_dashboard/static/src/dashboard/PieChartCard/pie_chart.js @@ -7,8 +7,11 @@ export class PieChart extends Component { static template = "awesome_dashboard.PieChart"; static props = { - tshirtSales: { - type: Object, + title: { + type: String, + }, + value: { + type: String | Number, } } @@ -21,17 +24,17 @@ export class PieChart extends Component { useEffect(() => { this.createChart(); return () => this.chart?.destroy(); - }, () => []); + }); } getChartConfig() { - if (!this.props.tshirtSales) return {}; + if (!this.props.value) return {}; return { type: 'pie', data: { - labels: Object.keys(this.props.tshirtSales), + labels: Object.keys(this.props.value), datasets: [{ - data: Object.values(this.props.tshirtSales), + data: Object.values(this.props.value), }] }, options: { diff --git a/awesome_dashboard/static/src/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/PieChartCard/pie_chart.xml similarity index 100% rename from awesome_dashboard/static/src/pie_chart/pie_chart.xml rename to awesome_dashboard/static/src/dashboard/PieChartCard/pie_chart.xml diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..6f23be1ef46 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,84 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +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 "./PieChartCard/pie_chart"; +import { dashboardCards } from "./dashboard_items"; +import { Component, useState } from "@odoo/owl"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, PieChart }; + + setup() { + this.action = useService("action"); + this.statistics = useService("awesome_dashboard.statistics"); + this.result = useState(this.statistics.stats); + this.items = useState(dashboardCards); + } + + get cards() { + return [ + { + id: "nb_new_orders", + description: "New t-shirt orders this month.", + size: 2, + props: (data) => ({ + title: "Number of new orders this month", + value: this.result.nb_new_orders, + }), + }, + { + id: "total_amount", + description: "New orders this month.", + size: 2, + title: "Total amount of new order this month", + value: this.result.total_amount, + }, + { + id: "average_quantity", + description: "Average amount of t-shirt.", + size: 2, + title: "Average amount of t-shirt by order this month", + value: this.result.average_quantity, + }, + { + id: "nb_cancelled_orders", + description: "Cancelled orders this month.", + size: 2, + title: "Number of cancelled orders this month", + value: this.result.nb_cancelled_orders, + }, + { + id: "average_time", + description: "Average time for an order to reach conclusion (sent or cancelled).", + size: 2, + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: this.result.average_time, + } + ] + } + + get chart() { + return this.result.orders_by_size; + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: 'ir.actions.act_window', + name: _t('Leads'), + target: 'current', + res_model: 'crm.lead', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], // [view_id, view_type] + }); + } +} + +registry.category("lazy_components").add("awesome_dashboard.LazyComponent", 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 62% rename from awesome_dashboard/static/src/dashboard.xml rename to awesome_dashboard/static/src/dashboard/dashboard.xml index ef3b571f218..2539d742830 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -10,19 +10,26 @@
- + + + + + + + +
- +
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..4724c23fa91 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,66 @@ + +import { NumberCard } from "./NumberCard/number_card"; +import { PieChart } from "./PieChartCard/pie_chart"; + +export const dashboardCards = [ + { + id: "nb_new_orders", + description: "New t-shirt orders this month.", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders, + }), + }, + { + id: "total_amount", + description: "New orders this month.", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Total amount of new order this month", + value: data.total_amount, + }), + }, + { + id: "average_quantity", + description: "Average amount of t-shirt.", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity, + }), + }, + { + id: "nb_cancelled_orders", + description: "Cancelled orders this month.", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders, + }), + }, + { + id: "average_time", + description: "Average time for an order to reach conclusion (sent or cancelled).", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time, + }), + }, + { + id: "orders_by_size", + description: "T-shirt orders grouped by their size.", + Component: PieChart, + size: 2, + props: (data) => ({ + title: "T-shirt order by size", + value: data.orders_by_size, + }), + } +] 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..a88a7b66171 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,42 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +let statisticsObj = reactive({ + nb_new_orders: 0, + total_amount: 0, + average_quantity: 0, + nb_cancelled_orders: 0, + average_time: 0, + orders_by_size: {} +}, () => { + console.log("Observed"); +}); + +const loadStatistics = async() => { + const result = await rpc("/awesome_dashboard/statistics"); + statisticsObj.nb_new_orders = result.nb_new_orders; + statisticsObj.total_amount = result.total_amount; + statisticsObj.average_quantity = result.average_quantity; + statisticsObj.nb_cancelled_orders = result.nb_cancelled_orders; + statisticsObj.average_time = result.average_time; + statisticsObj.orders_by_size = result.orders_by_size; +} + +export const loadStatService = { + dependencies: [], + start () { + loadStatistics(); + setInterval(loadStatistics, 60 * 1000 * 10); + + return { + get stats() { + return statisticsObj; + } + } + } +} + +registry.category("services").add("awesome_dashboard.statistics", loadStatService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..4c75637c3d1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,13 @@ +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +export class LazyDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", LazyDashboardLoader); + diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js index fa5819a5111..419e15853b7 100644 --- a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js @@ -9,7 +9,7 @@ export class DashboardItem extends Component { type: Number, optional: true, }, - slots: { + slots: { type: Object, } } diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js deleted file mode 100644 index 1b785cb0681..00000000000 --- a/awesome_dashboard/static/src/statistics_service.js +++ /dev/null @@ -1,20 +0,0 @@ -/** @odoo-module **/ - -import { registry } from "@web/core/registry"; -import { rpc } from "@web/core/network/rpc"; -import { memoize } from "@web/core/utils/functions"; - -const loadStatistics = async() => { - const result = await rpc("/awesome_dashboard/statistics"); - return result; -} - -export const loadStatService = { - start () { - return { - loadStatistics: memoize(loadStatistics) - } - } -} - -registry.category("services").add("awesome_dashboard.statistics", loadStatService); From 05c1a02945423b71eed64adc0460888be0b1df4c Mon Sep 17 00:00:00 2001 From: "Aaryan (aarp)" Date: Thu, 7 Aug 2025 19:06:15 +0530 Subject: [PATCH 12/12] [IMP] awesome_dashboard: make dashboard extensible and implement persistent dashboard. - Registered all the dashboard items in awesome_dashboard registry. - Used the newly registered dashboard item registry to import all dashboard items. - Adds a gear icon in the control panel to open a settings dialog. - The dialog lists dashboard items with checkboxes and an Apply button. - Unchecked items are stored in local storage and filtered out from the Dashboard view. - Removed trailing whitespaces and fixed linting issues. --- .../src/dashboard/PieChartCard/pie_chart.js | 2 +- .../static/src/dashboard/dashboard.js | 87 +++++++++---------- .../static/src/dashboard/dashboard.xml | 28 +++--- .../static/src/dashboard/dashboard_items.js | 3 + .../src/dashboard/statistics_service.js | 6 +- .../static/src/dashboard_action.js | 1 - .../src/dashboard_dialog/dashboard_dialog.js | 39 +++++++++ .../src/dashboard_dialog/dashboard_dialog.xml | 30 +++++++ estate/__manifest__.py | 2 +- estate/models/estate_property.py | 2 +- 10 files changed, 129 insertions(+), 71 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard_dialog/dashboard_dialog.js create mode 100644 awesome_dashboard/static/src/dashboard_dialog/dashboard_dialog.xml diff --git a/awesome_dashboard/static/src/dashboard/PieChartCard/pie_chart.js b/awesome_dashboard/static/src/dashboard/PieChartCard/pie_chart.js index 97608a93fb4..a61a0fd2c29 100644 --- a/awesome_dashboard/static/src/dashboard/PieChartCard/pie_chart.js +++ b/awesome_dashboard/static/src/dashboard/PieChartCard/pie_chart.js @@ -5,7 +5,7 @@ import { loadJS } from '@web/core/assets'; export class PieChart extends Component { static template = "awesome_dashboard.PieChart"; - + static props = { title: { type: String, diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index 6f23be1ef46..cf0a16dad0e 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -6,70 +6,65 @@ import { Layout } from "@web/search/layout"; import { useService } from "@web/core/utils/hooks"; import { DashboardItem } from "../dashboard_item/dashboard_item"; import { PieChart } from "./PieChartCard/pie_chart"; -import { dashboardCards } from "./dashboard_items"; import { Component, useState } from "@odoo/owl"; +import { DashboardDialog } from "../dashboard_dialog/dashboard_dialog"; +import { browser } from "@web/core/browser/browser"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout, DashboardItem, PieChart }; + static components = { Layout, DashboardItem, PieChart, DashboardDialog }; setup() { this.action = useService("action"); + this.dialog = useService("dialog"); this.statistics = useService("awesome_dashboard.statistics"); this.result = useState(this.statistics.stats); - this.items = useState(dashboardCards); + this.state = useState({ metricConfigs: {} }); + this.items = registry.category("awesome_dashboard_cards").get("awesome_dashboard.Cards"); + this.getBrowserCookie(); } - get cards() { - return [ - { - id: "nb_new_orders", - description: "New t-shirt orders this month.", - size: 2, - props: (data) => ({ - title: "Number of new orders this month", - value: this.result.nb_new_orders, - }), - }, - { - id: "total_amount", - description: "New orders this month.", - size: 2, - title: "Total amount of new order this month", - value: this.result.total_amount, - }, - { - id: "average_quantity", - description: "Average amount of t-shirt.", - size: 2, - title: "Average amount of t-shirt by order this month", - value: this.result.average_quantity, - }, - { - id: "nb_cancelled_orders", - description: "Cancelled orders this month.", - size: 2, - title: "Number of cancelled orders this month", - value: this.result.nb_cancelled_orders, - }, - { - id: "average_time", - description: "Average time for an order to reach conclusion (sent or cancelled).", - size: 2, - title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", - value: this.result.average_time, - } - ] + openDialog() { + this.dialog.add(DashboardDialog, { + metrics: this.items, + metricConfigs: this.state.metricConfigs, + closeDialog: this.closeDialog.bind(this), + updateMetricConfigCallback: this.updateMetricConfig.bind(this) + }); + } + + closeDialog() { + this.getBrowserCookie(); + } + + updateMetricConfig(updated_metricConfig) { + this.state.metricConfigs = updated_metricConfig; + this.setBrowserCookie(); } - get chart() { - return this.result.orders_by_size; + setBrowserCookie() { + browser.localStorage.setItem( + "awesome_dashboard.metric_configs", JSON.stringify(this.state.metricConfigs) + ); + } + + getBrowserCookie() { + const metric_cookie_data = browser.localStorage.getItem("awesome_dashboard.metric_configs"); + if (metric_cookie_data) { + this.state.metricConfigs = JSON.parse(metric_cookie_data); + } else { + const initialMetricState = {}; + for (const metric of this.items) { + initialMetricState[metric.id] = true; + } + this.state.metricConfigs = initialMetricState; + } } openCustomers() { this.action.doAction("base.action_partner_form"); } - + openLeads() { this.action.doAction({ type: 'ir.actions.act_window', diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml index 2539d742830..c2074dcf23e 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -7,29 +7,23 @@ + + +
- - - - - + + + + + + -
-
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js index 4724c23fa91..5e33d3af1b3 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_items.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -1,6 +1,7 @@ import { NumberCard } from "./NumberCard/number_card"; import { PieChart } from "./PieChartCard/pie_chart"; +import { registry } from "@web/core/registry"; export const dashboardCards = [ { @@ -64,3 +65,5 @@ export const dashboardCards = [ }), } ] + +registry.category("awesome_dashboard_cards").add("awesome_dashboard.Cards", dashboardCards); diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js index a88a7b66171..0c1f2be4820 100644 --- a/awesome_dashboard/static/src/dashboard/statistics_service.js +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -4,15 +4,13 @@ import { registry } from "@web/core/registry"; import { rpc } from "@web/core/network/rpc"; import { reactive } from "@odoo/owl"; -let statisticsObj = reactive({ +const statisticsObj = reactive({ nb_new_orders: 0, total_amount: 0, average_quantity: 0, nb_cancelled_orders: 0, average_time: 0, orders_by_size: {} -}, () => { - console.log("Observed"); }); const loadStatistics = async() => { @@ -34,7 +32,7 @@ export const loadStatService = { return { get stats() { return statisticsObj; - } + }, } } } diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js index 4c75637c3d1..5e2048dee3d 100644 --- a/awesome_dashboard/static/src/dashboard_action.js +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -10,4 +10,3 @@ export class LazyDashboardLoader extends Component { } registry.category("actions").add("awesome_dashboard.dashboard", LazyDashboardLoader); - diff --git a/awesome_dashboard/static/src/dashboard_dialog/dashboard_dialog.js b/awesome_dashboard/static/src/dashboard_dialog/dashboard_dialog.js new file mode 100644 index 00000000000..0a763ca8c30 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_dialog/dashboard_dialog.js @@ -0,0 +1,39 @@ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; + +export class DashboardDialog extends Component { + static template = "awesome_dashboard.DashboardDialog"; + static components = { Dialog, CheckBox } + static props = { + close: Function, + updateMetricConfigCallback: Function, + closeDialog: Function, + metrics: { + type: Object, + }, + metricConfigs: { + type: Object, + } + } + + setup() { + this.state = useState({ + metricConfigs: this.props.metricConfigs + }) + } + + applyChanges() { + this.props.updateMetricConfigCallback(this.state.metricConfigs); + this.props.close(); + } + + toggleMetricConfig(itemID, checked) { + this.state.metricConfigs[itemID] = checked; + } + + closeDialog() { + this.props.closeDialog(); + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard_dialog/dashboard_dialog.xml b/awesome_dashboard/static/src/dashboard_dialog/dashboard_dialog.xml new file mode 100644 index 00000000000..0de8b6825b9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_dialog/dashboard_dialog.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/estate/__manifest__.py b/estate/__manifest__.py index b194a41d05c..92ed93f5ab8 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,12 +10,12 @@ 'installable': True, 'license': 'LGPL-3', 'data': [ + 'security/ir.model.access.csv', 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tags_views.xml', 'views/res_users_views.xml', 'views/estate_property_views.xml', 'views/estate_menus_views.xml', - 'security/ir.model.access.csv', ] } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 91a18f3e0a0..1d427da4eab 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -105,7 +105,7 @@ def _onchange_garden(self): # CRUD methods # ----------------------- @api.ondelete(at_uninstall=False) - def _unlink_except_new_or_cancelled(self): + def _unlink_if_new_or_cancelled(self): for record in self: if record.state not in ['new', 'cancelled']: raise UserError('You can only delete properties that are in New or Cancelled state.')