From 9efdf599a3905424a92ec324d48388d06dee5838 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Mon, 19 Jan 2026 16:10:52 +0100 Subject: [PATCH 01/21] First try --- awesome_clicker/__manifest__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py index 56dc2f779b9..c436012c188 100644 --- a/awesome_clicker/__manifest__.py +++ b/awesome_clicker/__manifest__.py @@ -25,5 +25,7 @@ ], }, - 'license': 'AGPL-3' + 'license': 'AGPL-3' + + } From b75db3bf87c265cc6e01a81d6a7fff822f8c2621 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Tue, 20 Jan 2026 09:31:23 +0100 Subject: [PATCH 02/21] [ADD] some elements added --- estate/__init__.py | 1 + estate/__manifest__.py | 14 ++++++++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 27 +++++++++++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..e625d42dc59 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,14 @@ +{ + "name": "Estate", + "version": "1.0", + "summary": "Real Estate Property Management", + "description": """ + Tutorial module for managing real estate properties. + """, + "author": "Your Name", + "category": "Tutorial", + "depends": ["base"], + "data": [], + "installable": True, + "application": True, +} \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..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..68236253574 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,27 @@ +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ] + ) From a29cdcec5779ae0172bba9972bf0e52f0dd60ea5 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Tue, 20 Jan 2026 09:59:04 +0100 Subject: [PATCH 03/21] [ADD] elements added 2 --- estate/__manifest__.py | 4 +++- estate/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index e625d42dc59..a7bbbee6483 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,7 +8,9 @@ "author": "Your Name", "category": "Tutorial", "depends": ["base"], - "data": [], + "data": [ + 'security/ir.model.access.csv' + ], "installable": True, "application": True, } \ 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..98f4671fb0d --- /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 From c3295cb218a53acc16d70a08c43bd3bd51d9e53a Mon Sep 17 00:00:00 2001 From: eesteerina <126086200+eesteerina@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:27:27 +0100 Subject: [PATCH 04/21] Update awesome_clicker/__manifest__.py Co-authored-by: Arthur Nanson --- awesome_clicker/__manifest__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py index c436012c188..4295043399b 100644 --- a/awesome_clicker/__manifest__.py +++ b/awesome_clicker/__manifest__.py @@ -26,6 +26,4 @@ }, 'license': 'AGPL-3' - - } From 0ef923a578f4eab38a5dd803d02502ce531e40a6 Mon Sep 17 00:00:00 2001 From: eesteerina <126086200+eesteerina@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:27:49 +0100 Subject: [PATCH 05/21] Update estate/models/estate_property.py Co-authored-by: Arthur Nanson --- estate/models/estate_property.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 68236253574..daa40ec4041 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -19,9 +19,9 @@ class EstateProperty(models.Model): garden_area = fields.Integer() garden_orientation = fields.Selection( selection=[ - ("north", "North"), - ("south", "South"), - ("east", "East"), - ("west", "West"), + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), ] ) From 34a537ff5a500023d4880f50b77729bd8d104174 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Tue, 20 Jan 2026 11:35:05 +0100 Subject: [PATCH 06/21] [ADD] estate: add menus and action for property model This addition will allow to make the newly created model usable through the Odoo frontend and to validate access rights and views integration. --- awesome_clicker/__manifest__.py | 3 ++- estate/__manifest__.py | 8 +++++--- estate/models/__init__.py | 2 +- estate/views/estate_menus.xml | 15 +++++++++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 5 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py index 4295043399b..7e15d9dd616 100644 --- a/awesome_clicker/__manifest__.py +++ b/awesome_clicker/__manifest__.py @@ -25,5 +25,6 @@ ], }, - 'license': 'AGPL-3' + 'license': 'AGPL-3' } + diff --git a/estate/__manifest__.py b/estate/__manifest__.py index a7bbbee6483..23bf37dbd3c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,5 +1,5 @@ { - "name": "Estate", + "name": "Real Estate", "version": "1.0", "summary": "Real Estate Property Management", "description": """ @@ -9,8 +9,10 @@ "category": "Tutorial", "depends": ["base"], "data": [ - 'security/ir.model.access.csv' + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml' ], "installable": True, "application": True, -} \ No newline at end of file +} 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/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..58c0f44d08f --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..085b6da603e --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate.property + list,form + + From e7ec16b0ee5e2fcab5555abd08b784c082814001 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Tue, 20 Jan 2026 13:40:50 +0100 Subject: [PATCH 07/21] [IMP] estate: add defaults and reserved fields to property model This commit enhances the estate.property model by introducing reserved fields and field attributes required in the UI. --- awesome_clicker/__manifest__.py | 4 +-- estate/models/estate_property.py | 42 +++++++++++++++++++++----- estate/views/estate_menus.xml | 16 +++++----- estate/views/estate_property_views.xml | 10 +++--- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py index 7e15d9dd616..3247ccdcc1a 100644 --- a/awesome_clicker/__manifest__.py +++ b/awesome_clicker/__manifest__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- { - 'name': "Awesome Clicker", + 'name': 'Awesome Clicker', 'summary': """ Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game" @@ -10,7 +10,7 @@ Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game" """, - 'author': "Odoo", + 'author': 'Odoo', 'website': "https://www.odoo.com/", 'category': 'Tutorials', 'version': '0.1', diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index daa40ec4041..1af948c7127 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,17 +1,30 @@ from odoo import fields, models +from dateutil.relativedelta import relativedelta class EstateProperty(models.Model): - _name = "estate.property" - _description = "Real Estate Property" + _name = 'estate.property' + _description = 'Real Estate Property' - name = fields.Char(required=True) + name = fields.Char( + required=True + ) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() - expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + date_availability = fields.Date( + copy=False, + default=lambda self: fields.Date.context_today(self) + 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() @@ -25,3 +38,18 @@ class EstateProperty(models.Model): ('west', 'West'), ] ) + state = fields.Selection( + [ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Cancelled'), + ], + required=True, + copy=False, + default='new', + ) + active = fields.Boolean( + default=True + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 58c0f44d08f..af396848582 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,15 +1,15 @@ - + - + - + - + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 085b6da603e..3f9f9e3e082 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,8 +1,8 @@ - + - - Properties - estate.property - list,form + + Properties + estate.property + list,form From ebfc72cf96a2d9c2216b2d5b6c2173ee2ab4e98c Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Tue, 20 Jan 2026 16:08:04 +0100 Subject: [PATCH 08/21] [IMP] estate: improve property views usability The default auto-generated views are not suitable for real usage. This change provides a clearer and more efficient UI for users by presenting the key information in a structured way and enabling faster retrieval of relevant properties via dedicated search options. --- estate/__manifest__.py | 1 + estate/views/estate_property_views.xml | 85 ++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 23bf37dbd3c..605dcc1136a 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -15,4 +15,5 @@ ], "installable": True, "application": True, + 'license': 'LGPL-3', } diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3f9f9e3e082..5449e21f84e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,89 @@ estate.property list,form + + + estate.property.view.list + estate.property + + + + + + + + + + + + + + + + estate.property.view.form + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.view.search + estate.property + + + + + + + + + + + + + + + + + From f3d41a72bc3da7e7dc9c071470f93ad56948e7e1 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Wed, 21 Jan 2026 11:38:55 +0100 Subject: [PATCH 09/21] [ADD] add property types, property tags and offers This addition will allow to have categories of types and tags for a property and track all the offers made for a specific property --- estate/__manifest__.py | 3 + estate/models/__init__.py | 3 + estate/models/estate_property.py | 26 +++-- estate/models/estate_property_offer.py | 17 +++ estate/models/estate_property_tag.py | 8 ++ estate/models/estate_property_type.py | 8 ++ estate/security/ir.model.access.csv | 3 + estate/views/estate_menus.xml | 16 ++- estate/views/estate_property_offer_views.xml | 27 +++++ estate/views/estate_property_tag_views.xml | 18 +++ estate/views/estate_property_type_views.xml | 19 ++++ estate/views/estate_property_views.xml | 111 +++++++++++-------- 12 files changed, 199 insertions(+), 60 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 605dcc1136a..43b00f6a841 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,6 +10,9 @@ "depends": ["base"], "data": [ 'security/ir.model.access.csv', + 'views/estate_property_offer_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_type_views.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml' ], diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 1af948c7127..93a102422ab 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -6,25 +6,19 @@ class EstateProperty(models.Model): _name = 'estate.property' _description = 'Real Estate Property' - name = fields.Char( - required=True - ) + name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() date_availability = fields.Date( copy=False, default=lambda self: fields.Date.context_today(self) + relativedelta(months=3), ) - expected_price = fields.Float( - required=True, - ) + expected_price = fields.Float(required=True) selling_price = fields.Float( readonly=True, copy=False ) - bedrooms = fields.Integer( - default=2, - ) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() @@ -50,6 +44,14 @@ class EstateProperty(models.Model): copy=False, default='new', ) - active = fields.Boolean( - default=True - ) + active = fields.Boolean(default=True) + + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + + buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) + + salesperson_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user) + + tag_ids = fields.Many2many('estate.property.tag', string='Property Tag') + + 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..85f37592db6 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,17 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer' + + partner_id = fields.Many2one('res.partner', string='Partner', required=True) + property_id = fields.Many2one('estate.property', string='Estate Property', required=True) + price = fields.Float() + status = fields.Selection( + copy=False, + selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ] + ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..fef8d256bae --- /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(required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..8e04ff22035 --- /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(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 98f4671fb0d..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 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index af396848582..4b9ce3396f3 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,12 +4,24 @@ - + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..6230583bdc8 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,27 @@ + + + + estate.property.offer.view.list + estate.property.offer + + + + + + + + + + + estate.property.offer.view.form + estate.property.offer + +
+ + + + + +
+ +
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..0dae4b4d00c --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,18 @@ + + + + Property Tag + estate.property.tag + list,form + + + + estate.property.tag.view.list + estate.property.tag + + + + + + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..460d054fe4c --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,19 @@ + + + + Property Type + estate.property.type + list,form + + + + estate.property.type.view.list + estate.property.type + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 5449e21f84e..9935db6dfc3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,61 +6,78 @@ list,form - - estate.property.view.list - estate.property - - - - - - - - - - + + estate.property.view.list + estate.property + + + + + + + + + + - - estate.property.view.form - estate.property - -
+ + estate.property.view.form + estate.property + + -
+

- +

+
+ +
+ - - + + - - + + - + - + - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -70,24 +87,26 @@ - - estate.property.view.search - estate.property - - - - - - - - + + estate.property.view.search + estate.property + + + + + + + + - + - + + + From ed9fb0b2f209bad0394cebc38da27e98d849ab02 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Wed, 21 Jan 2026 11:49:11 +0100 Subject: [PATCH 10/21] [IMP] corrections --- estate/__init__.py | 2 +- estate/models/estate_property.py | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 93a102422ab..98f2e3c51a4 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -9,15 +9,9 @@ class EstateProperty(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date( - copy=False, - default=lambda self: fields.Date.context_today(self) + relativedelta(months=3), - ) + date_availability = fields.Date(copy=False, default=lambda self: fields.Date.context_today(self) + relativedelta(months=3)) expected_price = fields.Float(required=True) - selling_price = fields.Float( - readonly=True, - copy=False - ) + selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() From a4130adcf80af4f2c777a3909c6a131540db7f13 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Wed, 21 Jan 2026 16:18:03 +0100 Subject: [PATCH 11/21] [IMP] Improved property with best price calculation, validity, deadline Onchange on validity and deadline will allow consistency between the two and the calculation of best price will simplify the procedure to user. --- estate/models/estate_property.py | 40 +++++++++++++++++++- estate/models/estate_property_offer.py | 20 +++++++++- estate/views/estate_property_offer_views.xml | 14 +++++-- estate/views/estate_property_views.xml | 8 ++++ 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 98f2e3c51a4..f415d89a3f6 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 from dateutil.relativedelta import relativedelta @@ -49,3 +49,41 @@ class EstateProperty(models.Model): tag_ids = fields.Many2many('estate.property.tag', string='Property Tag') offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + + total_area = fields.Integer(compute='_compute_total_area') + + best_price = fields.Float(compute='_compute_best_price') + + @api.depends('garden_area', 'total_area') + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends('offer_ids.price') + def _compute_best_price(self): + for record in self: + prices = record.offer_ids.mapped('price') + record.best_price = max(prices, default=0.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 + + def action_sold(self): + for record in self: + if record.state == 'canceled': + raise UserError('A canceled property cannot be set as sold.') + record.state = 'sold' + return True + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise UserError('A sold property cannot be canceled.') + record.state = 'canceled' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 85f37592db6..88ed974ef2b 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,5 @@ -from odoo import fields, models - +from odoo import api, fields, models +from datetime import timedelta class EstatePropertyOffer(models.Model): _name = 'estate.property.offer' @@ -15,3 +15,19 @@ class EstatePropertyOffer(models.Model): ('refused', 'Refused'), ] ) + + validity = fields.Integer(default=7) + date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline', store=True) + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for offer in self: + create_dt = offer.create_date if offer.create_date else fields.Datetime.now() + offer.date_deadline = (create_dt + timedelta(days=offer.validity)).date() + + + def _inverse_date_deadline(self): + for offer in self: + if offer.date_deadline: + create_date = offer.create_date.date() if offer.create_date else fields.Datetime.now() + offer.validity = (offer.date_deadline - create_date).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 6230583bdc8..0c70b6352e2 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -7,6 +7,8 @@ + + @@ -17,9 +19,15 @@ estate.property.offer - - - + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9935db6dfc3..fa5fb20892a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -19,6 +19,7 @@ + @@ -28,6 +29,11 @@ estate.property
+
+

@@ -46,6 +52,7 @@ + @@ -65,6 +72,7 @@ + From 60d1ea29b2c39c0857381b168b59ee6266eeebee Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Wed, 21 Jan 2026 17:23:55 +0100 Subject: [PATCH 12/21] [ADD] confirmation buttons and interactions added Now it is possible to have a flow for the events makeing an interaction between the property and the offer. --- estate/models/estate_property.py | 24 ++++++++++++- estate/models/estate_property_offer.py | 36 ++++++++++++++++++++ estate/views/estate_property_offer_views.xml | 2 ++ estate/views/estate_property_views.xml | 7 ++-- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f415d89a3f6..26141cf0e57 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ from odoo import api, fields, models from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError class EstateProperty(models.Model): @@ -85,5 +86,26 @@ def action_cancel(self): for record in self: if record.state == 'sold': raise UserError('A sold property cannot be canceled.') - record.state = 'canceled' + record.state = 'new' + return True + + def action_set_new(self): + for record in self: + if record.state in ('sold', 'canceled'): + raise UserError('You cannot move a sold/canceled property back to New.') + record.state = 'new' + return True + + def action_set_offer_received(self): + for record in self: + if record.state in ('sold', 'canceled'): + raise UserError('You cannot change the state of a sold/canceled property.') + record.state = 'offer_received' + return True + + def action_set_offer_accepted(self): + for record in self: + if record.state in ('sold', 'canceled'): + raise UserError('You cannot change the state of a sold/canceled property.') + record.state = 'offer_accepted' return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 88ed974ef2b..9fbeb853c2f 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 datetime import timedelta +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): _name = 'estate.property.offer' @@ -31,3 +32,38 @@ def _inverse_date_deadline(self): if offer.date_deadline: create_date = offer.create_date.date() if offer.create_date else fields.Datetime.now() offer.validity = (offer.date_deadline - create_date).days + + def action_confirm(self): + for offer in self: + if offer.property_id.state in ('offer_accepted', 'sold', 'canceled'): + raise UserError('A sold property cannot accept new offers.') + + offer.status = 'accepted' + + offer.property_id.write({ + 'buyer_id': offer.partner_id.id, + 'selling_price': offer.price, + 'state': 'offer_accepted', + }) + + return True + + def action_refuse(self): + for offer in self: + if offer.status == 'accepted': + raise UserError('You cannot refuse an accepted offer.') + + offer.status = 'refused' + + property_record = offer.property_id + + active_offers = property_record.offer_ids.filtered(lambda o: o.status in ('pending', 'accepted')) + + if not active_offers: + property_record.write({ + 'buyer_id': False, + 'selling_price': 0, + 'state': 'new', + }) + + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 0c70b6352e2..e538c6d92ef 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + +

+
+

+ +

+
+ + + + + + + + + + + + +
+
+
+ diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4ca2ffc81a4..d280df75a31 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,13 +4,14 @@ Properties estate.property list,form + {'search_default_available': 1} estate.property.view.list estate.property - + @@ -18,7 +19,7 @@ - + @@ -30,12 +31,9 @@
-
@@ -45,11 +43,12 @@
- +
+ @@ -73,15 +72,15 @@ - - + + - + @@ -107,7 +106,7 @@ - + From 9386f037359b96d0536bac2b8e5e3d8a6fc9d830 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Thu, 22 Jan 2026 17:14:50 +0100 Subject: [PATCH 15/21] [IMP] inheritance in some models Created some methods for model inheritance to improve for example the res.users model. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 12 ++++++++---- estate/models/estate_property_offer.py | 8 ++++++++ estate/models/estate_property_type.py | 2 +- estate/models/res_users.py | 6 ++++++ estate/views/estate_property_views.xml | 1 + estate/views/res_users_views.xml | 24 ++++++++++++++++++++++++ 8 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 43b00f6a841..98e1055a2a5 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,6 +10,7 @@ "depends": ["base"], "data": [ 'security/ir.model.access.csv', + 'views/res_users_views.xml', 'views/estate_property_offer_views.xml', 'views/estate_property_tag_views.xml', 'views/estate_property_type_views.xml', diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 8d60307a3e6..ee284d9cfb5 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ from odoo import api, fields, models from dateutil.relativedelta import relativedelta -from odoo.tools.float_utils import float_compare, float_is_zero +from odoo.tools.float_utils import float_is_zero from odoo.exceptions import UserError, ValidationError @@ -72,9 +72,7 @@ def _check_selling_price(self): for record in self: if float_is_zero(record.selling_price, precision_digits=2): continue - - min_price = record.expected_price * 0.9 - if float_compare(record.selling_price, min_price, precision_digits=2) < 0: + if record.selling_price < record.expected_price * 0.9: raise ValidationError('The selling price cannot be lower than 90% of the expected price.') @api.depends('garden_area', 'total_area') @@ -96,6 +94,12 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = False + + @api.ondelete(at_uninstall=False) + def _unlink_if_new_or_cancelled(self): + for record in self: + if record.state not in ('new', 'cancelled'): + raise UserError('You can only delete a property when its state is New or Cancelled.') def action_sold(self): for record in self: diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 5632e3bf042..3a59a8ee292 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -73,3 +73,11 @@ def action_refuse(self): }) return True + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals['property_id'] and vals['price'] is not None: + if vals['price'] < self.env['estate.property'].browse(vals['property_id']).best_price: + raise UserError('The offer must be higher than the existing offers.') + return super().create(vals_list) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 0aec82d4d36..5a080c26857 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -14,7 +14,7 @@ class EstatePropertyType(models.Model): 'The property type name must be unique.', ) - property_ids = fields.One2many('estate.property','property_type_id', string='Properties') + property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties') offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers') offer_count = fields.Integer(compute='_compute_offer_count', string='Offer Count') diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..8acea9ae096 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,6 @@ +from odoo import fields, models + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many('estate.property', 'salesperson_id', string='Properties', domain=[('state', 'in', ['new', 'offer_received'])]) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index d280df75a31..31dc3252ef8 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -15,6 +15,7 @@ + diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..83417593df4 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,24 @@ + + + + + res.users.view.form.inherit.estate.property + res.users + + + + + + + + + + + + + From daf3303ecd8138bf75cf4a525067248bc7df4415 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Mon, 26 Jan 2026 11:20:10 +0100 Subject: [PATCH 16/21] [ADD] New module added Added the link between real estate module and invoice module --- .vscode/launch.json | 34 +++++++++++++++++++++++++ estate/__manifest__.py | 22 ++++++++-------- estate/models/estate_property_offer.py | 1 + estate_account/__init__.py | 1 + estate_account/__manifest__.py | 12 +++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_account.py | 31 ++++++++++++++++++++++ 7 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_account.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..70d842430fd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Odoo", + "type": "debugpy", + "request": "launch", + "program": "/home/odoo/odoo-src/odoo/odoo-bin", + "console": "integratedTerminal", + "args": [ + "--addons-path=../odoo/addons/,../enterprise, ../tutorials", + //"--addons-path=odoo/addons/", + //"-u", + //"pos_loyalty", + //"-i", + //"point_of_sale", + "-d", + "rd-demo3", + "--dev=all", + "-u", "estate", + //"--test-tags", + //".test_10_refund_with_reference", + "--without-demo=False", + "--limit-time-cpu=99999", + "--limit-time-real=99999", + "--limit-request=10000" + ], + "variablePresentation": {} + } + ] +} diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 98e1055a2a5..8e2147d0582 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,14 +1,12 @@ { - "name": "Real Estate", - "version": "1.0", - "summary": "Real Estate Property Management", - "description": """ - Tutorial module for managing real estate properties. - """, - "author": "Your Name", - "category": "Tutorial", - "depends": ["base"], - "data": [ + 'name': 'Real Estate', + 'version': '1.0', + 'summary': 'Real Estate Property Management', + 'description': 'Tutorial module for managing real estate properties.', + 'author': 'Ester Andreetto', + 'category': 'Tutorial', + 'depends': ['base'], + 'data': [ 'security/ir.model.access.csv', 'views/res_users_views.xml', 'views/estate_property_offer_views.xml', @@ -17,7 +15,7 @@ 'views/estate_property_views.xml', 'views/estate_menus.xml' ], - "installable": True, - "application": True, + 'installable': True, + 'application': True, 'license': 'LGPL-3', } diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 3a59a8ee292..ea40224d930 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -80,4 +80,5 @@ def create(self, vals_list): if vals['property_id'] and vals['price'] is not None: if vals['price'] < self.env['estate.property'].browse(vals['property_id']).best_price: raise UserError('The offer must be higher than the existing offers.') + self.env['estate.property'].browse(vals['property_id']).state = 'offer_received' return super().create(vals_list) 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..d975ba0e958 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,12 @@ +{ + 'name': 'Accounting for Real Estate', + 'version': '1.0', + 'description': 'Link invoices between estate and account modules.', + 'author': 'Ester Andreetto', + 'category': 'Tutorial', + 'depends': ['estate', 'account'], + 'data': [], + 'installable': True, + 'application': True, + 'license': 'LGPL-3', +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..02b688798a3 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_account diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py new file mode 100644 index 00000000000..1faab52fe53 --- /dev/null +++ b/estate_account/models/estate_account.py @@ -0,0 +1,31 @@ +from odoo import models, Command +from odoo.exceptions import UserError + +class EstateAccount(models.Model): + _inherit = 'estate.property' + + def action_sold(self): + pippo = super().action_sold() + journal = self.env['account.journal'].search([('type', '=', 'sale')], limit=1) + if not journal: + raise UserError( + 'No Sales Journal found for your company. Create one in Invoicing > Configuration > Journals.') + + for record in self: + invoice = {'partner_id': record.buyer_id.id, + 'move_type': 'out_invoice', + 'journal_id': journal.id, + 'line_ids': [ + Command.create({ + 'name': record.name, + 'quantity': 1, + 'price_unit': record.selling_price * 0.06, + }), + Command.create({ + 'name': 'Fixed fees', + 'quantity': 1, + 'price_unit': 100.00, + }) + ]} + self.env['account.move'].create(invoice) + return pippo From 29789448a702e38c074ea39ffa34e28ca8aca554 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Mon, 26 Jan 2026 13:47:25 +0100 Subject: [PATCH 17/21] [IMP] Improved views and added kanban view To have a better visualization of the properties. --- estate/views/estate_property_views.xml | 49 ++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 31dc3252ef8..9f825e6c10d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,11 +1,5 @@ - - Properties - estate.property - list,form - {'search_default_available': 1} - estate.property.view.list @@ -118,6 +112,47 @@ - + + estate.property.kanban + estate.property + + + + + +
+ + + +
+ This is new! +
+
+
+ + +
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + + Properties + estate.property + list,form,kanban + {'search_default_available': 1} + + From 95968eea33188dff6f0fbfee637435132b13c1ef Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Mon, 26 Jan 2026 14:26:18 +0100 Subject: [PATCH 18/21] [IMP] Improve code quality To have a clearer code --- estate/models/estate_property.py | 15 ++------------- estate/views/estate_property_views.xml | 2 +- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ee284d9cfb5..30e7eda91cc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ -from odoo import api, fields, models from dateutil.relativedelta import relativedelta +from odoo import api, fields, models from odoo.tools.float_utils import float_is_zero from odoo.exceptions import UserError, ValidationError @@ -36,25 +36,14 @@ class EstateProperty(models.Model): ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('canceled', 'Cancelled'), - ], - required=True, - copy=False, - default='new', - ) + ], required=True, copy=False, default='new') active = fields.Boolean(default=True) - property_type_id = fields.Many2one('estate.property.type', string='Property Type') - buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) - salesperson_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user) - tag_ids = fields.Many2many('estate.property.tag', string='Property Tag') - offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') - total_area = fields.Integer(compute='_compute_total_area') - best_price = fields.Float(compute='_compute_best_price') _check_expected_price = models.Constraint( diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9f825e6c10d..85544ec7043 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -113,7 +113,7 @@
- + estate.property.kanban estate.property From 37aadb04e30dc1a6429173e4d4f5f94fd2664351 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Tue, 27 Jan 2026 16:27:47 +0100 Subject: [PATCH 19/21] [ADD] awesome_owl buttons Starting to look into owl components --- awesome_owl/static/src/card/card.js | 22 ++++++++++ awesome_owl/static/src/card/card.xml | 18 +++++++++ awesome_owl/static/src/counter/counter.js | 19 +++++++++ awesome_owl/static/src/counter/counter.xml | 11 +++++ awesome_owl/static/src/playground.js | 17 +++++++- awesome_owl/static/src/playground.xml | 14 +++++++ awesome_owl/static/src/todo_list/todo_item.js | 21 ++++++++++ .../static/src/todo_list/todo_item.xml | 16 ++++++++ awesome_owl/static/src/todo_list/todo_list.js | 40 +++++++++++++++++++ .../static/src/todo_list/todo_list.xml | 13 ++++++ awesome_owl/static/src/utils.js | 8 ++++ 11 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml create mode 100644 awesome_owl/static/src/todo_list/todo_item.js create mode 100644 awesome_owl/static/src/todo_list/todo_item.xml create mode 100644 awesome_owl/static/src/todo_list/todo_list.js create mode 100644 awesome_owl/static/src/todo_list/todo_list.xml create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..bb56e025531 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,22 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title : String, + slots: { + type: Object, + shape: { + default: true + }, + } + }; + + setup() { + this.state = useState({ isOpen: true }); + } + + toggleContent() { + this.state.isOpen = !this.state.isOpen; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..f473dfbc1a2 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,18 @@ + + + + +
+
+
+ + +
+

+ +

+
+
+
+ +
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..58ae29905b7 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,19 @@ +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(); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..114da93cb83 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,11 @@ + + + + +
+ Counter: + +
+
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..dddc5ec40b4 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,20 @@ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo_list/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + + static components = { Counter, Card, TodoList }; + + setup(){ + this.str1 = "
some content
"; + this.str2 = markup("
some content
"); + this.sum = useState({ value: 2 }); + } + + incrementSum() { + this.sum.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..25863a53b40 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -4,6 +4,20 @@
hello world + + +
The sum is:
+
+
+ + content of card 1 + + + + +
+
+
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..b2aa2eed7fa --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,21 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + + static props = { + todo: { type: Object, + shape: { id: Number, description: String, isCompleted: Boolean } + }, + toggleState: Function, + removeTodo: Function, + }; + + onChange() { + this.props.toggleState(this.props.todo.id); + } + + onRemove() { + this.props.removeTodo(this.props.todo.id); + } +} 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..5b8113dd3d1 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,16 @@ + + + + +
+ + + +
+
+ +
+ 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..8d8d768f954 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,40 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + + static components = { TodoItem }; + + setup() { + this.nextId=1; + this.todos = useState([]); + useAutofocus("input"); + } + + addTodo(ev){ + if (ev.keyCode === 13 && ev.target.value != ""){ + this.todos.push({ + id: this.nextId++, + description: ev.target.value, + isCompleted: false + }); + ev.target.value = ""; + } + } + + toggleTodo(todoId) { + const todo = this.todos.find((todo) => todo.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(todoId) { + const todoIndex = this.todos.findIndex((todo) => todo.id === todoId); + if (todoIndex >= 0) { + this.todos.splice(todoIndex, 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..f137197a1d2 --- /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..a9b70206b9f --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,8 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(refName) { + const ref = useRef(refName); + onMounted(() => { + ref.el.focus(); + }); +} From 6eac3344f8f9b9143ff80764e5d579c5658fdd47 Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Tue, 27 Jan 2026 17:27:20 +0100 Subject: [PATCH 20/21] [CLN] Code cleaned To follow guidelines --- .gitignore | 3 ++ .vscode/launch.json | 34 ------------------- awesome_owl/static/src/todo_list/todo_item.js | 5 +-- estate/models/estate_property_offer.py | 2 -- estate_account/models/estate_account.py | 3 +- 5 files changed, 7 insertions(+), 40 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index b6e47617de1..faa0263468d 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +#launcher +.vscode/ diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 70d842430fd..00000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python Debugger: Odoo", - "type": "debugpy", - "request": "launch", - "program": "/home/odoo/odoo-src/odoo/odoo-bin", - "console": "integratedTerminal", - "args": [ - "--addons-path=../odoo/addons/,../enterprise, ../tutorials", - //"--addons-path=odoo/addons/", - //"-u", - //"pos_loyalty", - //"-i", - //"point_of_sale", - "-d", - "rd-demo3", - "--dev=all", - "-u", "estate", - //"--test-tags", - //".test_10_refund_with_reference", - "--without-demo=False", - "--limit-time-cpu=99999", - "--limit-time-real=99999", - "--limit-request=10000" - ], - "variablePresentation": {} - } - ] -} diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js index b2aa2eed7fa..9780002b56e 100644 --- a/awesome_owl/static/src/todo_list/todo_item.js +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -4,8 +4,9 @@ export class TodoItem extends Component { static template = "awesome_owl.TodoItem"; static props = { - todo: { type: Object, - shape: { id: Number, description: String, isCompleted: Boolean } + todo: { + type: Object, + shape: { id: Number, description: String, isCompleted: Boolean } }, toggleState: Function, removeTodo: Function, diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index ea40224d930..bf09127b89d 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -60,9 +60,7 @@ def action_refuse(self): raise UserError('You cannot refuse an accepted offer.') offer.status = 'refused' - property_record = offer.property_id - active_offers = property_record.offer_ids.filtered(lambda o: o.status in ('pending', 'accepted')) if not active_offers: diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py index 1faab52fe53..a98127b96ae 100644 --- a/estate_account/models/estate_account.py +++ b/estate_account/models/estate_account.py @@ -5,7 +5,6 @@ class EstateAccount(models.Model): _inherit = 'estate.property' def action_sold(self): - pippo = super().action_sold() journal = self.env['account.journal'].search([('type', '=', 'sale')], limit=1) if not journal: raise UserError( @@ -28,4 +27,4 @@ def action_sold(self): }) ]} self.env['account.move'].create(invoice) - return pippo + return super().action_sold() From 63e836cc0215106b37dda2fcbdd0416dd2b78e1a Mon Sep 17 00:00:00 2001 From: Ester Andreetto Date: Thu, 29 Jan 2026 15:39:31 +0100 Subject: [PATCH 21/21] [ADD] Awesome_dashboard creation Create a dashboard to visualize data. --- awesome_dashboard/__manifest__.py | 4 + awesome_dashboard/static/src/dashboard.js | 8 -- awesome_dashboard/static/src/dashboard.xml | 8 -- .../static/src/dashboard/dashboard.js | 81 +++++++++++++++++++ .../static/src/dashboard/dashboard.scss | 3 + .../static/src/dashboard/dashboard.xml | 44 ++++++++++ .../dashboard_item/dashboard_item.js | 18 +++++ .../dashboard_item/dashboard_item.xml | 12 +++ .../static/src/dashboard/dashboard_items.js | 65 +++++++++++++++ .../src/dashboard/number_card/number_card.js | 13 +++ .../src/dashboard/number_card/number_card.xml | 9 +++ .../src/dashboard/pie_chart/pie_chart.js | 39 +++++++++ .../src/dashboard/pie_chart/pie_chart.xml | 12 +++ .../pie_chart_card/pie_chart_card.js | 15 ++++ .../pie_chart_card/pie_chart_card.xml | 7 ++ .../src/dashboard/statistics_service.js | 20 +++++ .../static/src/dashboard_loader.js | 10 +++ 17 files changed, 352 insertions(+), 16 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js delete mode 100644 awesome_dashboard/static/src/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_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/pie_chart_card/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.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..d31fe4b6c11 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -24,7 +24,11 @@ 'assets': { 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', + ('remove', 'awesome_dashboard/static/src/dashboard/**/*'), ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*' + ] }, 'license': 'AGPL-3' } diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index c4fb245621b..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..ef4f452889f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,81 @@ +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + + static components = { Layout, DashboardItem }; + + setup() { + this.action = useService("action"); + this.statistics = useState(useService("awesome_dashboard.statistics")); + this.items = registry.category("awesome_dashboard").getAll(); + this.dialog = useService("dialog"); + this.state = useState({disabledItems: browser.localStorage.getItem("disabledDashboardItems")?.split(",") || []}); + } + + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }) + } + + updateConfiguration(newDisabledItems) { + this.state.disabledItems = newDisabledItems; + } + + openCustomerView() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Lead", + res_model: "crm.lead", + views: [ + [false, "form"], + [false, "list"], + ] + }) + } +} + +class ConfigurationDialog extends Component { + static template = "awesome_dashboard.ConfigurationDialog"; + static components = { CheckBox, Dialog } + static props = [ "close", "items", "disabledItems", "onUpdateConfiguration" ]; + + setup() { + this.items = useState(this.props.items.map((item) => { + return { + ...item, + enabled: !this.props.disabledItems.includes(item.id), + } + })); + } + + done() { + this.props.close(); + } + + onChange(checked, changedItem) { + changedItem.enabled = checked; + const newDisabledItems = Object.values(this.items).filter( + (item) => !item.enabled + ).map((item) => item.id) + + browser.localStorage.setItem("disabledDashboardItems", newDisabledItems); + this.props.onUpdateConfiguration(newDisabledItems); + } +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..ef30a37cb18 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: paleturquoise; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..8439d143401 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + +
+ + + + + + +
+
+
+ + + + Which cards do you whish to see ? + + + + + + + + + + + +
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..6dd67d6a4e2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,18 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem" + static props = { + slots: { + type: Object, + shape: { + default: Object + }, + }, + size: { + type: Number, + default: 1, + optional: true, + }, + }; +} 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..a0ba8116392 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,12 @@ + + + + +
+
+ +
+
+
+ +
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..c7bdb4ed164 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,65 @@ +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; +import { registry } from "@web/core/registry"; + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity, + }) + }, + { + id: "average_time", + description: "Average time for an order", + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time, + }) + }, + { + id: "number_new_orders", + description: "New orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders, + }) + }, + { + id: "cancelled_orders", + description: "Cancelled orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders, + }) + }, + { + id: "amount_new_orders", + description: "amount orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount, + }) + }, + { + id: "pie_chart", + description: "Shirt orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "Shirt orders by size", + values: data.orders_by_size, + }) + } +] + +items.forEach(item => { + registry.category("awesome_dashboard").add(item.id, item); +}) diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..ca8b584a334 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,13 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { + type: String, + }, + value: { + type: Number, + } + } +} diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..3a0713623fa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,9 @@ + + + + +
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js new file mode 100644 index 00000000000..f78ac8a11ae --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,39 @@ +import { loadJS } from "@web/core/assets"; +import { Component, onWillStart, useRef, onMounted, onWillUnmount, onPatched } from "@odoo/owl"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + label: String, + data: Object, + }; + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"])); + + onMounted(() => {this.renderChart();}); + onPatched(() => { + this.chart.destroy(); + this.renderChart(); + }); + onWillUnmount(() => {this.chart.destroy();}); + } + + renderChart() { + const labels = Object.keys(this.props.data); + const data = Object.values(this.props.data); + this.chart = new Chart(this.canvasRef.el, { + type: "pie", + data: { + labels: labels, + datasets: [ + { + label: this.props.label, + data: data, + }, + ], + }, + }); + } +} 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..99711839c96 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,12 @@ + + + + +
+
+ +
+
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js new file mode 100644 index 00000000000..4629e018f60 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js @@ -0,0 +1,15 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "../pie_chart/pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PieChart } + static props = { + title: { + type: String, + }, + values: { + type: Object, + } + } +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml new file mode 100644 index 00000000000..58a6811c83a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,7 @@ + + + + + + + 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..67beafcd947 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,20 @@ +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; + +const statisticsService = { + start() { + const statistics = reactive({ isReady: false }); + + async function loadData() { + const updates = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, updates, { isReady: true }); + } + loadData(); + // setInterval(loadData, 5*1000); + + return statistics; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..7eed4bd16da --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,10 @@ +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; +import { Component, xml } from "@odoo/owl"; + +class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml``; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader);