From effef7c30b656dac4250de57d850abe0f057f233 Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Wed, 2 Jul 2025 17:42:52 +0530 Subject: [PATCH 01/15] [ADD] estate: created real estate module created the init and manifest files and created the models for schema definition, security for the assigning access rights, created the views folder, and made the three-level menuitems along with the list and form view. --- estate/__init__.py | 0 estate/__manifest__.py | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..1a520bb1891 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,6 @@ +{ + 'name' : 'Estate', + 'installable': True, + 'application': True, + 'auto_install': False +} \ No newline at end of file From 9d2b8782f7f4807016b39bcc10062287e3a5e677 Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Fri, 4 Jul 2025 10:08:58 +0530 Subject: [PATCH 02/15] [IMP] estate: add access control and enhance UI views Added access rights, groups, and record rules to secure estate records. Enhanced form, tree views for better usability and layout. These changes ensure proper data protection and improve user experience. --- estate/__init__.py | 1 + estate/__manifest__.py | 12 +++- estate/models/__init__.py | 1 + estate/models/estate_property.py | 33 +++++++++ estate/security/ir.model.access.csv | 2 + estate/views/estate_menu.xml | 9 +++ estate/views/estate_property_views.xml | 99 ++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_menu.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__init__.py b/estate/__init__.py index e69de29bb2d..9a7e03eded3 100644 --- a/estate/__init__.py +++ 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 index 1a520bb1891..128f9d3515f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,14 @@ { - 'name' : 'Estate', + 'name': 'Estate', 'installable': True, 'application': True, - 'auto_install': False + 'auto_install': False, + + 'data' : [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menu.xml', + ], + + 'license': 'LGPL-3', } \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..af1fce46b83 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,33 @@ +from odoo import fields,models +from datetime import date +from dateutil.relativedelta import relativedelta + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property is defined" + + name= fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_avaiblity = fields.Date(copy=False,default=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() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string = 'Direction', + selection=[('north','North'), ('south','South'), ('east','East'), ('west','West')], + help = "Orientation is used to locate garden's direction" + ) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[('new','new'), ('Offer Received','Offer Received'), ('Offer Accepted','Offer Accepted'), ('sold','sold'),('Cancelled','Cancelled')], + required=True, + default='new', + copy=False, + ) \ 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/views/estate_menu.xml b/estate/views/estate_menu.xml new file mode 100644 index 00000000000..85edf509fed --- /dev/null +++ b/estate/views/estate_menu.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ 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..76664a30ab4 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,99 @@ + + + + Properties + estate.property + list,form + + + + estate.property + estate.property + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate_property.search + estate.property + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file From 904620d65554f6cecad5b652a1347f683f1c1e3e Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Mon, 7 Jul 2025 10:43:38 +0530 Subject: [PATCH 03/15] [IMP] estate: add SQL constraints and computed fields Implemented SQL constraints to enforce data integrity and added computed fields with inverse methods for key model attributes.These changes ensure better consistency in business logic, such as automatic computation of property fields and proper validation of constraints like expected price and selling price. --- estate/__manifest__.py | 2 + estate/models/__init__.py | 5 +- estate/models/estate_property.py | 71 ++++++++++++- estate/models/estate_property_offer.py | 79 ++++++++++++++ estate/models/estate_property_tag.py | 11 ++ estate/models/estate_property_type.py | 11 ++ estate/security/ir.model.access.csv | 5 +- estate/views/estate_menu.xml | 12 ++- estate/views/estate_property_tag_views.xml | 9 ++ estate/views/estate_property_type_views.xml | 23 ++++ estate/views/estate_property_views.xml | 111 +++++++++++++------- 11 files changed, 295 insertions(+), 44 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_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 128f9d3515f..e179930d1b7 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,6 +7,8 @@ 'data' : [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', 'views/estate_menu.xml', ], diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..09b2099fe84 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ -from . import estate_property \ No newline at end of file +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index af1fce46b83..bf4009b1b8b 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 datetime import date from dateutil.relativedelta import relativedelta @@ -30,4 +31,70 @@ class EstateProperty(models.Model): required=True, default='new', copy=False, - ) \ No newline at end of file + ) + salesman_id = fields.Many2one("res.users",index=True,string="salesman",default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner",string="buyer",index=True,default=lambda self:self.env.user.partner_id.id) + + property_type_id = fields.Many2one("estate.property.type",string="property type") + + property_tag_ids = fields.Many2many("estate.property.tag",string="tags") + + offer_ids = fields.One2many("estate.property.offer","property_id",string="offers") + + + + _sql_constraints = [ + ('selling_price_positive','CHECK(selling_price>=0)','The selling price should be positive.'), + ('expected_price_positive','CHECK(expected_price>=0)','The expected price should be positive.') + ] + + total_area = fields.Float(compute="_compute_total") + + @api.depends("garden_area","living_area") + def _compute_total(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + best_offer = fields.Float(compute="_find_best") + + @api.depends("offer_ids.price") + def _find_best(self): + for record in self: + if record.offer_ids: + record.best_offer = max(record.offer_ids.mapped('price')) + else: + record.best_offer=0.0 + + + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area=10 + self.garden_orientation="north" + else: + self.garden_area = 0 + self.garden_orientation = False + + + status = fields.Selection( + selection=[ + ('new', 'New'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], + default='new', + copy=False + ) + + def sold_button_action(self): + for record in self: + if record.status == 'cancelled': + raise UserError("Cancelled property cannot be marked as sold.") + record.status = 'sold' + + def cancel_button_action(self): + for record in self: + if record.status == 'sold': + raise UserError("Sold property cannot be cancelled.") + record.status = 'cancelled' \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..c9c36330d29 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,79 @@ +from odoo import api,fields,models +from datetime import timedelta +from odoo.exceptions import UserError,ValidationError +from odoo.tools.float_utils import float_compare + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "estate property OFFER created" + + price=fields.Float() + status = fields.Selection( + selection=[('accepted','Accepted'), ('refused','Refused')], + copy=False, + ) + + partner_id = fields.Many2one("res.partner",string="Partner",index=True,required=True,default=lambda self:self.env.user.partner_id.id) + property_id = fields.Many2one("estate.property",index=True,required=True) + + validity = fields.Integer(default = 7) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + store=True + ) + + _sql_constraints = [ + ('offer_price_positive','CHECK(price>=0)','The offer price should be positive.') + ] + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for record in self: + if record.create_date: + create_date = record.create_date.date() + else: + create_date = fields.Date.today() + record.date_deadline = create_date + timedelta(days=record.validity) + + + def _inverse_date_deadline(self): + for record in self: + if record.create_date: + create_date = record.create_date.date() + else: + create_date = fields.Date.today() + if record.date_deadline: + record.validity = (record.date_deadline - create_date).days + + + + def accept_icon_action(self): + for record in self: + # Ensure only one accepted offer per property + if any( + offer.status == 'accepted' + for offer in record.property_id.offer_ids + ): + raise UserError("Only one offer can be accepted per property.") + + # ✅ Enforce 90% price check + min_price = record.property_id.expected_price * 0.9 + if float_compare(record.price, min_price, precision_digits=2) < 0: + raise ValidationError("Offer must be at least 90% of the expected price to be accepted.") + + record.status = 'accepted' + + # Refuse all other offers + other_offers = record.property_id.offer_ids - record + other_offers.write({'status': 'refused'}) + + # Set buyer and selling price on property + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + + def refuse_icon_action(self): + for record in self: + record.status = 'refused' diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..3fd28199509 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,11 @@ +from odoo import fields,models + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "estate property TAG created" + + name = fields.Char(required=True) + + _sql_constraints = [ + ('tag_name_unique','unique(name)','Tag name should be unique') + ] \ No newline at end of file diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..a0ecc298620 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,11 @@ +from odoo import fields,models + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property type is defined" + + name = fields.Char(required=True) + + _sql_constraints = [ + ('property_type_unique','unique(name)','property type should be unique') + ] \ No newline at end of file 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_menu.xml b/estate/views/estate_menu.xml index 85edf509fed..07a77c484e5 100644 --- a/estate/views/estate_menu.xml +++ b/estate/views/estate_menu.xml @@ -1,9 +1,15 @@ - - + + + + + + + + + - \ No newline at end of file diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..d23fbda27e5 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,9 @@ + + + + property tags + estate.property.tag + list,form + + + \ No newline at end of file diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..dbfb6c061f0 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,23 @@ + + + + property type + estate.property.type + list,form + + + + estate.property.type.search + estate.property.type + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 76664a30ab4..89b3ed4b15d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,6 +6,7 @@ list,form + estate.property estate.property @@ -29,11 +30,24 @@ estate.property
+
+

+ + + + + + + + + @@ -46,54 +60,77 @@ + + + - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + +

+ +

+ + + + + + + + + + + + + + +
+ +
@@ -12,12 +68,25 @@ - - + + + + + estate.property.offer.tree + estate.property.offer + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 89b3ed4b15d..cf7e9da8d51 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,6 +4,7 @@ Properties estate.property list,form + {'search_default_state': 1} @@ -11,14 +12,19 @@ estate.property estate.property - + + + - + @@ -31,22 +37,23 @@
-

- + - + @@ -73,8 +80,8 @@ - - + + @@ -83,15 +90,18 @@ - - + + - - -

- + +

+

diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e9e62c6c744..d968df4f49d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ Properties estate.property - list,form + list,form,kanban {'search_default_state': 1} @@ -28,6 +28,39 @@
+ + estate.property.kanban + estate.property + + + + + + +
+
+ +
+
+ +
+
+ Expected Price: +
+
+ Best Price: +
+
+ Selling Price: +
+
+
+
+
+
+
+ estate.property.form estate.property @@ -39,6 +72,13 @@ +
+

diff --git a/estate/views/inherited_model_view.xml b/estate/views/res_users_view.xml similarity index 80% rename from estate/views/inherited_model_view.xml rename to estate/views/res_users_view.xml index 6bdac056fe9..7c8e22bfeba 100644 --- a/estate/views/inherited_model_view.xml +++ b/estate/views/res_users_view.xml @@ -5,6 +5,13 @@ res.users +
+
From 877024681a1463c61fae4945582d3bf88785fffa Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Wed, 16 Jul 2025 11:37:20 +0530 Subject: [PATCH 12/15] [IMP] estate: added unit tests This commit adds test cases to validate the key workflows in the estate module, including property creation, status transitions, and offer handling.These tests ensure better coverage and help prevent regressions during future development. --- awesome_owl/static/src/card/card.js | 11 +++ awesome_owl/static/src/card/card.xml | 11 +++ awesome_owl/static/src/counter/counter.js | 21 +++++ awesome_owl/static/src/counter/counter.xml | 9 +++ awesome_owl/static/src/playground.js | 13 ++- awesome_owl/static/src/playground.xml | 15 +++- awesome_owl/static/src/todolist/todo_item.js | 24 ++++++ awesome_owl/static/src/todolist/todo_list.js | 24 ++++++ estate/models/estate_property.py | 4 +- estate/tests/__init__.py | 2 + estate/tests/test_estate_property.py | 85 ++++++++++++++++++++ 11 files changed, 215 insertions(+), 4 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/todolist/todo_item.js create mode 100644 awesome_owl/static/src/todolist/todo_list.js create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/test_estate_property.py diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..b57080bfdd4 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,11 @@ +/** @odoo-module **/ + +import { Component} from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title : {type: String}, + content : String, + } +} \ 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..0cd46c861df --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,11 @@ + + + +
+
+
+

+
+
+
+
\ 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..d47661856ee --- /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++; + if(this.props.onChange){ + this.props.onChange(this.state.value); + } + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..e75cdea6e3a --- /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..356cc2fe449 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,18 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList} from "./todolist/todo_list" export class Playground extends Component { static template = "awesome_owl.playground"; + static components = {Counter, Card ,TodoList}; + + setup(){ + this.state = useState({sum:2}); + } + incrementSum(value){ + this.state.sum += 1; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..2bbb96b96d0 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,9 +2,20 @@ -
- hello world +
+ hello +

+ +
+
+

the sum is :

+


+ + + + + diff --git a/awesome_owl/static/src/todolist/todo_item.js b/awesome_owl/static/src/todolist/todo_item.js new file mode 100644 index 00000000000..1e8eea52e4c --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_item.js @@ -0,0 +1,24 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = xml/* xml */` +
+ . + +
+ `; + + static props = { + todo: { + type: Object, + shape: { + id: Number, + description: String, + isCompleted: Boolean, + }, + optional: false, + }, + }; +} \ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_list.js b/awesome_owl/static/src/todolist/todo_list.js new file mode 100644 index 00000000000..e56611785b5 --- /dev/null +++ b/awesome_owl/static/src/todolist/todo_list.js @@ -0,0 +1,24 @@ +/** @odoo-module **/ + +import { Component, useState, xml } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = xml/* xml */` +
+

Todo List

+
+ +
+
+ `; + + static components = { TodoItem }; + + setup() { + this.todos = useState([ + { id: 3, description: "buy milk", isCompleted: false }, + { id: 4, description: "complete assignment", isCompleted: false }, + ]); + } +} \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0a1435b539c..6ed029ed383 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -82,8 +82,10 @@ def _onchange_garden(self): def sold_button_action(self): for record in self: - if record.status == 'cancelled': + if record.state == 'cancelled': raise UserError("Cancelled property cannot be marked as sold.") + elif record.state != 'Offer Accepted': + raise UserError("At least one offer need to be accepted.") record.status = 'sold' record.state = 'sold' diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..74067f8c4b1 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import test_estate_property diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..b2ac0c8e7ee --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,85 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests import Form + + +class EstatePropertyTestCase(TransactionCase): + @classmethod + def setUpClass(cls): + return super().setUpClass() + + def test_offer_creation_on_sold_property(self): + property = self.env["estate.property"].create( + { + "name": "Test case Property", + "expected_price": "123", + } + ) + + self.env["estate.property.offer"].create( + { + "price": 1500.00, + "partner_id": self.env.ref("base.res_partner_1").id, + "date_deadline": "2025-09-14", + "property_id": property.id, + "status": "accepted", + } + ) + + property.sold_button_action() + + with self.assertRaises( + UserError, msg="Cannot create an offer for a sold property" + ): + self.env["estate.property.offer"].create( + { + "price": 1500.00, + "partner_id": self.env.ref("base.res_partner_1").id, + "date_deadline": "2025-09-14", + "property_id": property.id, + } + ) + + def test_sell_property_on_accepted_offer(self): + property = self.env["estate.property"].create( + { + "name": "Test case Property 2", + "expected_price": "456", + } + ) + + self.env["estate.property.offer"].create( + { + "price": 1500.00, + "partner_id": self.env.ref("base.res_partner_1").id, + "date_deadline": "2025-09-14", + "property_id": property.id, + } + ) + with self.assertRaises(UserError): + property.sold_button_action() + + # def test_reset_garden_area_and_orientation(self): + # property = self.env["estate.property"].create( + # { + # "name": "Garden Test Property", + # "expected_price": "789", + # "garden": True, + # "garden_area": 50, + # "garden_orientation": "north", + # } + # ) + + # with Form(property) as form: + # form.garden = False + # form.save() + + # self.assertFalse(property.garden, "Garden checkbox should be unchecked.") + # self.assertFalse( + # property.garden_area, + # "Garden area should be reset when the garden checkbox is unchecked.", + # ) + # self.assertFalse( + # property.garden_orientation, + # "Orientation should be reset when the garden checkbox is unchecked.", + # ) From 64be3231b58ba5e81f72d8856c14004961d2b3e1 Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Fri, 18 Jul 2025 10:33:38 +0530 Subject: [PATCH 13/15] [ADD] awesome_owl: complete custom OWL component module Implemented awesome_owl to render dynamic frontend components within the Odoo environment.The module includes an OWL component, JS logic, and integration with QWeb templates. It demonstrates how to use client-side rendering with OWL in a modular and reusable way and serves as a foundational example for building interactive UI features using OWL in Odoo. --- awesome_owl/static/src/card/card.js | 19 +++++-- awesome_owl/static/src/card/card.xml | 12 +++-- awesome_owl/static/src/counter/counter.js | 2 +- awesome_owl/static/src/counter/counter.xml | 2 +- awesome_owl/static/src/playground.js | 6 ++- awesome_owl/static/src/playground.xml | 31 ++++++----- awesome_owl/static/src/todolist/todo_list.js | 24 --------- .../todolist/{todo_item.js => todoitem.js} | 14 ++--- awesome_owl/static/src/todolist/todoitem.xml | 15 ++++++ awesome_owl/static/src/todolist/todolist.js | 52 +++++++++++++++++++ awesome_owl/static/src/todolist/todolist.xml | 19 +++++++ awesome_owl/static/src/utils/utils.js | 13 +++++ estate/tests/test_estate_property.py | 26 ---------- 13 files changed, 150 insertions(+), 85 deletions(-) delete mode 100644 awesome_owl/static/src/todolist/todo_list.js rename awesome_owl/static/src/todolist/{todo_item.js => todoitem.js} (60%) create mode 100644 awesome_owl/static/src/todolist/todoitem.xml create mode 100644 awesome_owl/static/src/todolist/todolist.js create mode 100644 awesome_owl/static/src/todolist/todolist.xml create mode 100644 awesome_owl/static/src/utils/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index b57080bfdd4..16fb4774fe3 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -1,11 +1,20 @@ /** @odoo-module **/ -import { Component} from "@odoo/owl"; +import { Component, useState } from "@odoo/owl" export class Card extends Component { - static template = "awesome_owl.Card"; + static template = "awesome_owl.Card" static props = { - title : {type: String}, - content : String, + title: String, + slots: { + type: Object, + shape: { default: true }, + }, + }; + setup() { + this.state = useState({ isToggled: true }); } -} \ No newline at end of file + toggle() { + this.state.isToggled = !this.state.isToggled; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index 0cd46c861df..1e806d23418 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -3,9 +3,15 @@
-
-

