From 2aae33ed2dd5be993bd8e75cea12ed9a9f1634d9 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Mon, 19 Jan 2026 16:08:37 +0100 Subject: [PATCH 01/22] adding real estate module --- estate/__init__.py | 0 estate/__manifest__.py | 9 +++++++++ 2 files changed, 9 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..883f6fb401b --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,9 @@ +{ + 'name': 'Estate', + 'version': '1.9', + 'category': 'Real Estate', + 'summary' : 'Manage your real estate properties', + 'depends': ['base'], + 'installable': True, + 'application': True, +} \ No newline at end of file From abd9e6b4943c438405a604021c89e54f05304dd8 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Mon, 19 Jan 2026 17:20:13 +0100 Subject: [PATCH 02/22] Add estate property model --- estate/__init__.py | 1 + estate/__manifest__.py | 2 ++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 33 ++++++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+) 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 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 883f6fb401b..778a364187e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,6 +3,8 @@ 'version': '1.9', 'category': 'Real Estate', 'summary' : 'Manage your real estate properties', + 'author': 'Odoo', + 'license': 'LGPL-3', 'depends': ['base'], 'installable': True, 'application': True, 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..4736d73934d --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,33 @@ +from odoo import models, fields + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Estate Property' + name = fields.Char(string='Name', required=True) + + property_type = fields.Selection([ + ('apartment', 'Apartment'), + ('house', 'House'), + ('villa', 'Villa'), + ], string='Property Type') + postcode = fields.Char(string='Postcode') + available_from = fields.Date(string='Available From') + + expected_price = fields.Float(string='Expected Price') + selling_price = fields.Float(string='Selling Price') + best_offer = fields.Float(string='Best Offer') + + description = fields.Text(string='Description') + bedrooms = fields.Integer(string='Bedrooms') + living_area = fields.Integer(string='Living Area (sqm)') + facades = fields.Integer(string='Facades') + garage = fields.Boolean(string='Garage') + garden = fields.Boolean(string='Garden') + garden_area = fields.Integer(string='Garden Area (sqm)') + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], string='Garden Orientation') + total_area = fields.Integer(string='Total Area (sqm)') \ No newline at end of file From 9737ef29b62049deb0a42a40af3e662096a325e3 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Tue, 20 Jan 2026 09:55:06 +0100 Subject: [PATCH 03/22] adding security access rights --- estate/__manifest__.py | 3 +++ estate/models/estate_property.py | 4 ++-- estate/security/ir.model.access.csv | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 778a364187e..9c5416e8f20 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,6 +6,9 @@ 'author': 'Odoo', 'license': 'LGPL-3', 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + ], 'installable': True, 'application': True, } \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4736d73934d..4959761c14a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -13,12 +13,12 @@ class EstateProperty(models.Model): postcode = fields.Char(string='Postcode') available_from = fields.Date(string='Available From') - expected_price = fields.Float(string='Expected Price') + expected_price = fields.Float(string='Expected Price', required=True) selling_price = fields.Float(string='Selling Price') best_offer = fields.Float(string='Best Offer') description = fields.Text(string='Description') - bedrooms = fields.Integer(string='Bedrooms') + bedrooms = fields.Integer(string='Bedrooms', required=True) living_area = fields.Integer(string='Living Area (sqm)') facades = fields.Integer(string='Facades') garage = fields.Boolean(string='Garage') diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..0e11f47e58d --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From cd2c46937f7202bcb3fb9ad33eb76a0b0472d795 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Tue, 20 Jan 2026 11:45:32 +0100 Subject: [PATCH 04/22] Chapter 5 - UI - --- .gitignore | 1 + estate/__init__.py | 2 +- estate/__manifest__.py | 26 +++++---- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 80 +++++++++++++++++--------- estate/views/estate_menus.xml | 8 +++ estate/views/estate_property_views.xml | 8 +++ 7 files changed, 85 insertions(+), 42 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/.gitignore b/.gitignore index b6e47617de1..899bc325511 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,4 @@ dmypy.json # Pyre type checker .pyre/ +ruff.toml \ No newline at end of file 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 9c5416e8f20..c1e19b372fb 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,14 +1,16 @@ { - 'name': 'Estate', - 'version': '1.9', - 'category': 'Real Estate', - 'summary' : 'Manage your real estate properties', - 'author': 'Odoo', - 'license': 'LGPL-3', - 'depends': ['base'], - 'data': [ - 'security/ir.model.access.csv', + "name": "Estate", + "version": "1.9", + "category": "Real Estate", + "summary": "Manage your real estate properties", + "author": "Odoo", + "license": "LGPL-3", + "depends": ["base"], + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_menus.xml", ], - 'installable': True, - 'application': True, -} \ No newline at end of file + "installable": True, + "application": True, +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4959761c14a..414fd8afbc7 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,33 +1,57 @@ -from odoo import models, fields +from odoo import fields, models + class EstateProperty(models.Model): - _name = 'estate.property' - _description = 'Estate Property' - name = fields.Char(string='Name', required=True) + _name = "estate.property" + _description = "Estate Property" - property_type = fields.Selection([ - ('apartment', 'Apartment'), - ('house', 'House'), - ('villa', 'Villa'), - ], string='Property Type') - postcode = fields.Char(string='Postcode') - available_from = fields.Date(string='Available From') + name = fields.Char(string="Name", required=True) + property_type = fields.Selection( + [ + ("apartment", "Apartment"), + ("house", "House"), + ("villa", "Villa"), + ], + string="Property Type", + ) + postcode = fields.Char(string="Postcode") + available_from = fields.Date( + string="Available From", + copy=False, + default=fields.Date.add(fields.Date.today(), months=3), + ) - expected_price = fields.Float(string='Expected Price', required=True) - selling_price = fields.Float(string='Selling Price') - best_offer = fields.Float(string='Best Offer') + expected_price = fields.Float(string="Expected Price", required=True) + selling_price = fields.Float(string="Selling Price", readonly=True) + best_offer = fields.Float(string="Best Offer") - description = fields.Text(string='Description') - bedrooms = fields.Integer(string='Bedrooms', required=True) - living_area = fields.Integer(string='Living Area (sqm)') - facades = fields.Integer(string='Facades') - garage = fields.Boolean(string='Garage') - garden = fields.Boolean(string='Garden') - garden_area = fields.Integer(string='Garden Area (sqm)') - garden_orientation = fields.Selection([ - ('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West'), - ], string='Garden Orientation') - total_area = fields.Integer(string='Total Area (sqm)') \ No newline at end of file + description = fields.Text(string="Description") + bedrooms = fields.Integer(string="Bedrooms", default=2) + living_area = fields.Integer(string="Living Area (sqm)") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area (sqm)") + garden_orientation = fields.Selection( + [ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + string="Garden Orientation", + ) + total_area = fields.Integer(string="Total Area (sqm)") + active = fields.Boolean(string="Active", default=True) + state = fields.Selection( + [ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled"), + ], + string="State", + default="new", + required=True, + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..1f84deb1b34 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..158cfa294a9 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Estate Property + estate.property + list,form + + From 1069baaf7f88e74921e8b9f420281e9ae4bf2776 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Tue, 20 Jan 2026 15:08:02 +0100 Subject: [PATCH 05/22] Chapter 6 Basic Views --- estate/views/estate_property_views.xml | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 158cfa294a9..40788c69982 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,8 +1,91 @@ + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+ +
+ + +
+
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + Estate Property estate.property list,form +
From 1c82057c3a2c3a460575b2e6c5e1265db85496bf Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Tue, 20 Jan 2026 17:34:51 +0100 Subject: [PATCH 06/22] Chapter 7 commit --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 7 ++++- estate/models/estate_property.py | 19 +++++++------ estate/models/estate_property_offer.py | 18 ++++++++++++ estate/models/estate_property_tag.py | 8 ++++++ estate/models/estate_property_type.py | 8 ++++++ estate/security/ir.model.access.csv | 5 +++- estate/views/estate_menus.xml | 4 +++ estate/views/estate_property_offer_views.xml | 30 ++++++++++++++++++++ estate/views/estate_property_tag_views.xml | 22 ++++++++++++++ estate/views/estate_property_type_views.xml | 23 +++++++++++++++ estate/views/estate_property_views.xml | 11 +++++++ 12 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c1e19b372fb..fdb4597c01b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -9,6 +9,9 @@ "data": [ "security/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offer_views.xml", "views/estate_menus.xml", ], "installable": True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..3683ff97b61 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,6 @@ -from . import estate_property +from . import ( + estate_property, + estate_property_offer, + estate_property_tag, + estate_property_type, +) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 414fd8afbc7..ee89f68d937 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -6,14 +6,6 @@ class EstateProperty(models.Model): _description = "Estate Property" name = fields.Char(string="Name", required=True) - property_type = fields.Selection( - [ - ("apartment", "Apartment"), - ("house", "House"), - ("villa", "Villa"), - ], - string="Property Type", - ) postcode = fields.Char(string="Postcode") available_from = fields.Date( string="Available From", @@ -55,3 +47,14 @@ class EstateProperty(models.Model): default="new", required=True, ) + + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + salesman_id = fields.Many2one( + "res.users", + string="Salesman", + index=True, + default=lambda self: self.env.user, + ) + buyer_id = fields.Many2one("res.partner", string="Buyer", index=True) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..327281cec33 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,18 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + + price = fields.Float(string="Price") + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + status = fields.Selection( + selection=[ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + string="Status", + copy=False, + ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..b18f46b839c --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + + name = fields.Char(string="Name", required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..1506088ca76 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + + name = fields.Char(string="Name", required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 0e11f47e58d..89f97c50842 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 1f84deb1b34..74326cb4029 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,9 @@ + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..cfa33bcf813 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,30 @@ + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + +
+
+
+ + + estate.property.offer.list + estate.property.offer + + + + + + + + +
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..4f2742df3f5 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,22 @@ + + + + estate.property.tag.form + estate.property.tag + +
+ + + + + +
+
+
+ + + 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..2279851746a --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,23 @@ + + + + + estate.property.type.form + estate.property.type + +
+ + + + + +
+
+
+ + + Property Types + estate.property.type + list,form + +
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 40788c69982..9a36b2f11e2 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -35,9 +35,11 @@

+ + @@ -60,6 +62,15 @@ + + + + + + + + + From cf4f2e6a0d9384c1b1cf60de322feb35d4e95307 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Wed, 21 Jan 2026 10:52:48 +0100 Subject: [PATCH 07/22] Chapter 8 commit --- estate/models/estate_property.py | 29 ++++++++++++++++++-- estate/models/estate_property_offer.py | 23 +++++++++++++++- estate/views/estate_property_offer_views.xml | 5 +++- estate/views/estate_property_views.xml | 1 + 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ee89f68d937..7442a81d4d1 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -15,7 +15,6 @@ class EstateProperty(models.Model): expected_price = fields.Float(string="Expected Price", required=True) selling_price = fields.Float(string="Selling Price", readonly=True) - best_offer = fields.Float(string="Best Offer") description = fields.Text(string="Description") bedrooms = fields.Integer(string="Bedrooms", default=2) @@ -33,7 +32,11 @@ class EstateProperty(models.Model): ], string="Garden Orientation", ) - total_area = fields.Integer(string="Total Area (sqm)") + total_area = fields.Integer( + string="Total Area (sqm)", + compute="_compute_total_area", + readonly=True, + ) active = fields.Boolean(string="Active", default=True) state = fields.Selection( [ @@ -58,3 +61,23 @@ class EstateProperty(models.Model): buyer_id = fields.Many2one("res.partner", string="Buyer", index=True) tag_ids = fields.Many2many("estate.property.tag", string="Tags") offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + best_offer = fields.Float(string="Best Offer", compute="_compute_best_offer") + + @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_offer(self): + for record in self: + record.best_offer = max(record.offer_ids.mapped("price"), default=0) + + @api.onchange("garden") + def _onchange_garden(self): + if not self.garden: + self.garden_area = 0 + self.garden_orientation = "" + else: + self.garden_area = 10 + self.garden_orientation = "north" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 327281cec33..1856a3f8c61 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -8,6 +8,13 @@ class EstatePropertyOffer(models.Model): price = fields.Float(string="Price") partner_id = fields.Many2one("res.partner", string="Partner", required=True) property_id = fields.Many2one("estate.property", string="Property", required=True) + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + store=True, + ) status = fields.Selection( selection=[ ("accepted", "Accepted"), @@ -16,3 +23,17 @@ class EstatePropertyOffer(models.Model): string="Status", copy=False, ) + + @api.depends("validity") + def _compute_date_deadline(self): + for record in self: + base_date = fields.Date.to_date(record.create_date) or fields.Date.today() + record.date_deadline = fields.Date.add( + base_date, + days=record.validity, + ) + + def _inverse_date_deadline(self): + for record in self: + base_date = fields.Date.to_date(record.create_date) or fields.Date.today() + record.validity = (record.date_deadline - base_date).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index cfa33bcf813..006201efe6c 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + + @@ -23,7 +25,8 @@ - + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9a36b2f11e2..e1a3c8fae13 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -45,6 +45,7 @@ + From 96ee9b6fa7a5501b5fb1bab2d20bc513235aef2a Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Wed, 21 Jan 2026 12:04:30 +0100 Subject: [PATCH 08/22] Chapter 9 --- estate/models/estate_property.py | 15 +++++++++++++++ estate/models/estate_property_offer.py | 18 ++++++++++++++++++ estate/views/estate_property_offer_views.xml | 3 +++ estate/views/estate_property_views.xml | 4 ++++ 4 files changed, 40 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 7442a81d4d1..afb92859da3 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class EstateProperty(models.Model): @@ -81,3 +82,17 @@ def _onchange_garden(self): else: self.garden_area = 10 self.garden_orientation = "north" + + def action_cancel(self): + for record in self: + if record.state == "sold": + message = "Sold properties cannot be canceled" + raise UserError(message) + record.state = "canceled" + + def action_sold(self): + for record in self: + if record.state == "canceled": + message = "Canceled properties cannot be sold" + raise UserError(message) + record.state = "sold" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 1856a3f8c61..26b502ce253 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): @@ -37,3 +38,20 @@ def _inverse_date_deadline(self): for record in self: base_date = fields.Date.to_date(record.create_date) or fields.Date.today() record.validity = (record.date_deadline - base_date).days + + def action_accept(self): + for record in self: + if record.property_id.state == "offer_accepted": + message = "Another offer already accepted" + raise UserError(message) + if record.property_id.state == "sold": + message = "Property is already sold" + raise UserError(message) + record.status = "accepted" + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + record.property_id.state = "offer_accepted" + + def action_refuse(self): + for record in self: + record.status = "refused" diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 006201efe6c..958ad9b2a69 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -27,6 +27,9 @@ + + + + + + + + + + + + + + + estate.property.type.list + estate.property.type + + + + + + + + Property Types estate.property.type diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 17aeb8a937c..004c8ebe0b5 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -10,8 +10,8 @@ - + @@ -24,8 +24,8 @@
-
@@ -37,12 +37,12 @@

- + - + @@ -67,7 +67,7 @@ - + @@ -85,15 +85,19 @@ estate.property.list estate.property - + + - + @@ -102,6 +106,7 @@ Estate Property estate.property list,form + {'search_default_available': True} From 2a17db60f6031a81a29e5dc80ad8e2b32a6dfe8f Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Thu, 22 Jan 2026 09:29:08 +0100 Subject: [PATCH 12/22] [IMP] estate : add different view for state button --- estate/views/estate_property_type_views.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index d1ca64484f2..56d796f2e21 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -1,11 +1,28 @@ + + estate.property.offer.list.stat + estate.property.offer + + + + + + + + + + + Offers estate.property.offer list,form [('property_type_id', '=', active_id)] + From 06405efcfb39ab1612fa15ee4a7b8a086c7a644b Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Thu, 22 Jan 2026 11:11:35 +0100 Subject: [PATCH 13/22] [IMP] estate : add inheirted view --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 7 +++++++ estate/models/estate_property_offer.py | 16 +++++++++++++++- estate/models/res_users.py | 12 ++++++++++++ estate/views/res_users_views.xml | 15 +++++++++++++++ 6 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index fdb4597c01b..7eaf4cde163 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -13,6 +13,7 @@ "views/estate_property_tag_views.xml", "views/estate_property_offer_views.xml", "views/estate_menus.xml", + "views/res_users_views.xml", ], "installable": True, "application": True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2fdf28b839d..031221b4eae 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_tag from . import estate_property_type from . import estate_property +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 72960db614f..8e4ba6558f0 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -76,6 +76,13 @@ class EstateProperty(models.Model): "Expected price must be positive", ) + @api.ondelete(at_uninstall=False) + def _unlink_if_state_is_new_or_canceled(self): + if any(state not in ("new", "canceled") for state in self.mapped("state")): + raise UserError( + "Only properties in 'New' or 'Canceled' state can be deleted.", + ) + @api.depends("living_area", "garden_area") def _compute_total_area(self): for record in self: diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index baa87101898..c7d218aa4fd 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,6 @@ from odoo import api, fields, models from odoo.exceptions import UserError +from odoo.tools import float_compare class EstatePropertyOffer(models.Model): @@ -11,7 +12,11 @@ class EstatePropertyOffer(models.Model): partner_id = fields.Many2one("res.partner", string="Partner", required=True) property_id = fields.Many2one("estate.property", string="Property", required=True) validity = fields.Integer(string="Validity (days)", default=7) - property_type_id = fields.Many2one(related="property_id.property_type_id", string="Property Type", store=True) + property_type_id = fields.Many2one( + related="property_id.property_type_id", + string="Property Type", + store=True, + ) date_deadline = fields.Date( string="Deadline", compute="_compute_date_deadline", @@ -32,6 +37,15 @@ class EstatePropertyOffer(models.Model): "Price must be positive", ) + @api.model + def create(self, vals): + for record in vals: + this_property = self.env["estate.property"].browse(record["property_id"]) + if float_compare(this_property.best_offer, record["price"], precision_digits=2) > 0: + raise UserError("Can't create offer with less price than best price.") + this_property.state = "offer_received" + return super().create(vals) + @api.depends("create_date", "validity") def _compute_date_deadline(self): for record in self: diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..20d3872ae62 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesman_id", + string="My Properties", + domain=[("state", "in", ("new", "offer_received"))], + ) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..0731b4c7945 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.form.inherit.estate + res.users + + + + + + + + + + From 2c6b078263c5e95b761e4f7aa70f713f5174e8eb Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Thu, 22 Jan 2026 13:50:37 +0100 Subject: [PATCH 14/22] [ADD] estate_account: auto-generate invoices when properties are sold Create a new link module 'estate_account' that depends on 'estate' and 'account'. Implement model inheritance for 'estate.property' to override 'action_sold': - Automatically create an 'account.move' (Customer Invoice) for the property buyer. - Add two invoice lines using the Command namespace: 1. Commission: 6% of the property selling price. 2. Administrative fees: Fixed amount of 100.00. --- estate/models/estate_property.py | 1 + estate_account/__init__.py | 1 + estate_account/__manifest__.py | 12 +++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 32 ++++++++++++++++++++++++ 5 files changed, 47 insertions(+) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 8e4ba6558f0..83b820e1b30 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -124,3 +124,4 @@ def action_sold(self): message = "Canceled properties cannot be sold" raise UserError(message) record.state = "sold" + return True 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..a42ce5d614f --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "Estate Account", + "version": "1.9", + "category": "Real Estate Accounting", + "summary": "Manage your real estate accounting", + "author": "Odoo", + "license": "LGPL-3", + "depends": ["estate", "account"], + "data": [], + "installable": True, + "application": True, +} 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..0e33cca635e --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,32 @@ +from odoo import models +from odoo.fields import Command + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold(self): + super().action_sold() + self.env["account.move"].create( + { + "move_type": "out_invoice", + "partner_id": self.buyer_id.id, + "invoice_line_ids": [ + Command.create( + { + "name": self.name, + "price_unit": self.selling_price * 0.06, + "quantity": 1, + }, + ), + Command.create( + { + "name": "Administrative Fees", + "price_unit": 100, + "quantity": 1, + }, + ), + ], + }, + ) + return True From 76b5284f2d48e2a9ed76e5f315c7c64ff0282592 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Thu, 22 Jan 2026 14:30:22 +0100 Subject: [PATCH 15/22] [IMP] estate: add kanban view Create a kanban view with name, selling price, expected price, best price and tag ids and group them by type id --- estate/views/estate_property_views.xml | 129 ++++++++++++++++--------- 1 file changed, 81 insertions(+), 48 deletions(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 004c8ebe0b5..edbe0e88fa1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,14 +6,16 @@ estate.property - - - - - - - - + + + + + + + + @@ -24,55 +26,62 @@
-
- +
-

- +

- +
- - - - + + + + - - - + + + - - - - - - - - - + + + + + + + + + - + - - - + + + @@ -86,26 +95,50 @@ estate.property - - - - - - - - - + decoration-success="state in ('offer_received', 'offer_accepted')" + decoration-bf="state == 'offer_accepted'" + decoration-muted="state == 'sold'"> + + + + + + + + + + + estate.property.kanban + estate.property + + + + + +
+ +
Expected Price:
+
Best Price:
+
Selling Price:
+ +
+
+
+
+
+
+ Estate Property estate.property - list,form + list,form,kanban {'search_default_available': True} From de85059f4a50015dbca97360ad5ce41162514c8d Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Mon, 26 Jan 2026 09:42:38 +0100 Subject: [PATCH 16/22] [ADD] awesome_owl: implement Todo List and core Owl components Complete Chapter 1 of the Owl training by building a reactive Todo application: - Implement 'Playground' and 'Counter' components using 'useState' for reactivity. - Create a reusable 'Card' component featuring 'slots', 'markup' for HTML, and props validation. - Build a 'TodoList' with 'TodoItem' sub-components using 't-foreach' and dynamic attributes. - Implement Todo logic: adding tasks via 'keyup', toggling completion, and deletion via callback props. - Utilize Owl hooks: 'onMounted' and 'useRef' for DOM access (autofocus), and custom 'useAutofocus' hook. --- awesome_owl/static/src/card/card.js | 15 +++ awesome_owl/static/src/card/card.xml | 13 +++ awesome_owl/static/src/counter/counter.js | 21 ++++ awesome_owl/static/src/counter/counter.xml | 10 ++ awesome_owl/static/src/main.js | 1 - awesome_owl/static/src/playground.css | 95 +++++++++++++++++++ awesome_owl/static/src/playground.js | 31 +++++- awesome_owl/static/src/playground.xml | 16 +++- awesome_owl/static/src/todo_list/todo_item.js | 20 ++++ .../static/src/todo_list/todo_item.xml | 20 ++++ awesome_owl/static/src/todo_list/todo_list.js | 36 +++++++ .../static/src/todo_list/todo_list.xml | 13 +++ awesome_owl/static/src/utils.js | 13 +++ 13 files changed, 299 insertions(+), 5 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/playground.css create mode 100644 awesome_owl/static/src/todo_list/todo_item.js create mode 100644 awesome_owl/static/src/todo_list/todo_item.xml create mode 100644 awesome_owl/static/src/todo_list/todo_list.js create mode 100644 awesome_owl/static/src/todo_list/todo_list.xml create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..947c062dc83 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,15 @@ +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: String, + slots: Object, + folded: Boolean, + toggleFold: Function, + }; + + toggleFold() { + this.props.toggleFold(); + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..bc36048faf6 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,13 @@ + + + + +
+
+
+ +
+ +
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..f835e1d7ec9 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,21 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + static props = { + count: { type: Number, optional: true }, + onChange: { type: Function, optional: false }, + }; + + setup() { + this.state = useState({ count: 0 }); + } + + increment() { + if (this.props.onChange) { + this.props.onChange(); + } else { + this.state.count++; + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..545f0cada94 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,10 @@ + + + + +
+
+ +
+
+
diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1aaea902b55..6c108687e29 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -9,4 +9,3 @@ const config = { // Mount the Playground component when the document.body is ready whenReady(() => mountComponent(Playground, document.body, config)); - diff --git a/awesome_owl/static/src/playground.css b/awesome_owl/static/src/playground.css new file mode 100644 index 00000000000..12b21d6a7ac --- /dev/null +++ b/awesome_owl/static/src/playground.css @@ -0,0 +1,95 @@ +:root { + --primary-gradient: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); + --glass-bg: rgba(255, 255, 255, 0.7); + --glass-border: rgba(255, 255, 255, 0.2); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +body { + background: #f3f4f6; + font-family: 'Inter', sans-serif; +} + +.playground-container { + max-width: 900px; + margin: 40px auto; + padding: 30px; + background: var(--glass-bg); + backdrop-filter: blur(10px); + border-radius: 24px; + border: 1px solid var(--glass-border); + box-shadow: var(--shadow-lg); +} + +.total-count-card { + background: var(--primary-gradient); + color: white; + padding: 20px 30px; + border-radius: 16px; + margin-bottom: 30px; + text-align: center; + box-shadow: var(--shadow-md); +} + +.total-count-card h1 { + font-size: 1.2rem; + text-transform: uppercase; + letter-spacing: 0.1em; + margin: 0; + opacity: 0.9; +} + +.total-count-card .display-1 { + font-weight: 800; + margin: 10px 0 0 0; +} + +.counters-grid { + display: flex; + gap: 20px; + flex-wrap: wrap; + justify-content: center; +} + +.counter-card { + background: white; + padding: 24px; + border-radius: 16px; + width: 260px; + text-align: center; + transition: transform 0.2s ease, box-shadow 0.2s ease; + border: 1px solid #e5e7eb; +} + +.counter-card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); +} + +.counter-value { + font-size: 2.5rem; + font-weight: 700; + color: #1f2937; + margin: 10px 0 20px 0; +} + +.btn-premium { + background: var(--primary-gradient); + border: none; + color: white; + padding: 12px 24px; + border-radius: 12px; + font-weight: 600; + transition: transform 0.1s active, opacity 0.2s; + width: 100%; +} + +.btn-premium:hover { + opacity: 0.9; + color: white; +} + +.btn-premium:active { + transform: scale(0.98); +} \ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..48cb7b50155 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,34 @@ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; +import { Card } from "./card/card"; +import { Counter } from "./counter/counter"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Card, Counter }; + + setup() { + this.state = useState({ + cards: [ + { id: 1, title: "Card 1", folded: false, count: 0 }, + { id: 2, title: "Card 2", folded: false, count: 0 }, + { id: 3, title: "Card 3", folded: false, count: 0 }, + ] + }); + } + + get totalCount() { + return this.state.cards.reduce((acc, card) => acc + card.count, 0); + } + + increment(card) { + card.count++; + } + + toggleFold(id) { + const card = this.state.cards.find((card) => card.id === id); + if (card) { + card.folded = !card.folded; + } + } + } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..bc5c55b5729 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,9 +1,19 @@ - + -
- hello world +
+ + + + + +
+ Total Score + + + +
diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js new file mode 100644 index 00000000000..0cdff6a6e66 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,20 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todo_item"; + static props = { + id: Number, + description: String, + isCompleted: Boolean, + toggleCompleted: Function, + removeTodo: Function, + }; + + toggleCompleted() { + this.props.toggleCompleted(this.props.id); + } + + removeTodo() { + this.props.removeTodo(this.props.id); + } +} diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml new file mode 100644 index 00000000000..73ddc886c84 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,20 @@ + + + +
+ +
+ + . +
+ + +
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..3d82677dc5f --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,36 @@ +import { Component, useState } from "@odoo/owl"; +import { useAutofocus } from "../utils"; +import {TodoItem} from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static components = { TodoItem }; + static counter = 0; + + setup() { + this.todos = useState([]); + this.state = useState({ newTodo: "" }); + this.inputRef = useAutofocus("input_focus"); + } + + addTodo(ev) { + if(ev.keyCode === 13 && this.state.newTodo.trim() !== "") { + this.todos.push({ id: TodoList.counter++, description: this.state.newTodo, isCompleted: false }); + this.state.newTodo = ""; + } + } + + toggleCompleted(id) { + const todo = this.todos.find((todo) => todo.id === id); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(id) { + const index = this.todos.findIndex(todo => todo.id === id); + if (index !== -1) { + this.todos.splice(index, 1); + } + } +} diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml new file mode 100644 index 00000000000..906244dcfc0 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,13 @@ + + + +
+ +
+
    + + + +
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..82e8309a8f5 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,13 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(name) { + const inputRef = useRef(name); + + onMounted(() => { + if (inputRef.el) { + inputRef.el.focus(); + } + }); + + return inputRef; +} From 80d83228025f846f4faeeaa13175f14d7a1db902 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Mon, 26 Jan 2026 09:48:09 +0100 Subject: [PATCH 17/22] [FIX] estate, estate_account Move all views of a model to its file, remove empty data array --- estate/views/estate_property_offer_views.xml | 25 ++++++++++++++++++++ estate/views/estate_property_type_views.xml | 24 ------------------- estate_account/__manifest__.py | 1 - 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index adf0c0d18ba..6755894879f 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,5 +1,30 @@ + + + estate.property.offer.list.stat + estate.property.offer + + + + + + + + + + + + + Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + estate.property.offer.form estate.property.offer diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 56d796f2e21..d69251de93b 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -1,29 +1,5 @@ - - - estate.property.offer.list.stat - estate.property.offer - - - - - - - - - - - - - Offers - estate.property.offer - list,form - [('property_type_id', '=', active_id)] - - estate.property.type.form diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index a42ce5d614f..c7faaa3d376 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -6,7 +6,6 @@ "author": "Odoo", "license": "LGPL-3", "depends": ["estate", "account"], - "data": [], "installable": True, "application": True, } From d7046bfcf9e81489614ae66cb386b0925d334ce1 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Mon, 26 Jan 2026 17:24:41 +0100 Subject: [PATCH 18/22] [ADD] awesome dashboard --- awesome_dashboard/__manifest__.py | 5 +- awesome_dashboard/static/src/dashboard.js | 8 --- awesome_dashboard/static/src/dashboard.xml | 8 --- .../src/dashboard/configuration_dialog.js | 30 ++++++++ .../src/dashboard/configuration_dialog.xml | 23 ++++++ .../static/src/dashboard/dashboard.css | 3 + .../static/src/dashboard/dashboard.js | 67 ++++++++++++++++++ .../static/src/dashboard/dashboard.xml | 32 +++++++++ .../dashboard_item/dashboard_item.js | 13 ++++ .../dashboard_item/dashboard_item.xml | 9 +++ .../static/src/dashboard/dashboard_items.js | 70 +++++++++++++++++++ .../src/dashboard/number_card/number_card.js | 9 +++ .../src/dashboard/number_card/number_card.xml | 11 +++ .../src/dashboard/pie_chart/pie_chart.js | 45 ++++++++++++ .../src/dashboard/pie_chart/pie_chart.xml | 8 +++ .../src/dashboard/statistics_service.js | 23 ++++++ .../static/src/dashboard_loader.js | 15 ++++ 17 files changed, 362 insertions(+), 17 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js delete mode 100644 awesome_dashboard/static/src/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/configuration_dialog.js create mode 100644 awesome_dashboard/static/src/dashboard/configuration_dialog.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.css create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml create mode 100644 awesome_dashboard/static/src/dashboard/statistics_service.js create mode 100644 awesome_dashboard/static/src/dashboard_loader.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..f7f58c7e769 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -23,8 +23,11 @@ ], 'assets': { 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + 'awesome_dashboard/static/src/dashboard_loader.js', ], + '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 c4fb245621b..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/configuration_dialog.js b/awesome_dashboard/static/src/dashboard/configuration_dialog.js new file mode 100644 index 00000000000..ef4508bd4be --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/configuration_dialog.js @@ -0,0 +1,30 @@ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { registry } from "@web/core/registry"; +import { browser } from "@web/core/browser/browser"; + +export class ConfigurationDialog extends Component { + static components = { Dialog }; + static template = "awesome_dashboard.ConfigurationDialog"; + + setup() { + const allItems = registry.category("awesome_dashboard").getAll(); + this.state = useState({ + items: allItems.map((item) => ({ + ...item, + isEnabled: !this.props.initialRemovedItems.includes(item.id), + })), + }); + } + + apply() { + const removedIds = this.state.items + .filter((i) => !i.isEnabled) + .map((i) => i.id); + + browser.localStorage.setItem("dashboard_removed_items", JSON.stringify(removedIds)); + + this.props.onApply(removedIds); + this.props.close(); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/configuration_dialog.xml b/awesome_dashboard/static/src/dashboard/configuration_dialog.xml new file mode 100644 index 00000000000..06208c85b9f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/configuration_dialog.xml @@ -0,0 +1,23 @@ + + + + +
+ +
+ + +
+
+
+ + + + +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.css b/awesome_dashboard/static/src/dashboard/dashboard.css new file mode 100644 index 00000000000..769fc1e72f9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.css @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: gray; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..ec01ec57ba0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,67 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; +import { useState } from "@odoo/owl"; +import { browser } from "@web/core/browser/browser"; +import { ConfigurationDialog } from "./configuration_dialog"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem }; + + setup() { + this.action = useService("action"); + this.statistics = useState(useService("statistics_service")); + this.dialogService = useService("dialog"); + const savedConfig = browser.localStorage.getItem("dashboard_removed_items"); + this.state = useState({ + removedItems: savedConfig ? JSON.parse(savedConfig) : [], + }); + } + + get items() { + return registry + .category("awesome_dashboard") + .getAll() + .filter((item) => !this.state.removedItems.includes(item.id)); + } + + openConfiguration() { + this.dialogService.add(ConfigurationDialog, { + initialRemovedItems: this.state.removedItems, + onApply: (newRemovedIds) => { + this.state.removedItems = newRemovedIds; + }, + }); + } + + openCustomers() { + this.action.doAction( + { + type: "ir.actions.act_window", + res_model: "res.partner", + name: "Partner Form", + view_mode: "kanban", + views: [[false, "kanban"]], + } + ); + } + + openLeads() { + this.action.doAction( + { + type: "ir.actions.act_window", + res_model: "crm.lead", + name: "Lead Form", + view_mode: "list,form", + views: [[false, "list"], [false, "form"]], + } + ); + } + + +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..4a69d68c74e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,32 @@ + + + + +
+ + + + + + + + + + + + + + + + + +
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..df90d4cd516 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,13 @@ +import { Component } from "@odoo/owl"; + + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static props = { + size: { type: Number, optional: true}, + slot: {type: Object, optional: true}, + }; + static defaultProps = { + size: 1, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..1e020421041 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,9 @@ + + + +
+ +
+
+
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..35148bb82f9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,70 @@ +import { registry } from "@web/core/registry"; +import { NumberCard } from "./number_card/number_card"; +import { PieChart } from "./pie_chart/pie_chart"; + +export const items = [ + { + id: "new_orders", + description: "Number of new orders", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "New Orders", + value: data.nb_new_orders, + }), + }, + { + id: "total_amount", + description: "Total amount of orders", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Total Amount", + value: `${data.total_amount} €`, + }), + }, + { + id: "avg_tshirt", + description: "Average number of t-shirts per order", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Avg T-Shirts/Order", + value: data.average_quantity, + }), + }, + { + id: "cancelled", + description: "Number of cancelled orders", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Cancelled Orders", + value: data.nb_cancelled_orders, + }), + }, + { + id: "avg_time", + description: "Average time from new to sent", + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Avg Time (New to Sent)", + value: `${data.average_time} Days`, + }), + }, + { + id: "pie_chart", + description: "Pie chart of shirts orders by size", + Component: PieChart, + size: 2, + props: (data) => ({ + title: "Pie Chart of shirts orders by size", + data: data.orders_by_size, + }), + }, +]; + +items.forEach(item => { + registry.category("awesome_dashboard").add(item.id, item); +}); diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..8eecf133e0c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: String, + value: Number, + }; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..ef9b6d83acc --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,11 @@ + + + +
+
+
+

+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js new file mode 100644 index 00000000000..cebf50b8a4b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,45 @@ +import { loadJS } from "@web/core/assets"; +import { Component, onWillStart, useRef, onMounted, onWillUnmount, useEffect } from "@odoo/owl"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + title: String, + data: Object, + }; + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js")); + onMounted(() => this.renderChart()); + onWillUnmount(() => this.chart.destroy()); + } + + renderChart() { + + if(this.chart) { + this.chart.destroy(); + } + + if(!this.props.data) { + return; + } + + const config = { + type: "pie", + data: { + labels: Object.keys(this.props.data), + datasets: [ + { + label: this.props.title, + data: Object.values(this.props.data), + }, + ], + }, + }; + + this.chart = new Chart(this.canvasRef.el, config); + + } + +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..2db41c02325 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,8 @@ + +
+
+ +
+ +
+
\ No newline at end of file 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..d974901ebe3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,23 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +const statisticsService = { + start(env) { + const statistics = reactive({}); + + async function loadStatistics() { + console.log("Loading statistics..."); + const data = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, data); + console.log("Statistics loaded:", statistics); + } + + const interval = setInterval(loadStatistics, 10 * 60 * 1000); + loadStatistics(); + + return statistics; + }, +}; + +registry.category("services").add("statistics_service", statisticsService); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..59ebe55f26c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,15 @@ +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +export class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); From 7b631fda9926847919ec8545db8d5960f3988311 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Mon, 26 Jan 2026 17:29:15 +0100 Subject: [PATCH 19/22] [FIX] awesome_owl, awesome_dashboard: add new line --- .../static/src/dashboard/configuration_dialog.js | 6 +++--- awesome_dashboard/static/src/dashboard/dashboard.css | 2 +- awesome_dashboard/static/src/dashboard/dashboard.xml | 2 +- .../static/src/dashboard/dashboard_item/dashboard_item.js | 4 ++-- .../static/src/dashboard/dashboard_item/dashboard_item.xml | 2 +- .../static/src/dashboard/number_card/number_card.js | 2 +- .../static/src/dashboard/number_card/number_card.xml | 2 +- .../static/src/dashboard/pie_chart/pie_chart.js | 6 +++--- .../static/src/dashboard/pie_chart/pie_chart.xml | 4 ++-- .../static/src/dashboard/statistics_service.js | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/awesome_dashboard/static/src/dashboard/configuration_dialog.js b/awesome_dashboard/static/src/dashboard/configuration_dialog.js index ef4508bd4be..4ae98a4bd97 100644 --- a/awesome_dashboard/static/src/dashboard/configuration_dialog.js +++ b/awesome_dashboard/static/src/dashboard/configuration_dialog.js @@ -21,10 +21,10 @@ export class ConfigurationDialog extends Component { const removedIds = this.state.items .filter((i) => !i.isEnabled) .map((i) => i.id); - + browser.localStorage.setItem("dashboard_removed_items", JSON.stringify(removedIds)); - + this.props.onApply(removedIds); this.props.close(); } -} \ No newline at end of file +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.css b/awesome_dashboard/static/src/dashboard/dashboard.css index 769fc1e72f9..32862ec0d82 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.css +++ b/awesome_dashboard/static/src/dashboard/dashboard.css @@ -1,3 +1,3 @@ .o_dashboard { background-color: gray; -} \ No newline at end of file +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml index 4a69d68c74e..bf57065564b 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -1,4 +1,4 @@ - + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js index df90d4cd516..b67f468fa17 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -4,8 +4,8 @@ import { Component } from "@odoo/owl"; export class DashboardItem extends Component { static template = "awesome_dashboard.DashboardItem"; static props = { - size: { type: Number, optional: true}, - slot: {type: Object, optional: true}, + size: { type: Number, optional: true }, + slot: { type: Object, optional: true }, }; static defaultProps = { size: 1, diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml index 1e020421041..748a5da1898 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -1,4 +1,4 @@ - +
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js index cebf50b8a4b..8504522b8b3 100644 --- a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -17,11 +17,11 @@ export class PieChart extends Component { renderChart() { - if(this.chart) { + if (this.chart) { this.chart.destroy(); } - if(!this.props.data) { + if (!this.props.data) { return; } @@ -42,4 +42,4 @@ export class PieChart extends Component { } -} \ No newline at end of file +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml index 2db41c02325..d6c515b3213 100644 --- a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -1,8 +1,8 @@
- +
-
\ No newline at end of file + diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js index d974901ebe3..a1e3a8267f5 100644 --- a/awesome_dashboard/static/src/dashboard/statistics_service.js +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -20,4 +20,4 @@ const statisticsService = { }, }; -registry.category("services").add("statistics_service", statisticsService); \ No newline at end of file +registry.category("services").add("statistics_service", statisticsService); From 0d96c0d80d6d3276be1d756f0c8c5661af6b1bfa Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Tue, 27 Jan 2026 13:17:26 +0100 Subject: [PATCH 20/22] [FIX] awesome_owl, awesome_dashboard: add new line --- awesome_owl/__init__.py | 4 +- awesome_owl/__manifest__.py | 54 ++++++++++++--------------- awesome_owl/static/src/card/card.xml | 2 +- awesome_owl/static/src/main.js | 2 +- awesome_owl/static/src/playground.css | 2 +- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/awesome_owl/__init__.py b/awesome_owl/__init__.py index 457bae27e11..e046e49fbe2 100644 --- a/awesome_owl/__init__.py +++ b/awesome_owl/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - -from . import controllers \ No newline at end of file +from . import controllers diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 55002ab81de..52b5287856c 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -1,43 +1,37 @@ -# -*- coding: utf-8 -*- { - 'name': "Awesome Owl", - - 'summary': """ + "name": "Awesome Owl", + "summary": """ Starting module for "Discover the JS framework, chapter 1: Owl components" """, - - 'description': """ + "description": """ Starting module for "Discover the JS framework, chapter 1: Owl components" """, - - 'author': "Odoo", - 'website': "https://www.odoo.com", - + "author": "Odoo", + "website": "https://www.odoo.com", # Categories can be used to filter modules in modules listing # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list - 'category': 'Tutorials', - 'version': '0.1', - + "category": "Tutorials", + "version": "0.1", # any module necessary for this one to work correctly - 'depends': ['base', 'web'], - 'application': True, - 'installable': True, - 'data': [ - 'views/templates.xml', + "depends": ["base", "web"], + "application": True, + "installable": True, + "data": [ + "views/templates.xml", ], - 'assets': { - 'awesome_owl.assets_playground': [ - ('include', 'web._assets_helpers'), - ('include', 'web._assets_backend_helpers'), - 'web/static/src/scss/pre_variables.scss', - 'web/static/lib/bootstrap/scss/_variables.scss', - 'web/static/lib/bootstrap/scss/_maps.scss', - ('include', 'web._assets_bootstrap'), - ('include', 'web._assets_core'), - 'web/static/src/libs/fontawesome/css/font-awesome.css', - 'awesome_owl/static/src/**/*', + "assets": { + "awesome_owl.assets_playground": [ + ("include", "web._assets_helpers"), + ("include", "web._assets_backend_helpers"), + "web/static/src/scss/pre_variables.scss", + "web/static/lib/bootstrap/scss/_variables.scss", + "web/static/lib/bootstrap/scss/_maps.scss", + ("include", "web._assets_bootstrap"), + ("include", "web._assets_core"), + "web/static/src/libs/fontawesome/css/font-awesome.css", + "awesome_owl/static/src/**/*", ], }, - 'license': 'AGPL-3' + "license": "AGPL-3", } diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index bc36048faf6..9c6de885fb4 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -1,4 +1,4 @@ - + diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 6c108687e29..dc156f424bc 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -4,7 +4,7 @@ import { Playground } from "./playground"; const config = { dev: true, - name: "Owl Tutorial" + name: "Owl Tutorial" }; // Mount the Playground component when the document.body is ready diff --git a/awesome_owl/static/src/playground.css b/awesome_owl/static/src/playground.css index 12b21d6a7ac..0e32c6602a0 100644 --- a/awesome_owl/static/src/playground.css +++ b/awesome_owl/static/src/playground.css @@ -92,4 +92,4 @@ body { .btn-premium:active { transform: scale(0.98); -} \ No newline at end of file +} From e462c50a6113b9603c6c2b4caa053ee462fae202 Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Wed, 28 Jan 2026 10:59:17 +0100 Subject: [PATCH 21/22] [IMP] estate, estate_account: add secuirty roles --- estate/__manifest__.py | 5 +-- estate/security/estate_security.xml | 44 ++++++++++++++++++++++++ estate/security/ir.model.access.csv | 12 ++++--- estate/views/estate_menus.xml | 2 +- estate_account/models/estate_property.py | 2 +- 5 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 estate/security/estate_security.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 7eaf4cde163..ea84c8e782e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,17 +1,18 @@ { "name": "Estate", "version": "1.9", - "category": "Real Estate", + "category": "Real Estate/Real Estate", "summary": "Manage your real estate properties", "author": "Odoo", "license": "LGPL-3", "depends": ["base"], "data": [ + "security/estate_security.xml", "security/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_offer_views.xml", "views/estate_property_type_views.xml", "views/estate_property_tag_views.xml", - "views/estate_property_offer_views.xml", "views/estate_menus.xml", "views/res_users_views.xml", ], diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..59b46721fb8 --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,44 @@ + + + + + Brokerage + 10 + + + + + Agent + + + + + + Manager + + + + + + Agent: Personal Properties Only + + + ['|', ('salesman_id', '=', user.id), ('salesman_id', '=', False)] + + + + Manager: See All Properties + + + [(1, '=', 1)] + + + + Personal Offers Only + + + [('property_id.salesman_id', '=', user.id)] + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 89f97c50842..e451642ac51 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,9 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 -access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 -access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 -access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 +access_estate_property_agent,estate.property,model_estate_property,estate.estate_group_user,1,1,1,1 +access_estate_property_manager,estate.property,model_estate_property,estate.estate_group_manager,1,1,1,1 +access_estate_property_offer_agent,estate.property.offer,model_estate_property_offer,estate.estate_group_user,1,1,1,1 +access_estate_property_offer_manager,estate.property.offer,model_estate_property_offer,estate.estate_group_manager,1,1,1,1 +access_estate_property_type_agent,estate.property.type,model_estate_property_type,estate.estate_group_user,1,0,0,0 +access_estate_property_type_manager,estate.property.type,model_estate_property_type,estate.estate_group_manager,1,1,1,1 +access_estate_property_tag_agent,estate.property.tag,model_estate_property_tag,estate.estate_group_user,1,0,0,0 +access_estate_property_tag_manager,estate.property.tag,model_estate_property_tag,estate.estate_group_manager,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 74326cb4029..6e34b0dbaba 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,7 +4,7 @@ - + diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 0e33cca635e..66c2c022076 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -7,7 +7,7 @@ class EstateProperty(models.Model): def action_sold(self): super().action_sold() - self.env["account.move"].create( + self.env["account.move"].sudo().create( { "move_type": "out_invoice", "partner_id": self.buyer_id.id, From 66cbb82bdabf44883330cc1c2b0011bec083325b Mon Sep 17 00:00:00 2001 From: Amr Elkhatieb Date: Wed, 28 Jan 2026 15:16:20 +0100 Subject: [PATCH 22/22] [IMP] estate: add test cases for estate module --- estate/models/estate_property.py | 5 +- estate/models/estate_property_offer.py | 10 +-- estate/tests/__init__.py | 1 + estate/tests/common.py | 54 ++++++++++++ estate/tests/test_estate_property.py | 86 ++++++++++++++++++++ estate/tests/test_offer_for_sold_property.py | 30 +++++++ 6 files changed, 178 insertions(+), 8 deletions(-) create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/common.py create mode 100644 estate/tests/test_estate_property.py create mode 100644 estate/tests/test_offer_for_sold_property.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 83b820e1b30..b42225250d7 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -121,7 +121,8 @@ def action_cancel(self): def action_sold(self): for record in self: if record.state == "canceled": - message = "Canceled properties cannot be sold" - raise UserError(message) + raise UserError("Canceled properties cannot be sold") + if record.state != "offer_accepted": + raise UserError("Property must have an accepted offer") record.state = "sold" return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index c7d218aa4fd..2bd7a3a66d2 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -43,6 +43,8 @@ def create(self, vals): this_property = self.env["estate.property"].browse(record["property_id"]) if float_compare(this_property.best_offer, record["price"], precision_digits=2) > 0: raise UserError("Can't create offer with less price than best price.") + if this_property.state in ["sold", "canceled", "offer_accepted"]: + raise UserError("Can't create offer for sold, accepted or canceled property.") this_property.state = "offer_received" return super().create(vals) @@ -62,12 +64,8 @@ def _inverse_date_deadline(self): def action_accept(self): for record in self: - if record.property_id.state == "offer_accepted": - message = "Another offer already accepted" - raise UserError(message) - if record.property_id.state == "sold": - message = "Property is already sold" - raise UserError(message) + if record.property_id.state in ["offer_accepted", "sold", "canceled"]: + raise UserError("Can't accept offer for sold, accepted or canceled property.") record.status = "accepted" record.property_id.selling_price = record.price record.property_id.buyer_id = record.partner_id diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..6aa28e97cc5 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_estate_property, test_offer_for_sold_property diff --git a/estate/tests/common.py b/estate/tests/common.py new file mode 100644 index 00000000000..715918f36fa --- /dev/null +++ b/estate/tests/common.py @@ -0,0 +1,54 @@ +from odoo.fields import Command +from odoo.tests.common import TransactionCase + + +class TestCommon(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.property_type = cls.env["estate.property.type"].create( + { + "name": "Residential", + }, + ) + + cls.property_tag = cls.env["estate.property.tag"].create( + { + "name": "Cozy", + }, + ) + + cls.buyer = cls.env["res.partner"].create( + { + "name": "John Doe", + }, + ) + + cls.salesman = cls.env["res.users"].create( + { + "name": "Jane Smith", + "login": "jane_smith", + "email": "jane.smith@example.com", + }, + ) + + cls.property = cls.env["estate.property"].create( + { + "name": "Initial House", + "description": "A very cozy house", + "postcode": "12345", + "expected_price": 100000.0, + "bedrooms": 3, + "living_area": 100, + "facades": 4, + "garage": True, + "garden": True, + "garden_area": 50, + "garden_orientation": "north", + "property_type_id": cls.property_type.id, + "tag_ids": [Command.link(cls.property_tag.id)], + "salesman_id": cls.salesman.id, + }, + ) diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..f5eb91e57c8 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,86 @@ +from psycopg2 import errors + +from odoo.exceptions import UserError +from odoo.tests import Form, tagged + +from .common import TestCommon + + +@tagged("post_install", "-at_install") +class TestEstateProperty(TestCommon): + def test_01_property_defaults(self): + property_test = self.env["estate.property"].create( + { + "name": "Test House", + "expected_price": 10000.0, + }, + ) + self.assertEqual(property_test.state, "new") + self.assertEqual(property_test.bedrooms, 2) + self.assertTrue(property_test.active) + + def test_02_total_area(self): + self.property.living_area = 100 + self.property.garden_area = 50 + self.assertEqual(self.property.total_area, 150) + + def test_03_garden_onchange(self): + with Form(self.env["estate.property"].with_context(default_property_type_id=self.property_type.id)) as prop_form: + prop_form.name = "Garden Test" + prop_form.expected_price = 10000.0 + prop_form.garden = True + self.assertEqual(prop_form.garden_area, 10) + self.assertEqual(prop_form.garden_orientation, "north") + + prop_form.garden = False + self.assertEqual(prop_form.garden_area, 0) + self.assertFalse(prop_form.garden_orientation) + + def test_04_expected_price_constraint(self): + """Test that expected price must be positive.""" + with self.assertRaises(errors.CheckViolation): + self.env["estate.property"].create( + { + "name": "Negative Price House", + "expected_price": -100.0, + }, + ) + + def test_05_selling_price_constraint(self): + self.property.expected_price = 100000.0 + with self.assertRaises( + UserError, + msg="Selling price must be at least 90% of the expected price", + ): + self.property.selling_price = 80000.0 + self.property._constrains_selling_price() + + def test_06_action_cancel(self): + self.property.action_cancel() + self.assertEqual(self.property.state, "canceled") + + self.property.state = "sold" + with self.assertRaises(UserError, msg="Sold properties cannot be canceled"): + self.property.action_cancel() + + def test_07_action_sold(self): + with self.assertRaises(UserError, msg="Property must have an accepted offer"): + self.property.action_sold() + + offer = self.env["estate.property.offer"].create( + { + "price": 95000.0, + "partner_id": self.buyer.id, + "property_id": self.property.id, + }, + ) + offer.action_accept() + self.assertEqual(self.property.state, "offer_accepted") + + self.property.action_sold() + self.assertEqual(self.property.state, "sold") + self.assertEqual(self.property.selling_price, 95000.0) + self.assertEqual(self.buyer.id, offer.partner_id.id) + self.property.state = "canceled" + with self.assertRaises(UserError, msg="Canceled properties cannot be sold"): + self.property.action_sold() diff --git a/estate/tests/test_offer_for_sold_property.py b/estate/tests/test_offer_for_sold_property.py new file mode 100644 index 00000000000..44acab74c9e --- /dev/null +++ b/estate/tests/test_offer_for_sold_property.py @@ -0,0 +1,30 @@ +from odoo.exceptions import UserError +from odoo.tests.common import tagged + +from .common import TestCommon + + +@tagged("post_install", "-at_install") +class TestOfferForSoldProperty(TestCommon): + def test_cant_create_offer_for_sold_property(self): + offer = self.env["estate.property.offer"].create( + { + "price": 95000.0, + "partner_id": self.buyer.id, + "property_id": self.property.id, + }, + ) + offer.action_accept() + self.property.action_sold() + self.assertEqual(self.property.state, "sold") + + with self.assertRaises( + UserError, msg="Can't create offer for sold, accepted or canceled property.", + ): + self.env["estate.property.offer"].create( + { + "price": 100000.0, + "partner_id": self.buyer.id, + "property_id": self.property.id, + }, + )