+
+ + + +
+

+ +

- \ No newline at end of file + diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index d47661856ee..491805cec25 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -18,4 +18,4 @@ export class Counter extends Component { this.props.onChange(this.state.value); } } -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml index e75cdea6e3a..e06a71bade8 100644 --- a/awesome_owl/static/src/counter/counter.xml +++ b/awesome_owl/static/src/counter/counter.xml @@ -6,4 +6,4 @@
-
\ No newline at end of file + diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 356cc2fe449..3bcf9c44466 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { Component, useState } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; import { Counter } from "./counter/counter"; import { Card } from "./card/card"; -import { TodoList} from "./todolist/todo_list" +import { TodoList} from "./todolist/todolist" export class Playground extends Component { static template = "awesome_owl.playground"; @@ -15,4 +15,6 @@ export class Playground extends Component { incrementSum(value){ this.state.sum += 1; } + content1 = "
some content
" + content2 = markup("
some content
") } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 2bbb96b96d0..ed203822ef6 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,21 +1,24 @@ - + - -
- hello -

+
+ hello world + -
-

the sum is :

-


- - - - - +

The Sum is:

+
+
+ +

This is the content of Card 1.

+
+ + + +
+
+ +
-
diff --git a/awesome_owl/static/src/todolist/todo_list.js b/awesome_owl/static/src/todolist/todo_list.js deleted file mode 100644 index e56611785b5..00000000000 --- a/awesome_owl/static/src/todolist/todo_list.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @odoo-module **/ - -import { Component, useState, xml } from "@odoo/owl"; -import { TodoItem } from "./todo_item"; - -export class TodoList extends Component { - static template = xml/* xml */` -
-

Todo List

-
- -
-
- `; - - static components = { TodoItem }; - - setup() { - this.todos = useState([ - { id: 3, description: "buy milk", isCompleted: false }, - { id: 4, description: "complete assignment", isCompleted: false }, - ]); - } -} \ No newline at end of file diff --git a/awesome_owl/static/src/todolist/todo_item.js b/awesome_owl/static/src/todolist/todoitem.js similarity index 60% rename from awesome_owl/static/src/todolist/todo_item.js rename to awesome_owl/static/src/todolist/todoitem.js index 1e8eea52e4c..e2a5973cc88 100644 --- a/awesome_owl/static/src/todolist/todo_item.js +++ b/awesome_owl/static/src/todolist/todoitem.js @@ -1,15 +1,9 @@ /** @odoo-module **/ -import { Component, xml } from "@odoo/owl"; +import { Component } from "@odoo/owl"; export class TodoItem extends Component { - static template = xml/* xml */` -
- . - -
- `; - + static template = "awesome_owl.TodoItem"; static props = { todo: { type: Object, @@ -20,5 +14,7 @@ export class TodoItem extends Component { }, optional: false, }, + toggleState: Function, + todoDelete: Function, }; -} \ No newline at end of file +} diff --git a/awesome_owl/static/src/todolist/todoitem.xml b/awesome_owl/static/src/todolist/todoitem.xml new file mode 100644 index 00000000000..4290ed55944 --- /dev/null +++ b/awesome_owl/static/src/todolist/todoitem.xml @@ -0,0 +1,15 @@ + + + +
+ + . + + +
+
+
diff --git a/awesome_owl/static/src/todolist/todolist.js b/awesome_owl/static/src/todolist/todolist.js new file mode 100644 index 00000000000..6a5610c0ea6 --- /dev/null +++ b/awesome_owl/static/src/todolist/todolist.js @@ -0,0 +1,52 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todoitem"; +import { useAutofocus } from "../utils/utils"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + setup() { + this.state = useState({ + text: "", + todos: [], + nextId: 1, + }); + this.inputRef = useAutofocus("input"); + this.toggleState = this.toggleState.bind(this); + this.todoDelete = this.todoDelete.bind(this); + } + + addTodo(ev) { + if (ev.key === "Enter") { + const description = this.state.text.trim(); + if (!description) return; + + this.state.todos.push({ + id: this.state.nextId++, + description: description, + isCompleted: false, + }); + this.state.text = ""; + } + } + + toggleState(todoId) { + const todo = this.state.todos.find((t) => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + todoDelete(todoId) { + const index = this.state.todos.findIndex((t) => t.id === todoId); + if (index >= 0) { + this.state.todos.splice(index, 1); + for (let i = index; i < this.state.todos.length; i++) { + this.state.todos[i].id = i + 1; + } + } + this.state.nextId = this.state.todos.length + 1; + } +} diff --git a/awesome_owl/static/src/todolist/todolist.xml b/awesome_owl/static/src/todolist/todolist.xml new file mode 100644 index 00000000000..38833d59b85 --- /dev/null +++ b/awesome_owl/static/src/todolist/todolist.xml @@ -0,0 +1,19 @@ + + + +
+

Todo List

+ +
+ +
+
+
+
diff --git a/awesome_owl/static/src/utils/utils.js b/awesome_owl/static/src/utils/utils.js new file mode 100644 index 00000000000..3bda444e64a --- /dev/null +++ b/awesome_owl/static/src/utils/utils.js @@ -0,0 +1,13 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(input) { + const inputRef = useRef(input); + + onMounted(() => { + if (inputRef.el) { + inputRef.el.focus(); + } + }); + + return { inputRef }; +} \ No newline at end of file diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py index b2ac0c8e7ee..b18e0f01e26 100644 --- a/estate/tests/test_estate_property.py +++ b/estate/tests/test_estate_property.py @@ -1,6 +1,5 @@ from odoo.tests.common import TransactionCase from odoo.exceptions import UserError -from odoo.tests import Form class EstatePropertyTestCase(TransactionCase): @@ -58,28 +57,3 @@ def test_sell_property_on_accepted_offer(self): ) with self.assertRaises(UserError): property.sold_button_action() - - # def test_reset_garden_area_and_orientation(self): - # property = self.env["estate.property"].create( - # { - # "name": "Garden Test Property", - # "expected_price": "789", - # "garden": True, - # "garden_area": 50, - # "garden_orientation": "north", - # } - # ) - - # with Form(property) as form: - # form.garden = False - # form.save() - - # self.assertFalse(property.garden, "Garden checkbox should be unchecked.") - # self.assertFalse( - # property.garden_area, - # "Garden area should be reset when the garden checkbox is unchecked.", - # ) - # self.assertFalse( - # property.garden_orientation, - # "Orientation should be reset when the garden checkbox is unchecked.", - # ) From 1b97a76c3298194866c75d186104d1cd6983dd31 Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Fri, 18 Jul 2025 18:24:17 +0530 Subject: [PATCH 14/15] [ADD] awesome_dashboard: implement custom dashboard module Developed a custom dashboard module using OWL and JavaScript to display key business metrics in a dynamic and user-friendly interface. The dashboard integrates with backend models to show real-time data using cards and charts.This module demonstrates how to build interactive dashboards in Odoo using client-side rendering and custom components. --- awesome_dashboard/static/src/dashboard.js | 10 --- awesome_dashboard/static/src/dashboard.xml | 8 --- .../static/src/dashboard/dashboard.js | 57 +++++++++++++++ .../static/src/dashboard/dashboard.scss | 40 +++++++++++ .../static/src/dashboard/dashboard.xml | 21 ++++++ .../dashboard/dashboardItem/dashboarditem.js | 11 +++ .../dashboard/dashboardItem/dashboarditem.xml | 10 +++ .../static/src/dashboard/dashboard_action.js | 14 ++++ .../static/src/dashboard/dashboard_item.js | 72 +++++++++++++++++++ .../src/dashboard/numbercard/numbercard.js | 17 +++++ .../src/dashboard/numbercard/numbercard.xml | 15 ++++ .../src/dashboard/pieChart/pieChart.xml | 6 ++ .../static/src/dashboard/pieChart/piechart.js | 67 +++++++++++++++++ .../dashboard/pieChartCard/pie_chart_card.js | 36 ++++++++++ .../dashboard/pieChartCard/pie_chart_card.xml | 22 ++++++ .../static/src/dashboard/statistics.js | 34 +++++++++ 16 files changed, 422 insertions(+), 18 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js delete mode 100644 awesome_dashboard/static/src/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_action.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/numbercard/numbercard.js create mode 100644 awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml create mode 100644 awesome_dashboard/static/src/dashboard/pieChart/pieChart.xml create mode 100644 awesome_dashboard/static/src/dashboard/pieChart/piechart.js create mode 100644 awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/statistics.js diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index 637fa4bb972..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @odoo-module **/ - -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..9edb60955b8 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,57 @@ +/** @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 "./dashboardItem/dashboarditem"; +import { rpc } from "@web/core/network/rpc"; +import { PieChart } from "./pieChart/piechart"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, PieChart}; + + setup(){ + const dashboardItemsRegistry = registry.category("awesome_dashboard"); + this.items = dashboardItemsRegistry.getAll(); + this.action = useService("action"); + this.statisticsService = useService("awesome_dashboard.statistics"); + this.state = useState({ statistics: this.statisticsService.statistics }); + this.displayState = useState({ + disabledItems: [], + isLoading: true, + }); + onWillStart(async () => { + try { + const fetchedDisabledItems = await rpc("/web/dataset/call_kw/res.users/get_dashboard_settings", { + model: 'res.users', + method: 'get_dashboard_settings', + args: [], + kwargs: {}, + }); + this.displayState.disabledItems = fetchedDisabledItems; + } catch (error) { + console.error("Error loading initial dashboard settings from server:", error); + this.displayState.disabledItems = []; + } finally { + this.displayState.isLoading = false; + } + }); + } + + openCustomerView() { + this.action.doAction("base.action_partner_form") + } + + openLeadsView() { + this.action.doAction({ + type: 'ir.actions.act_window', + target: 'current', + res_model: 'crm.lead', + views: [[false, 'list'], [false, 'form']], + }) + } +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..22f0895fd7f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,40 @@ +.o_dashboard{ + background-color: gray; +} +.o_dashboard_stat_block { + text-align: center; + margin-bottom: 24px; +} + +.o_dashboard_stat_label { + font-weight: normal; + margin-bottom: 10px; + display: block; +} + +.o_dashboard_stat_value { + font-size: 48px; + color: #228B22; + font-weight: bold; +} +.o_dashboard_item { + background: #fff; + border-radius: 0.75rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.07); + padding: 1rem; + margin: 1rem; + display: inline-flex; + justify-content: center; + vertical-align: top; + min-height: 3rem; +} + +@media (max-width: 426px) { + .o_dashboard_item { + width: 100% !important; + display: flex; + margin-left: 0.5rem; + margin-right: 0.5rem; + box-sizing: border-box; + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..5ae0df9b259 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,21 @@ + + + + + + + + + +
+ + + + + + +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.js b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.js new file mode 100644 index 00000000000..15e7c7404bd --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.js @@ -0,0 +1,11 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl" + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem" + + static props = { + size: { type: Number, optional: true, default: 1 }, + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.xml b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.xml new file mode 100644 index 00000000000..cacdc48785b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.xml @@ -0,0 +1,10 @@ + + + + +
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_action.js b/awesome_dashboard/static/src/dashboard/dashboard_action.js new file mode 100644 index 00000000000..b93847bb272 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_action.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl" +import { LazyComponent } from "@web/core/assets"; +import { registry } from "@web/core/registry"; + +export class DashboardComponentLoader extends Component { + static components = { LazyComponent } + static template = xml` + + `; + +} +registry.category("actions").add("awesome_dashboard.dashboard", DashboardComponentLoader); diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js new file mode 100644 index 00000000000..b68d8ad72da --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,72 @@ +/** @odoo-module **/ + +import { NumberCard } from "./numbercard/numbercard"; +import { PieChartCard } from "./pieChartCard/pie_chart_card"; +import { registry } from "@web/core/registry"; +import { _t } from "@web/core/l10n/translation"; + +const items = [ + { + id: "nb_new_orders", + description: _t("The number of new orders, this month"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("New Orders This Month:"), + value: data.data.nb_new_orders + }), + }, + { + id: "total_amount", + description: _t("The total amount of orders, this month"), + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Total Amount This Month:", + value: data.data.total_amount + }), + }, + { + id: "average_quantity", + description: _t("The average number of t-shirts by order"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Avg. T-Shirts per Order:"), + value: data.data.average_quantity + }), + }, + { + id: "nb_cancelled_orders", + description: _t("The number of cancelled orders, this month"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Cancelled Orders:"), + value: data.data.nb_cancelled_orders + }), + }, + { + id: "average_time", + description: _t("The average time (in hours) elapsed between the moment an order is created, and the moment is it sent"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Avg. Time New → Sent/Cancelled:"), + value: data.data.average_time + }), + }, + { + id: "orders_by_size", + description: _t("Number of shirts ordered based on size"), + Component: PieChartCard, + size: 3, + props: (data) => ({ + title: _t("Shirt orders by size:"), + value: data.data.orders_by_size + }), + } +] +items.forEach((item) => { + registry.category("awesome_dashboard").add(item.id, item) +}); diff --git a/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js new file mode 100644 index 00000000000..1dcbb67e063 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js @@ -0,0 +1,17 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + + static props = { + title: { type: String }, + value: { type: [String, Number] } + } + + _t(...args) { + return _t(...args); + } +} diff --git a/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml new file mode 100644 index 00000000000..1916e9648bb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml @@ -0,0 +1,15 @@ + + + +
+ + + + + + + + +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pieChart/pieChart.xml b/awesome_dashboard/static/src/dashboard/pieChart/pieChart.xml new file mode 100644 index 00000000000..83b790ccf7a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pieChart/pieChart.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/pieChart/piechart.js b/awesome_dashboard/static/src/dashboard/pieChart/piechart.js new file mode 100644 index 00000000000..0669d317a99 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pieChart/piechart.js @@ -0,0 +1,67 @@ +/** @odoo-module **/ + +import { Component, onWillStart, useRef, onMounted, useEffect, onWillUnmount } from "@odoo/owl"; +import { loadJS } from "@web/core/assets" + +export class PieChart extends Component { + static template = "awesome_dashboard.Piechart"; + static props = { + data: { type: Object }, + onSliceClick: { type: Function, optional: true }, + }; + setup() { + this.chart = null; + this.pieChartCanvasRef = useRef("pie_chart_canvas"); + + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }) + + this.chartData = { + labels: Object.keys(this.props.data), + datasets: [{ + data: Object.values(this.props.data) + }] + }; + + onMounted(() => { + this.makePieChart(); + }) + + this.cleanupPieChart = () => { + if (this.chart) { + this.chart.destroy(); + this.chart = null; + } + }; + + onWillUnmount(this.cleanupPieChart); + + useEffect(() => { + this.cleanupPieChart(); + if (this.pieChartCanvasRef.el) { + this.makePieChart(); + } + }, () => [this.props.data]) + } + + makePieChart() { + this.chart = new Chart(this.pieChartCanvasRef.el, { + type: "pie", + data: this.chartData, + options: { + responsive: true, + maintainAspectRatio: false, + onClick: (event, elements) => { + if (elements.length > 0) { + const clickedElementIndex = elements[0].index; + const label = this.chartData.labels[clickedElementIndex]; + if (this.props.onSliceClick) { + this.props.onSliceClick(label); + } + } + } + } + }) + } +} diff --git a/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.js new file mode 100644 index 00000000000..80e83306583 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.js @@ -0,0 +1,36 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; +import { PieChart } from "../pieChart/piechart"; +import { useService } from "@web/core/utils/hooks"; +import { _t } from "@web/core/l10n/translation"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PieChart }; + + static props = { + title: { type: String }, + data: { type: Object } + } + + setup() { + this.action = useService("action"); + } + + _t(...args) { + return _t(...args); + } + + onPieSliceClick(size) { + console.log("Clicked on slice for size:", size); + this.action.doAction({ + type: 'ir.actions.act_window', + name: `Orders (Size: ${size.toUpperCase()})`, + res_model: 'sale.order', + views: [[false, 'list'], [false, 'form']], + domain: [["order_line.product_template_attribute_value_ids.display_name", "ilike", size]], + target: 'current', + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.xml new file mode 100644 index 00000000000..210da7ccd4c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.xml @@ -0,0 +1,22 @@ + + + +
+ + + + + + + + + + +
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/statistics.js b/awesome_dashboard/static/src/dashboard/statistics.js new file mode 100644 index 00000000000..2b670a73084 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics.js @@ -0,0 +1,34 @@ +/** @odoo-module **/ + +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; + +const statisticsService = { + start() { + const statistics = reactive({ data: null, loading: true, error: null }); + + async function fetchStatistics() { + statistics.loading = true; + statistics.error = null; + try { + const response = await rpc("/awesome_dashboard/statistics"); + statistics.data = response; + } catch (e) { + statistics.error = e; + } finally { + statistics.loading = false; + } + } + + fetchStatistics(); + + setInterval(fetchStatistics, 600000); + + return { + statistics, + reload: fetchStatistics, + }; + }, +}; +registry.category("services").add("awesome_dashboard.statistics", statisticsService); From d92c3d6ff059dd1d702c1c317c280f3e7d50f49b Mon Sep 17 00:00:00 2001 From: arkp-odoo Date: Mon, 21 Jul 2025 12:57:19 +0530 Subject: [PATCH 15/15] [FIX] awesome_dashboard,*: fixed codebase accourding PR review *=awesome_owl,estate This commit improves the frontend code for the module to align with Odoo's modern best practices. QWeb templates and OWL JS components have been refactored so that string translations using are handled exclusively in JavaScript code. Getters such as and are used to expose translated strings to the templates, ensuring QWeb remains simple and translations are performed in JS only. Periodic server polling for statistics now uses a recursive approach for efficient and reliable scheduling, preventing overlapping RPC calls that can occur with . The PieChart component's click handler has been simplified: temporary variables are removed when values are only used once, resulting in cleaner and more maintainable code. --- .../static/src/dashboard/dashboard.js | 10 ++--- .../static/src/dashboard/dashboard.scss | 37 ------------------- .../dashboard/dashboardItem/dashboarditem.js | 2 - .../dashboard/dashboardItem/dashboarditem.xml | 15 ++++---- .../static/src/dashboard/dashboard_action.js | 2 - .../static/src/dashboard/dashboard_item.js | 4 +- .../src/dashboard/numbercard/numbercard.js | 6 +-- .../src/dashboard/numbercard/numbercard.xml | 21 +++++------ .../static/src/dashboard/pieChart/piechart.js | 6 +-- .../dashboard/pieChartCard/pie_chart_card.js | 20 +++------- .../dashboard/pieChartCard/pie_chart_card.xml | 10 ++--- .../static/src/dashboard/statistics.js | 11 +++--- awesome_owl/static/src/card/card.xml | 1 - awesome_owl/static/src/playground.js | 6 +-- estate/tests/test_estate_property.py | 2 +- 15 files changed, 43 insertions(+), 110 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index 9edb60955b8..622bc2f7200 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -1,16 +1,14 @@ -/** @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 "./dashboardItem/dashboarditem"; +import { DashboardItem } from "@awesome_dashboard/dashboard/dashboardItem/dashboarditem"; import { rpc } from "@web/core/network/rpc"; -import { PieChart } from "./pieChart/piechart"; +import { PieChart } from "@awesome_dashboard/dashboard/pieChart/piechart"; class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; - static components = { Layout, DashboardItem, PieChart}; + static components = { Layout, DashboardItem, PieChart }; setup(){ const dashboardItemsRegistry = registry.category("awesome_dashboard"); @@ -49,7 +47,7 @@ class AwesomeDashboard extends Component { type: 'ir.actions.act_window', target: 'current', res_model: 'crm.lead', - views: [[false, 'list'], [false, 'form']], + views: [[false, 'list'], [false, 'form']] }) } } diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss index 22f0895fd7f..51f9e73e642 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.scss +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -1,40 +1,3 @@ .o_dashboard{ background-color: gray; } -.o_dashboard_stat_block { - text-align: center; - margin-bottom: 24px; -} - -.o_dashboard_stat_label { - font-weight: normal; - margin-bottom: 10px; - display: block; -} - -.o_dashboard_stat_value { - font-size: 48px; - color: #228B22; - font-weight: bold; -} -.o_dashboard_item { - background: #fff; - border-radius: 0.75rem; - box-shadow: 0 2px 8px rgba(0,0,0,0.07); - padding: 1rem; - margin: 1rem; - display: inline-flex; - justify-content: center; - vertical-align: top; - min-height: 3rem; -} - -@media (max-width: 426px) { - .o_dashboard_item { - width: 100% !important; - display: flex; - margin-left: 0.5rem; - margin-right: 0.5rem; - box-sizing: border-box; - } -} diff --git a/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.js b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.js index 15e7c7404bd..a9454db1a64 100644 --- a/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.js +++ b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.js @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import { Component } from "@odoo/owl" export class DashboardItem extends Component { diff --git a/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.xml b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.xml index cacdc48785b..40379610b20 100644 --- a/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.xml +++ b/awesome_dashboard/static/src/dashboard/dashboardItem/dashboarditem.xml @@ -1,10 +1,9 @@ - - - - -
+ + +
+
- - +
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_action.js b/awesome_dashboard/static/src/dashboard/dashboard_action.js index b93847bb272..6d27b2f12a9 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_action.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_action.js @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import { Component, xml } from "@odoo/owl" import { LazyComponent } from "@web/core/assets"; import { registry } from "@web/core/registry"; diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js index b68d8ad72da..a9dad03bc40 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_item.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { NumberCard } from "./numbercard/numbercard"; -import { PieChartCard } from "./pieChartCard/pie_chart_card"; +import { NumberCard } from "@awesome_dashboard/dashboard/numbercard/numbercard"; +import { PieChartCard } from "@awesome_dashboard/dashboard/pieChartCard/pie_chart_card"; import { registry } from "@web/core/registry"; import { _t } from "@web/core/l10n/translation"; diff --git a/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js index 1dcbb67e063..efcafa17f39 100644 --- a/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import { Component } from "@odoo/owl"; import { _t } from "@web/core/l10n/translation"; @@ -11,7 +9,7 @@ export class NumberCard extends Component { value: { type: [String, Number] } } - _t(...args) { - return _t(...args); + get translatedTitle() { + return _t(this.props.title); } } diff --git a/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml index 1916e9648bb..6182c100e3e 100644 --- a/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml @@ -1,15 +1,12 @@ - - - -
- - - - + +
+
+ + - - + +
- - +
+
diff --git a/awesome_dashboard/static/src/dashboard/pieChart/piechart.js b/awesome_dashboard/static/src/dashboard/pieChart/piechart.js index 0669d317a99..f7f3f17bb0a 100644 --- a/awesome_dashboard/static/src/dashboard/pieChart/piechart.js +++ b/awesome_dashboard/static/src/dashboard/pieChart/piechart.js @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import { Component, onWillStart, useRef, onMounted, useEffect, onWillUnmount } from "@odoo/owl"; import { loadJS } from "@web/core/assets" @@ -54,10 +52,8 @@ export class PieChart extends Component { maintainAspectRatio: false, onClick: (event, elements) => { if (elements.length > 0) { - const clickedElementIndex = elements[0].index; - const label = this.chartData.labels[clickedElementIndex]; if (this.props.onSliceClick) { - this.props.onSliceClick(label); + this.props.onSliceClick(this.chartData.labels[elements[0].index]); } } } diff --git a/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.js index 80e83306583..6b0bc840a54 100644 --- a/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.js +++ b/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.js @@ -1,7 +1,5 @@ -/** @odoo-module **/ - import { Component } from "@odoo/owl"; -import { PieChart } from "../pieChart/piechart"; +import { PieChart } from "@awesome_dashboard/dashboard/pieChart/piechart"; import { useService } from "@web/core/utils/hooks"; import { _t } from "@web/core/l10n/translation"; @@ -18,19 +16,11 @@ export class PieChartCard extends Component { this.action = useService("action"); } - _t(...args) { - return _t(...args); + get translatedTitle() { + return _t(this.props.title); } - onPieSliceClick(size) { - console.log("Clicked on slice for size:", size); - this.action.doAction({ - type: 'ir.actions.act_window', - name: `Orders (Size: ${size.toUpperCase()})`, - res_model: 'sale.order', - views: [[false, 'list'], [false, 'form']], - domain: [["order_line.product_template_attribute_value_ids.display_name", "ilike", size]], - target: 'current', - }); + get translatedValue() { + return _t(this.props.value); } } diff --git a/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.xml index 210da7ccd4c..d888fb89480 100644 --- a/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.xml +++ b/awesome_dashboard/static/src/dashboard/pieChartCard/pie_chart_card.xml @@ -1,15 +1,15 @@ -
- +
+ - + - + - +
diff --git a/awesome_dashboard/static/src/dashboard/statistics.js b/awesome_dashboard/static/src/dashboard/statistics.js index 2b670a73084..0fc1d9ba749 100644 --- a/awesome_dashboard/static/src/dashboard/statistics.js +++ b/awesome_dashboard/static/src/dashboard/statistics.js @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import { rpc } from "@web/core/network/rpc"; import { registry } from "@web/core/registry"; import { reactive } from "@odoo/owl"; @@ -21,13 +19,16 @@ const statisticsService = { } } - fetchStatistics(); + async function fetchStatisticsRecursively() { + await fetchStatistics(); + setTimeout(fetchStatisticsRecursively, 600000); + } - setInterval(fetchStatistics, 600000); + fetchStatisticsRecursively(); return { statistics, - reload: fetchStatistics, + reload: fetchStatistics }; }, }; diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index 1e806d23418..7ea2ed0e6a1 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -5,7 +5,6 @@
-

diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 3bcf9c44466..035d338f3eb 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import { Component, markup, useState } from "@odoo/owl"; import { Counter } from "./counter/counter"; import { Card } from "./card/card"; @@ -12,9 +10,7 @@ export class Playground extends Component { setup(){ this.state = useState({sum:2}); } - incrementSum(value){ + incrementSum(){ this.state.sum += 1; } - content1 = "

some content
" - content2 = markup("
some content
") } diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py index b18e0f01e26..22d1d8384bf 100644 --- a/estate/tests/test_estate_property.py +++ b/estate/tests/test_estate_property.py @@ -28,7 +28,7 @@ def test_offer_creation_on_sold_property(self): property.sold_button_action() with self.assertRaises( - UserError, msg="Cannot create an offer for a sold property" + UserError ): self.env["estate.property.offer"].create( {