From fc13383f984a813e1fdeababc7ab48ef1dc64bf5 Mon Sep 17 00:00:00 2001 From: odoo Date: Mon, 19 Jan 2026 15:25:39 +0100 Subject: [PATCH 01/18] [ADD] estate: chapter 1-5 --- estate/__init__.py | 1 + estate/__manifest__.py | 17 ++++++++++ estate/data/ir.model.access.csv | 2 ++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 47 ++++++++++++++++++++++++++ estate/views/estate_menu.xml | 8 +++++ estate/views/estate_property_views.xml | 8 +++++ 7 files changed, 84 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/data/ir.model.access.csv create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/views/estate_menu.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__init__.py b/estate/__init__.py 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..7fea31ab2e0 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': "Estate", + 'depends': [ + 'base' + ], + 'data':[ + 'data/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menu.xml' + ], + 'installable' : True, + 'application' : True +} \ No newline at end of file diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv new file mode 100644 index 00000000000..ab63520e22b --- /dev/null +++ b/estate/data/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/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..553f4828dd6 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,47 @@ +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property" + + name = fields.Char("Name", required=True) + description = fields.Text("Description") + postcode = fields.Char("Postcode") + date_availability = fields.Date( + "Date Availability", + copy=False, + default=lambda self: fields.Date.add(fields.Date.today(), months=3), + ) + expected_price = fields.Float("Expected Price", required=True) + selling_price = fields.Float("Selling Price", readonly=True, copy=False) + bedrooms = fields.Integer("Bedrooms", default=2) + garage = fields.Boolean("Garage") + garden = fields.Boolean("Garden") + garden_area = fields.Integer("Garden Area") + garden_orientation = fields.Selection( + string="Type", + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + ) + active= fields.Boolean("Active", default=True) + state=fields.Selection( + string="State", + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled"), + ], + default="new", + required=True, + copy=False + ) + _sql_constraints = [ + ("value", "CHECK(value >= 0)", "The value be negative."), + ] diff --git a/estate/views/estate_menu.xml b/estate/views/estate_menu.xml new file mode 100644 index 00000000000..9e427ef6c00 --- /dev/null +++ b/estate/views/estate_menu.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..085b6da603e --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate.property + list,form + + From 6e855267ec18c361548e95386de7e7ececb8bc2a Mon Sep 17 00:00:00 2001 From: TudorCalinCS Date: Tue, 20 Jan 2026 13:12:49 +0100 Subject: [PATCH 02/18] [FIX] estate: Update estate/models/estate_property.py Co-authored-by: plha-odoo --- estate/models/estate_property.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 553f4828dd6..5fb46b5cb29 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -28,8 +28,8 @@ class EstateProperty(models.Model): ("west", "West"), ], ) - active= fields.Boolean("Active", default=True) - state=fields.Selection( + active = fields.Boolean("Active", default=True) + state = fields.Selection( string="State", selection=[ ("new", "New"), From 8065ba31641370c7414cba43f79099c2f4820c77 Mon Sep 17 00:00:00 2001 From: TudorCalinCS Date: Tue, 20 Jan 2026 13:13:00 +0100 Subject: [PATCH 03/18] [FIX] estate: Update estate/__manifest__.py Co-authored-by: plha-odoo --- estate/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 7fea31ab2e0..df280a796b9 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,7 +7,7 @@ 'depends': [ 'base' ], - 'data':[ + 'data': [ 'data/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_menu.xml' From 21af683bbafe8ef00fcce8f5234f212b0ce2a60d Mon Sep 17 00:00:00 2001 From: TudorCalinCS Date: Tue, 20 Jan 2026 13:13:07 +0100 Subject: [PATCH 04/18] [FIX] estate: Update estate/__manifest__.py Co-authored-by: plha-odoo --- estate/__manifest__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index df280a796b9..ddd696aa936 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,6 +12,6 @@ 'views/estate_property_views.xml', 'views/estate_menu.xml' ], - 'installable' : True, - 'application' : True + 'installable': True, + 'application': True } \ No newline at end of file From 5daaf90029a87de9ca0b6398002adfea1e8a986d Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Tue, 20 Jan 2026 13:36:25 +0100 Subject: [PATCH 05/18] [IMP] estate: added list, form, and search views (CHAPTER 6) --- estate/__init__.py | 2 +- estate/__manifest__.py | 5 +++- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 4 ++- estate/views/estate_form_view.xml | 46 +++++++++++++++++++++++++++++ estate/views/estate_list_view.xml | 18 +++++++++++ estate/views/estate_menu.xml | 2 +- estate/views/estate_search_view.xml | 23 +++++++++++++++ 8 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 estate/views/estate_form_view.xml create mode 100644 estate/views/estate_list_view.xml create mode 100644 estate/views/estate_search_view.xml 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 ddd696aa936..ffae4833dea 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,7 +10,10 @@ 'data': [ 'data/ir.model.access.csv', 'views/estate_property_views.xml', - 'views/estate_menu.xml' + 'views/estate_menu.xml', + 'views/estate_list_view.xml', + 'views/estate_form_view.xml', + 'views/estate_search_view.xml', ], '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 5fb46b5cb29..238fcb19e47 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -16,6 +16,8 @@ class EstateProperty(models.Model): expected_price = fields.Float("Expected Price", required=True) selling_price = fields.Float("Selling Price", readonly=True, copy=False) bedrooms = fields.Integer("Bedrooms", default=2) + living_area = fields.Integer("Living Area (sqm)") + facades = fields.Integer("Facades") garage = fields.Boolean("Garage") garden = fields.Boolean("Garden") garden_area = fields.Integer("Garden Area") @@ -40,7 +42,7 @@ class EstateProperty(models.Model): ], default="new", required=True, - copy=False + copy=False, ) _sql_constraints = [ ("value", "CHECK(value >= 0)", "The value be negative."), diff --git a/estate/views/estate_form_view.xml b/estate/views/estate_form_view.xml new file mode 100644 index 00000000000..354938b003a --- /dev/null +++ b/estate/views/estate_form_view.xml @@ -0,0 +1,46 @@ + + + + estate.form.view + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/estate/views/estate_list_view.xml b/estate/views/estate_list_view.xml new file mode 100644 index 00000000000..afbbc91412e --- /dev/null +++ b/estate/views/estate_list_view.xml @@ -0,0 +1,18 @@ + + + + estate.list.view + estate.property + + + + + + + + + + + + + diff --git a/estate/views/estate_menu.xml b/estate/views/estate_menu.xml index 9e427ef6c00..0d8ae32f3c8 100644 --- a/estate/views/estate_menu.xml +++ b/estate/views/estate_menu.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/estate/views/estate_search_view.xml b/estate/views/estate_search_view.xml new file mode 100644 index 00000000000..e1c87cfa1f0 --- /dev/null +++ b/estate/views/estate_search_view.xml @@ -0,0 +1,23 @@ + + + + estate.search.view + estate.property + + + + + + + + + + + + + + + + + + From 773144246121d82d026242115fa5174b8eb2b347 Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Wed, 21 Jan 2026 10:07:15 +0100 Subject: [PATCH 06/18] [IMP] estate: added relations between models (CHAPTER 7) --- estate/__manifest__.py | 30 ++++++++++--------- estate/data/ir.model.access.csv | 5 +++- estate/models/__init__.py | 3 ++ estate/models/estate_property.py | 17 +++++++++++ estate/models/estate_property_offer.py | 17 +++++++++++ estate/models/estate_property_tag.py | 8 +++++ estate/models/estate_property_type.py | 8 +++++ estate/views/estate_form_view.xml | 11 +++++++ estate/views/estate_list_view.xml | 1 + estate/views/estate_menu.xml | 8 +++-- estate/views/estate_offer_form_view.xml | 18 +++++++++++ estate/views/estate_offer_list_view.xml | 14 +++++++++ estate/views/estate_search_view.xml | 1 + estate/views/estate_tag_views.xml | 8 +++++ estate/views/estate_type_views.xml | 8 +++++ ...te_property_views.xml => estate_views.xml} | 0 16 files changed, 140 insertions(+), 17 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_offer_form_view.xml create mode 100644 estate/views/estate_offer_list_view.xml create mode 100644 estate/views/estate_tag_views.xml create mode 100644 estate/views/estate_type_views.xml rename estate/views/{estate_property_views.xml => estate_views.xml} (100%) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index ffae4833dea..4f9c3d957e7 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,18 +3,20 @@ { - 'name': "Estate", - 'depends': [ - 'base' + "name": "Estate", + "depends": ["base"], + "data": [ + "data/ir.model.access.csv", + "views/estate_views.xml", + "views/estate_list_view.xml", + "views/estate_offer_list_view.xml", + "views/estate_offer_form_view.xml", + "views/estate_form_view.xml", + "views/estate_search_view.xml", + "views/estate_type_views.xml", + "views/estate_tag_views.xml", + "views/estate_menu.xml", ], - 'data': [ - 'data/ir.model.access.csv', - 'views/estate_property_views.xml', - 'views/estate_menu.xml', - 'views/estate_list_view.xml', - 'views/estate_form_view.xml', - 'views/estate_search_view.xml', - ], - 'installable': True, - 'application': True -} \ No newline at end of file + "installable": True, + "application": True, +} diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv index ab63520e22b..0c0b62b7fee 100644 --- a/estate/data/ir.model.access.csv +++ b/estate/data/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/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 238fcb19e47..d8bafc63e7d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -44,6 +44,23 @@ class EstateProperty(models.Model): required=True, copy=False, ) + 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="Tags") + offer_ids = fields.One2many( + "estate.property.offer", + "property_id", + string="Offers", + ) _sql_constraints = [ ("value", "CHECK(value >= 0)", "The value be negative."), ] diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..96c62abdab5 --- /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" + + price = fields.Float("Price") + status = fields.Selection( + string="Status", + selection=[ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + ) + partner_id = fields.Many2one("res.partner", string="Partner") + property_id = fields.Many2one("estate.property", string="Property") diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..3ebec85000c --- /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("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..54459e3c267 --- /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("Name", required=True) diff --git a/estate/views/estate_form_view.xml b/estate/views/estate_form_view.xml index 354938b003a..e605b054d50 100644 --- a/estate/views/estate_form_view.xml +++ b/estate/views/estate_form_view.xml @@ -13,6 +13,8 @@ + + @@ -36,6 +38,15 @@ + + + + + + + + + diff --git a/estate/views/estate_list_view.xml b/estate/views/estate_list_view.xml index afbbc91412e..ba6355b4797 100644 --- a/estate/views/estate_list_view.xml +++ b/estate/views/estate_list_view.xml @@ -7,6 +7,7 @@ + diff --git a/estate/views/estate_menu.xml b/estate/views/estate_menu.xml index 0d8ae32f3c8..9cb376c186a 100644 --- a/estate/views/estate_menu.xml +++ b/estate/views/estate_menu.xml @@ -1,8 +1,12 @@ - - + + + + + + diff --git a/estate/views/estate_offer_form_view.xml b/estate/views/estate_offer_form_view.xml new file mode 100644 index 00000000000..3aa70b17c51 --- /dev/null +++ b/estate/views/estate_offer_form_view.xml @@ -0,0 +1,18 @@ + + + + estate.offer.form.view + estate.property.offer + +
+ + + + + + + +
+
+
+
diff --git a/estate/views/estate_offer_list_view.xml b/estate/views/estate_offer_list_view.xml new file mode 100644 index 00000000000..b5621816e37 --- /dev/null +++ b/estate/views/estate_offer_list_view.xml @@ -0,0 +1,14 @@ + + + + estate.offer.list.view + estate.property.offer + + + + + + + + + diff --git a/estate/views/estate_search_view.xml b/estate/views/estate_search_view.xml index e1c87cfa1f0..15d6e07256f 100644 --- a/estate/views/estate_search_view.xml +++ b/estate/views/estate_search_view.xml @@ -7,6 +7,7 @@ + diff --git a/estate/views/estate_tag_views.xml b/estate/views/estate_tag_views.xml new file mode 100644 index 00000000000..0ec3dc550e0 --- /dev/null +++ b/estate/views/estate_tag_views.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_type_views.xml b/estate/views/estate_type_views.xml new file mode 100644 index 00000000000..6384061b8ee --- /dev/null +++ b/estate/views/estate_type_views.xml @@ -0,0 +1,8 @@ + + + + Property Types + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_views.xml similarity index 100% rename from estate/views/estate_property_views.xml rename to estate/views/estate_views.xml From 98144b24cd9525a4e9883aaa7725d6e5ffd4d4e2 Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Wed, 21 Jan 2026 14:21:00 +0100 Subject: [PATCH 07/18] [IMP] estate: add computet fields and onchanges (CHAPTER 8) Complete chapter 8 Fix naming for view fiels (estate_views.xml -> estate_view.xml) Remove unsupported '_sql_constraints' from estate_property model Add missing author and license keys in manifest to remove the warnings --- estate/__manifest__.py | 9 ++++--- estate/models/estate_property.py | 26 ++++++++++++++++--- estate/models/estate_property_offer.py | 20 ++++++++++++-- estate/views/estate_form_view.xml | 2 ++ estate/views/estate_offer_form_view.xml | 4 ++- estate/views/estate_offer_list_view.xml | 4 ++- ...tate_tag_views.xml => estate_tag_view.xml} | 0 ...te_type_views.xml => estate_type_view.xml} | 0 .../{estate_views.xml => estate_view.xml} | 0 9 files changed, 53 insertions(+), 12 deletions(-) rename estate/views/{estate_tag_views.xml => estate_tag_view.xml} (100%) rename estate/views/{estate_type_views.xml => estate_type_view.xml} (100%) rename estate/views/{estate_views.xml => estate_view.xml} (100%) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 4f9c3d957e7..aba83b33ad3 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. @@ -7,16 +6,18 @@ "depends": ["base"], "data": [ "data/ir.model.access.csv", - "views/estate_views.xml", + "views/estate_view.xml", "views/estate_list_view.xml", "views/estate_offer_list_view.xml", "views/estate_offer_form_view.xml", "views/estate_form_view.xml", "views/estate_search_view.xml", - "views/estate_type_views.xml", - "views/estate_tag_views.xml", + "views/estate_type_view.xml", + "views/estate_tag_view.xml", "views/estate_menu.xml", ], "installable": True, "application": True, + "author": "Tudor-Calin Panzaru (tupan)", + "license": "LGPL-3", } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d8bafc63e7d..3b1a1e89518 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): @@ -61,6 +61,24 @@ class EstateProperty(models.Model): "property_id", string="Offers", ) - _sql_constraints = [ - ("value", "CHECK(value >= 0)", "The value be negative."), - ] + total_area = fields.Integer("Total Area (sqm)", compute="_compute_total_area") + best_price = fields.Float("Best Offer", compute="_compute_best_price") + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped("price") or [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 = "" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 96c62abdab5..6c93c1820ae 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ -from odoo import fields, models +from datetime import timedelta +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -14,4 +15,19 @@ class EstatePropertyOffer(models.Model): ], ) partner_id = fields.Many2one("res.partner", string="Partner") - property_id = fields.Many2one("estate.property", string="Property") + property_id = fields.Many2one("estate.property", string="Property") + validity = fields.Integer("Validity (days)", default=7) + date_deadline = fields.Date( + "Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline" + ) + + @api.depends("validity", "create_date") + 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 = base_date + timedelta(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_form_view.xml b/estate/views/estate_form_view.xml index e605b054d50..a30ec9ca70d 100644 --- a/estate/views/estate_form_view.xml +++ b/estate/views/estate_form_view.xml @@ -20,6 +20,7 @@ + @@ -36,6 +37,7 @@ + diff --git a/estate/views/estate_offer_form_view.xml b/estate/views/estate_offer_form_view.xml index 3aa70b17c51..f79859bb83f 100644 --- a/estate/views/estate_offer_form_view.xml +++ b/estate/views/estate_offer_form_view.xml @@ -8,8 +8,10 @@ - + + + diff --git a/estate/views/estate_offer_list_view.xml b/estate/views/estate_offer_list_view.xml index b5621816e37..a3d22c23f6f 100644 --- a/estate/views/estate_offer_list_view.xml +++ b/estate/views/estate_offer_list_view.xml @@ -4,9 +4,11 @@ estate.offer.list.view estate.property.offer - + + + diff --git a/estate/views/estate_tag_views.xml b/estate/views/estate_tag_view.xml similarity index 100% rename from estate/views/estate_tag_views.xml rename to estate/views/estate_tag_view.xml diff --git a/estate/views/estate_type_views.xml b/estate/views/estate_type_view.xml similarity index 100% rename from estate/views/estate_type_views.xml rename to estate/views/estate_type_view.xml diff --git a/estate/views/estate_views.xml b/estate/views/estate_view.xml similarity index 100% rename from estate/views/estate_views.xml rename to estate/views/estate_view.xml From 236a46c0db4422675fccbe4bfbcfeacbaf1ed488 Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Wed, 21 Jan 2026 16:18:00 +0100 Subject: [PATCH 08/18] [IMP] estate: add buttons to accept/refuse offers and sold/cancel properties (CHAPTER 9) --- estate/models/estate_property.py | 13 +++++++++++++ estate/models/estate_property_offer.py | 13 +++++++++++++ estate/views/estate_form_view.xml | 5 +++++ estate/views/estate_offer_list_view.xml | 2 ++ 4 files changed, 33 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3b1a1e89518..6cae2add176 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): @@ -82,3 +83,15 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = "" + + def action_sold(self): + for record in self: + if record.state == "canceled": + raise UserError("You cannot sell a canceled property") + record.state = "sold" + + def action_cancel(self): + for record in self: + if record.state == "sold": + raise UserError("You cannot cancel a sold property") + record.state = "canceled" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 6c93c1820ae..8cb0a8adb64 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,6 @@ from datetime import timedelta from odoo import api, fields, models +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): @@ -31,3 +32,15 @@ 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: + # only one offer can be accepted + if record.property_id.offer_ids.filtered(lambda x: x.status == "accepted"): + raise UserError("Only one offer can be accepted") + record.status = "accepted" + record.property_id.selling_price = record.price + + def action_refuse(self): + for record in self: + record.status = "refused" diff --git a/estate/views/estate_form_view.xml b/estate/views/estate_form_view.xml index a30ec9ca70d..940c4e9bab5 100644 --- a/estate/views/estate_form_view.xml +++ b/estate/views/estate_form_view.xml @@ -5,6 +5,10 @@ estate.property
+
+

@@ -13,6 +17,7 @@

+ diff --git a/estate/views/estate_offer_list_view.xml b/estate/views/estate_offer_list_view.xml index a3d22c23f6f..086726f3cdc 100644 --- a/estate/views/estate_offer_list_view.xml +++ b/estate/views/estate_offer_list_view.xml @@ -9,6 +9,8 @@ + + + +
+

+ +

+
+ + + + + + + + + + + + + + + + +
+
+
+ + + + estate.property.type.list.view + estate.property.type + + + + + + + diff --git a/estate/views/estate_view.xml b/estate/views/estate_view.xml index 085b6da603e..004e8c0d660 100644 --- a/estate/views/estate_view.xml +++ b/estate/views/estate_view.xml @@ -4,5 +4,6 @@ Properties estate.property list,form + {'search_default_state': True} From 318b18b1d98f7574aee98c8ac7e20b0324fd50e7 Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Thu, 22 Jan 2026 15:41:58 +0100 Subject: [PATCH 11/18] [FIX] estate: fix runbot styling errors --- estate/models/estate_property.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 67e4256efde..fd8efa8f4a0 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError -from odoo.tools.float_utils import float_is_zero, float_compare +from odoo.tools.float_utils import float_compare class EstateProperty(models.Model): @@ -76,7 +76,7 @@ class EstateProperty(models.Model): "CHECK (selling_price > 0)", "A property selling price must be strictly positive", ) - + @api.depends("living_area", "garden_area") def _compute_total_area(self): for record in self: @@ -111,5 +111,5 @@ def action_cancel(self): @api.constrains("selling_price", "expected_price") def _check_selling_price(self): for record in self: - if float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0 : + if float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0: raise ValidationError("The selling price cannot be lower than 90% of the expected price!") From d91b111a5a41838e80d8cc7740191cc26559453b Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Thu, 22 Jan 2026 16:17:58 +0100 Subject: [PATCH 12/18] [IMP] estate: add inheritance and extension (CHAPTER 12) --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 42 ++++++++------------------ estate/models/estate_property_offer.py | 11 +++++++ estate/models/estate_property_tag.py | 5 +-- estate/models/estate_property_type.py | 5 +-- estate/models/res_users.py | 12 ++++++++ estate/views/estate_form_view.xml | 1 + estate/views/estate_type_view.xml | 4 --- estate/views/res_users_view.xml | 15 +++++++++ 10 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_view.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index aba83b33ad3..0cd474a58c4 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -14,6 +14,7 @@ "views/estate_search_view.xml", "views/estate_type_view.xml", "views/estate_tag_view.xml", + "views/res_users_view.xml", "views/estate_menu.xml", ], "installable": True, 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 fd8efa8f4a0..0a46e829f40 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -12,9 +12,7 @@ class EstateProperty(models.Model): description = fields.Text("Description") postcode = fields.Char("Postcode") date_availability = fields.Date( - "Date Availability", - copy=False, - default=lambda self: fields.Date.add(fields.Date.today(), months=3), + "Date Availability", copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3) ) expected_price = fields.Float("Expected Price", required=True) selling_price = fields.Float("Selling Price", readonly=True, copy=False) @@ -25,13 +23,7 @@ class EstateProperty(models.Model): garden = fields.Boolean("Garden") garden_area = fields.Integer("Garden Area") garden_orientation = fields.Selection( - string="Type", - selection=[ - ("north", "North"), - ("south", "South"), - ("east", "East"), - ("west", "West"), - ], + string="Type", selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")] ) active = fields.Boolean("Active", default=True) state = fields.Selection( @@ -48,33 +40,19 @@ class EstateProperty(models.Model): copy=False, ) 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, - ) + 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="Tags") - offer_ids = fields.One2many( - "estate.property.offer", - "property_id", - string="Offers", - ) + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") total_area = fields.Integer("Total Area (sqm)", compute="_compute_total_area") best_price = fields.Float("Best Offer", compute="_compute_best_price") _check_expected_price = models.Constraint( - "CHECK (expected_price > 0)", - "A property expected price must be strictly positive", + "CHECK (expected_price > 0)", "A property expected price must be strictly positive" ) _check_selling_price = models.Constraint( - "CHECK (selling_price > 0)", - "A property selling price must be strictly positive", + "CHECK (selling_price > 0)", "A property selling price must be strictly positive" ) @api.depends("living_area", "garden_area") @@ -113,3 +91,9 @@ def _check_selling_price(self): for record in self: if float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0: raise ValidationError("The selling price cannot be lower than 90% of the expected price!") + + @api.ondelete(at_uninstall=False) + def _check_state_before_deletion(self): + for record in self: + if record.state not in ["new", "canceled"]: + raise UserError("You cannot delete a property that is not 'New' or 'Canceled'!") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 42a0bcad384..4750815e807 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -56,3 +56,14 @@ def action_accept(self): def action_refuse(self): for record in self: record.status = "refused" + + @api.model + def create(self, vals_list): + for vals in vals_list: + property_record = self.env["estate.property"].browse(vals["property_id"]) + if vals.get("price") < property_record.best_price: + raise UserError( + "The offer price cannot be lower than the best offer price!" + ) + property_record.state = "offer_received" + return super().create(vals_list) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 1b378eb346b..15fe78bdc66 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -9,7 +9,4 @@ class EstatePropertyTag(models.Model): name = fields.Char("Name", required=True) color = fields.Integer("Color") - _unique_name = models.Constraint( - "UNIQUE (name)", - "A property tag name must be unique", - ) + _unique_name = models.Constraint("UNIQUE (name)", "A property tag name must be unique") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 9161fdd3640..d705d6acb8c 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -17,7 +17,4 @@ def _compute_offer_count(self): for record in self: record.offer_count = len(record.offer_ids) - _unique_name = models.Constraint( - "UNIQUE (name)", - "A property type name must be unique", - ) + _unique_name = models.Constraint("UNIQUE (name)", "A property type name must be unique") diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..dcd64824875 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesperson_id", + string="Real EstateProperties", + domain=[("state", "in", ["new", "offer_received"])], + ) diff --git a/estate/views/estate_form_view.xml b/estate/views/estate_form_view.xml index 0c5382cc96f..101978af2ec 100644 --- a/estate/views/estate_form_view.xml +++ b/estate/views/estate_form_view.xml @@ -63,3 +63,4 @@
+ \ No newline at end of file diff --git a/estate/views/estate_type_view.xml b/estate/views/estate_type_view.xml index 160c1ade4fb..ebe3443653e 100644 --- a/estate/views/estate_type_view.xml +++ b/estate/views/estate_type_view.xml @@ -24,10 +24,6 @@ - - - - diff --git a/estate/views/res_users_view.xml b/estate/views/res_users_view.xml new file mode 100644 index 00000000000..d34ba2d772b --- /dev/null +++ b/estate/views/res_users_view.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + From ae3a717ddcc3a762fda848b54e4a31b0e6f8c2f8 Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Fri, 23 Jan 2026 09:47:38 +0100 Subject: [PATCH 13/18] [ADD] estate_account: new module to create an invoice when a property is sold (CHAPTER 13) --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 11 +++++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 23 +++++++++++++++++++++++ 4 files changed, 36 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_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..e2e5e9217ec --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,11 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + "name": "Estate Account", + "depends": ["estate", "account"], + "installable": True, + "application": True, + "author": "Tudor-Calin Panzaru (tupan)", + "license": "LGPL-3", +} 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..50b0277bee7 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,23 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold(self): + for record in self: + self.env["account.move"].create( + [ + { + "partner_id": record.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create( + {"name": record.name, "quantity": 1, "price_unit": record.selling_price * 0.06} + ), + Command.create({"name": "Administrative fees", "quantity": 1, "price_unit": 100}), + ], + } + ] + ) + return super().action_sold() From e0041e1b9b3a3594936b8373d00dcfdc0e76be6d Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Fri, 23 Jan 2026 11:19:27 +0100 Subject: [PATCH 14/18] [IMP] estate: add kanban view (CHAPTER 14) [FIX] estate: fix bug - error for selling price lower than 90% of expected price when creating a new property (selling_price is zero) --- estate/__manifest__.py | 3 +- estate/models/estate_property.py | 7 +++-- estate/models/estate_property_offer.py | 26 ++++------------- estate/views/estate_kanban_view.xml | 40 ++++++++++++++++++++++++++ estate/views/estate_view.xml | 2 +- 5 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 estate/views/estate_kanban_view.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 0cd474a58c4..924143b6497 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,8 +6,8 @@ "depends": ["base"], "data": [ "data/ir.model.access.csv", - "views/estate_view.xml", "views/estate_list_view.xml", + "views/estate_kanban_view.xml", "views/estate_offer_list_view.xml", "views/estate_offer_form_view.xml", "views/estate_form_view.xml", @@ -15,6 +15,7 @@ "views/estate_type_view.xml", "views/estate_tag_view.xml", "views/res_users_view.xml", + "views/estate_view.xml", "views/estate_menu.xml", ], "installable": True, diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0a46e829f40..4b962d9b48f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError -from odoo.tools.float_utils import float_compare +from odoo.tools.float_utils import float_compare, float_is_zero class EstateProperty(models.Model): @@ -89,7 +89,10 @@ def action_cancel(self): @api.constrains("selling_price", "expected_price") def _check_selling_price(self): for record in self: - if float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0: + if ( + not float_is_zero(record.selling_price, precision_digits=2) + and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0 + ): raise ValidationError("The selling price cannot be lower than 90% of the expected price!") @api.ondelete(at_uninstall=False) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 4750815e807..d9cce3df469 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -9,30 +9,16 @@ class EstatePropertyOffer(models.Model): _order = "price desc" price = fields.Float("Price") - status = fields.Selection( - string="Status", - selection=[ - ("accepted", "Accepted"), - ("refused", "Refused"), - ], - ) + status = fields.Selection(string="Status", selection=[("accepted", "Accepted"), ("refused", "Refused")]) partner_id = fields.Many2one("res.partner", string="Partner") property_id = fields.Many2one("estate.property", string="Property") validity = fields.Integer("Validity (days)", default=7) - date_deadline = fields.Date( - "Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline" - ) + date_deadline = fields.Date("Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline") property_type_id = fields.Many2one( - "estate.property.type", - related="property_id.property_type_id", - string="Property Type", - store=True, + "estate.property.type", related="property_id.property_type_id", string="Property Type", store=True ) - _check_price = models.Constraint( - "CHECK (price > 0)", - "A property offer price must be strictly positive", - ) + _check_price = models.Constraint("CHECK (price > 0)", "A property offer price must be strictly positive") @api.depends("validity", "create_date") def _compute_date_deadline(self): @@ -62,8 +48,6 @@ def create(self, vals_list): for vals in vals_list: property_record = self.env["estate.property"].browse(vals["property_id"]) if vals.get("price") < property_record.best_price: - raise UserError( - "The offer price cannot be lower than the best offer price!" - ) + raise UserError("The offer price cannot be lower than the best offer price!") property_record.state = "offer_received" return super().create(vals_list) diff --git a/estate/views/estate_kanban_view.xml b/estate/views/estate_kanban_view.xml new file mode 100644 index 00000000000..8d0d08d5378 --- /dev/null +++ b/estate/views/estate_kanban_view.xml @@ -0,0 +1,40 @@ + + + + estate.kanban.view + estate.property + + + + + + + +
+
+ + + +
+
+ Expected Price: +
+ +
+ Best Offer: +
+ +
+ Selling Price: +
+ +
+ +
+
+
+
+
+
+
+
diff --git a/estate/views/estate_view.xml b/estate/views/estate_view.xml index 004e8c0d660..45febf9d10b 100644 --- a/estate/views/estate_view.xml +++ b/estate/views/estate_view.xml @@ -3,7 +3,7 @@ Properties estate.property - list,form + list,kanban,form {'search_default_state': True} From 6d8a6e249b0fbb23ec63e8df15e49c2378f7cc73 Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Fri, 23 Jan 2026 12:06:15 +0100 Subject: [PATCH 15/18] [IMP] estate: follow coding guidelines (CHAPTER 15) --- estate/__manifest__.py | 19 +-- estate/{data => security}/ir.model.access.csv | 0 estate/views/estate_form_view.xml | 66 -------- estate/views/estate_kanban_view.xml | 40 ----- estate/views/estate_list_view.xml | 20 --- .../{estate_menu.xml => estate_menus.xml} | 1 + estate/views/estate_offer_form_view.xml | 20 --- ...r_list_view.xml => estate_offer_views.xml} | 29 +++- estate/views/estate_search_view.xml | 24 --- ...tate_tag_view.xml => estate_tag_views.xml} | 0 ...te_type_view.xml => estate_type_views.xml} | 12 +- estate/views/estate_view.xml | 9 -- estate/views/estate_views.xml | 150 ++++++++++++++++++ ...res_users_view.xml => res_users_views.xml} | 0 14 files changed, 188 insertions(+), 202 deletions(-) rename estate/{data => security}/ir.model.access.csv (100%) delete mode 100644 estate/views/estate_form_view.xml delete mode 100644 estate/views/estate_kanban_view.xml delete mode 100644 estate/views/estate_list_view.xml rename estate/views/{estate_menu.xml => estate_menus.xml} (98%) delete mode 100644 estate/views/estate_offer_form_view.xml rename estate/views/{estate_offer_list_view.xml => estate_offer_views.xml} (63%) delete mode 100644 estate/views/estate_search_view.xml rename estate/views/{estate_tag_view.xml => estate_tag_views.xml} (100%) rename estate/views/{estate_type_view.xml => estate_type_views.xml} (100%) delete mode 100644 estate/views/estate_view.xml create mode 100644 estate/views/estate_views.xml rename estate/views/{res_users_view.xml => res_users_views.xml} (100%) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 924143b6497..fd4bbdad268 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,18 +5,13 @@ "name": "Estate", "depends": ["base"], "data": [ - "data/ir.model.access.csv", - "views/estate_list_view.xml", - "views/estate_kanban_view.xml", - "views/estate_offer_list_view.xml", - "views/estate_offer_form_view.xml", - "views/estate_form_view.xml", - "views/estate_search_view.xml", - "views/estate_type_view.xml", - "views/estate_tag_view.xml", - "views/res_users_view.xml", - "views/estate_view.xml", - "views/estate_menu.xml", + "security/ir.model.access.csv", + "views/estate_offer_views.xml", + "views/estate_tag_views.xml", + "views/estate_type_views.xml", + "views/estate_views.xml", + "views/res_users_views.xml", + "views/estate_menus.xml", ], "installable": True, "application": True, diff --git a/estate/data/ir.model.access.csv b/estate/security/ir.model.access.csv similarity index 100% rename from estate/data/ir.model.access.csv rename to estate/security/ir.model.access.csv diff --git a/estate/views/estate_form_view.xml b/estate/views/estate_form_view.xml deleted file mode 100644 index 101978af2ec..00000000000 --- a/estate/views/estate_form_view.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - estate.form.view - estate.property - -
-
-
- -
-

- -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- \ No newline at end of file diff --git a/estate/views/estate_kanban_view.xml b/estate/views/estate_kanban_view.xml deleted file mode 100644 index 8d0d08d5378..00000000000 --- a/estate/views/estate_kanban_view.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - estate.kanban.view - estate.property - - - - - - - -
-
- - - -
-
- Expected Price: -
- -
- Best Offer: -
- -
- Selling Price: -
- -
- -
-
-
-
-
-
-
-
diff --git a/estate/views/estate_list_view.xml b/estate/views/estate_list_view.xml deleted file mode 100644 index 54c49eb8fa4..00000000000 --- a/estate/views/estate_list_view.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - estate.list.view - estate.property - - - - - - - - - - - - - - - diff --git a/estate/views/estate_menu.xml b/estate/views/estate_menus.xml similarity index 98% rename from estate/views/estate_menu.xml rename to estate/views/estate_menus.xml index 9cb376c186a..a4c83c8c483 100644 --- a/estate/views/estate_menu.xml +++ b/estate/views/estate_menus.xml @@ -4,6 +4,7 @@ + diff --git a/estate/views/estate_offer_form_view.xml b/estate/views/estate_offer_form_view.xml deleted file mode 100644 index f79859bb83f..00000000000 --- a/estate/views/estate_offer_form_view.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - estate.offer.form.view - estate.property.offer - -
- - - - - - - - - -
-
-
-
diff --git a/estate/views/estate_offer_list_view.xml b/estate/views/estate_offer_views.xml similarity index 63% rename from estate/views/estate_offer_list_view.xml rename to estate/views/estate_offer_views.xml index b7324d6668c..1e8dbc32f17 100644 --- a/estate/views/estate_offer_list_view.xml +++ b/estate/views/estate_offer_views.xml @@ -1,10 +1,21 @@ - - Offers - estate.property.offer - list,form - [('property_type_id', '=', active_id)] + + estate.offer.form.view + estate.property.offer + +
+ + + + + + + + + +
+
@@ -21,4 +32,12 @@
+ + + + Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + diff --git a/estate/views/estate_search_view.xml b/estate/views/estate_search_view.xml deleted file mode 100644 index c63393ebc93..00000000000 --- a/estate/views/estate_search_view.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - estate.search.view - estate.property - - - - - - - - - - - - - - - - - - - diff --git a/estate/views/estate_tag_view.xml b/estate/views/estate_tag_views.xml similarity index 100% rename from estate/views/estate_tag_view.xml rename to estate/views/estate_tag_views.xml diff --git a/estate/views/estate_type_view.xml b/estate/views/estate_type_views.xml similarity index 100% rename from estate/views/estate_type_view.xml rename to estate/views/estate_type_views.xml index ebe3443653e..237d7a3f733 100644 --- a/estate/views/estate_type_view.xml +++ b/estate/views/estate_type_views.xml @@ -1,11 +1,5 @@ - - Property Types - estate.property.type - list,form - - estate.property.type.form.view estate.property.type @@ -50,4 +44,10 @@ + + + Property Types + estate.property.type + list,form + diff --git a/estate/views/estate_view.xml b/estate/views/estate_view.xml deleted file mode 100644 index 45febf9d10b..00000000000 --- a/estate/views/estate_view.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - Properties - estate.property - list,kanban,form - {'search_default_state': True} - - diff --git a/estate/views/estate_views.xml b/estate/views/estate_views.xml new file mode 100644 index 00000000000..29e1d69531d --- /dev/null +++ b/estate/views/estate_views.xml @@ -0,0 +1,150 @@ + + + + estate.form.view + estate.property + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.kanban.view + estate.property + + + + + + + +
+
+ + + +
+
+ Expected Price: +
+ +
+ Best Offer: +
+ +
+ Selling Price: +
+ +
+ +
+
+
+
+
+
+
+ + + estate.list.view + estate.property + + + + + + + + + + + + + + + + + estate.search.view + estate.property + + + + + + + + + + + + + + + + + + + + + Properties + estate.property + list,kanban,form + {'search_default_state': True} + +
diff --git a/estate/views/res_users_view.xml b/estate/views/res_users_views.xml similarity index 100% rename from estate/views/res_users_view.xml rename to estate/views/res_users_views.xml From 93daf46bbb7988138de1a9695274f2015547a708 Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Mon, 26 Jan 2026 17:12:17 +0100 Subject: [PATCH 16/18] [IMP] awesome_owl: add owl components (CHAPTER 1) --- awesome_owl/static/src/card.js | 17 ++++++++ awesome_owl/static/src/card.xml | 19 +++++++++ awesome_owl/static/src/counter.js | 18 ++++++++ awesome_owl/static/src/counter.xml | 10 +++++ awesome_owl/static/src/main.js | 3 +- awesome_owl/static/src/playground.js | 20 ++++++++- awesome_owl/static/src/playground.xml | 20 +++++++-- awesome_owl/static/src/todo_list/todo_item.js | 25 +++++++++++ .../static/src/todo_list/todo_item.xml | 11 +++++ awesome_owl/static/src/todo_list/todo_list.js | 41 +++++++++++++++++++ .../static/src/todo_list/todo_list.xml | 12 ++++++ awesome_owl/static/src/utils.js | 11 +++++ 12 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 awesome_owl/static/src/card.js create mode 100644 awesome_owl/static/src/card.xml create mode 100644 awesome_owl/static/src/counter.js create mode 100644 awesome_owl/static/src/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.js b/awesome_owl/static/src/card.js new file mode 100644 index 00000000000..e3415195c95 --- /dev/null +++ b/awesome_owl/static/src/card.js @@ -0,0 +1,17 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title: { type: String }, + slots: { type: Object, optional: true }, + }; + + setup() { + this.state = useState({ isOpen: true }); + } + + toggle() { + this.state.isOpen = !this.state.isOpen; + } +} diff --git a/awesome_owl/static/src/card.xml b/awesome_owl/static/src/card.xml new file mode 100644 index 00000000000..024b903b38d --- /dev/null +++ b/awesome_owl/static/src/card.xml @@ -0,0 +1,19 @@ + + + +
+
+
+
+ +
+ +
+ +
+
+
+
+
diff --git a/awesome_owl/static/src/counter.js b/awesome_owl/static/src/counter.js new file mode 100644 index 00000000000..6c45e2940c4 --- /dev/null +++ b/awesome_owl/static/src/counter.js @@ -0,0 +1,18 @@ +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: 0 }); + } + + increment() { + this.state.value++; + + if (this.props.onChange) { + this.props.onChange(); + } + } +} diff --git a/awesome_owl/static/src/counter.xml b/awesome_owl/static/src/counter.xml new file mode 100644 index 00000000000..c711b84482f --- /dev/null +++ b/awesome_owl/static/src/counter.xml @@ -0,0 +1,10 @@ + + + +
+ Counter: + + +
+
+
diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1aaea902b55..1af6c827e0b 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -4,9 +4,8 @@ import { Playground } from "./playground"; const config = { dev: true, - name: "Owl Tutorial" + name: "Owl Tutorial", }; // Mount the Playground component when the document.body is ready whenReady(() => mountComponent(Playground, document.body, config)); - diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..df6e9abf2fd 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,21 @@ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./counter"; +import { Card } from "./card"; +import { TodoList } from "./todo_list/todo_list"; export class Playground extends Component { - static template = "awesome_owl.playground"; + static components = { + Counter, + Card, + TodoList, + }; + static template = "awesome_owl.Playground"; + + setup() { + this.state = useState({ sum: 0 }); + } + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..d5cf1fa5dbb 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,9 +1,23 @@ - -
- hello world + +
+
+
+ + +
+

The sum is: +

+
+ +

Card 1 content

+
+ + + +
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..d2010abbcc6 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,25 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + static props = { + todo: { + type: Object, + shape: { + id: { type: Number }, + description: { type: String }, + isCompleted: { type: Boolean }, + }, + }, + toggleState: { type: Function }, + removeTodo: { type: Function }, + }; + + onChange() { + this.props.toggleState(this.props.todo.id); + } + + remove() { + 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..d0a5e5728c0 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,11 @@ + + + +
+ + + + +
+
+
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..0811a933bcb --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,41 @@ +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.todos = useState([]); + this.nextId = 1; + this.todoInput = useAutoFocus("todoInput"); + } + + addTodo(ev) { + if (ev.keyCode === 13) { + this.todos.push({ + id: this.nextId++, + description: ev.target.value, + isCompleted: false, + }); + ev.target.value = ""; + } + } + + toggleTodo(todoId) { + const todo = this.todos.find((t) => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(todoId) { + const index = this.todos.findIndex((t) => t.id === todoId); + if (index >= 0) { + 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..0c27106c82d --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,12 @@ + + + +
+

Todo List

+ +
+ +
+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..ba2a364f7d2 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,11 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutoFocus(name) { + const ref = useRef(name); + onMounted(() => { + if (ref.el) { + ref.el.focus(); + } + }); + return ref; +} From 78d39d13b96fc9c723ada46dab1d35616a1eb821 Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Wed, 28 Jan 2026 16:41:49 +0100 Subject: [PATCH 17/18] [IMP] awesome_dashboard: build a dashboard (CHAPTER 2) --- .gitignore | 2 + awesome_dashboard/__init__.py | 2 - awesome_dashboard/__manifest__.py | 41 ++++++------ awesome_dashboard/controllers/__init__.py | 4 +- awesome_dashboard/controllers/controllers.py | 28 ++++---- awesome_dashboard/static/src/dashboard.js | 8 --- awesome_dashboard/static/src/dashboard.xml | 8 --- .../src/dashboard/configuration_dashboard.js | 27 ++++++++ .../src/dashboard/configuration_dashboard.xml | 19 ++++++ .../static/src/dashboard/dahsboard.scss | 3 + .../static/src/dashboard/dashboard.js | 57 ++++++++++++++++ .../static/src/dashboard/dashboard.xml | 20 ++++++ .../static/src/dashboard/dashboard_item.js | 12 ++++ .../static/src/dashboard/dashboard_item.xml | 8 +++ .../static/src/dashboard/dashboard_items.js | 66 +++++++++++++++++++ .../static/src/dashboard/number_card.js | 9 +++ .../static/src/dashboard/number_card.xml | 9 +++ .../static/src/dashboard/pie_chart.js | 34 ++++++++++ .../static/src/dashboard/pie_chart.xml | 8 +++ .../static/src/dashboard/pie_chart_card.js | 13 ++++ .../static/src/dashboard/pie_chart_card.xml | 9 +++ .../static/src/dashboard_action.js | 15 +++++ .../static/src/statistics_service.js | 23 +++++++ 23 files changed, 368 insertions(+), 57 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_dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/configuration_dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dahsboard.scss 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.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card.xml create mode 100644 awesome_dashboard/static/src/dashboard_action.js create mode 100644 awesome_dashboard/static/src/statistics_service.js diff --git a/.gitignore b/.gitignore index b6e47617de1..dc80942f943 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,5 @@ dmypy.json # Pyre type checker .pyre/ + +ruff.toml diff --git a/awesome_dashboard/__init__.py b/awesome_dashboard/__init__.py index b0f26a9a602..e046e49fbe2 100644 --- a/awesome_dashboard/__init__.py +++ b/awesome_dashboard/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from . import controllers diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..ac09d85fb52 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -1,30 +1,27 @@ -# -*- coding: utf-8 -*- { - 'name': "Awesome Dashboard", - - 'summary': """ + "name": "Awesome Dashboard", + "summary": """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - - 'description': """ + "description": """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - - 'author': "Odoo", - 'website': "https://www.odoo.com/", - 'category': 'Tutorials', - 'version': '0.1', - 'application': True, - 'installable': True, - 'depends': ['base', 'web', 'mail', 'crm'], - - 'data': [ - 'views/views.xml', - ], - 'assets': { - 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + "author": "Odoo", + "website": "https://www.odoo.com/", + "category": "Tutorials", + "version": "0.1", + "application": True, + "installable": True, + "depends": ["base", "web", "mail", "crm"], + "data": ["views/views.xml"], + "assets": { + "web.assets_backend": [ + "awesome_dashboard/static/src/statistics_service.js", + "awesome_dashboard/static/src/dashboard_action.js", + "awesome_dashboard/static/src/dashboard/**/*.xml", + "awesome_dashboard/static/src/dashboard/**/*.scss", ], + "awesome_dashboard.dashboard": ["awesome_dashboard/static/src/dashboard/**/*.js"], }, - 'license': 'AGPL-3' + "license": "AGPL-3", } diff --git a/awesome_dashboard/controllers/__init__.py b/awesome_dashboard/controllers/__init__.py index 457bae27e11..e046e49fbe2 100644 --- a/awesome_dashboard/controllers/__init__.py +++ b/awesome_dashboard/controllers/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - -from . import controllers \ No newline at end of file +from . import controllers diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py index 05977d3bd7f..48ae74809c4 100644 --- a/awesome_dashboard/controllers/controllers.py +++ b/awesome_dashboard/controllers/controllers.py @@ -1,15 +1,13 @@ -# -*- coding: utf-8 -*- - import logging import random from odoo import http -from odoo.http import request logger = logging.getLogger(__name__) + class AwesomeDashboard(http.Controller): - @http.route('/awesome_dashboard/statistics', type='jsonrpc', auth='user') + @http.route("/awesome_dashboard/statistics", type="jsonrpc", auth="user") def get_statistics(self): """ Returns a dict of statistics about the orders: @@ -22,15 +20,17 @@ def get_statistics(self): """ return { - 'average_quantity': random.randint(4, 12), - 'average_time': random.randint(4, 123), - 'nb_cancelled_orders': random.randint(0, 50), - 'nb_new_orders': random.randint(10, 200), - 'orders_by_size': { - 'm': random.randint(0, 150), - 's': random.randint(0, 150), - 'xl': random.randint(0, 150), + "average_quantity": random.randint(4, 12), + "average_time": random.randint(4, 123), + "nb_cancelled_orders": random.randint(0, 50), + "nb_new_orders": random.randint(10, 200), + "orders_by_size": { + "xs": random.randint(0, 150), + "s": random.randint(0, 150), + "m": random.randint(0, 150), + "l": random.randint(0, 150), + "xl": random.randint(0, 150), + "xxl": random.randint(0, 150), }, - 'total_amount': random.randint(100, 1000) + "total_amount": random.randint(100, 1000), } - 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_dashboard.js b/awesome_dashboard/static/src/dashboard/configuration_dashboard.js new file mode 100644 index 00000000000..cc01e2a0752 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/configuration_dashboard.js @@ -0,0 +1,27 @@ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; + +export class ConfigurationDashboard extends Component { + static template = "awesome_dashboard.ConfigurationDashboard"; + static components = { + Dialog, + CheckBox, + }; + static props = ["close", "items", "disabledItems", "onApply"]; + + setup() { + this.items = useState( + this.props.items.map((item) => ({ + ...item, + enabled: !this.props.disabledItems.includes(item.id), + })) + ); + } + + onApply() { + const disabledItems = this.items.filter((item) => !item.enabled).map((item) => item.id); + this.props.onApply(disabledItems); + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/configuration_dashboard.xml b/awesome_dashboard/static/src/dashboard/configuration_dashboard.xml new file mode 100644 index 00000000000..2bf860f331c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/configuration_dashboard.xml @@ -0,0 +1,19 @@ + + + + +
+

Which cards do you wish to see?

+
+ + + +
+
+ + + + +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dahsboard.scss b/awesome_dashboard/static/src/dashboard/dahsboard.scss new file mode 100644 index 00000000000..979249193f7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dahsboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: #bd917a; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..bbabb4887be --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,57 @@ +import { Component, onWillStart, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item"; +import { browser } from "@web/core/browser/browser"; +import { ConfigurationDashboard } from "./configuration_dashboard"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { + Layout, + DashboardItem, + }; + + setup() { + this.action = useService("action"); + this.dialog = useService("dialog"); + this.statisticsService = useService("awesome_dashboard.statistics"); + this.statistics = useState(this.statisticsService.statistics); + + const storedConfig = browser.localStorage.getItem("disabledDashboardItems"); + this.state = useState({ + disabledItems: storedConfig ? JSON.parse(storedConfig) : [], + }); + } + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"], + ], + }); + } + get filteredItems() { + const allItems = registry.category("dashboard_items").getAll(); + return allItems.filter((item) => !this.state.disabledItems.includes(item.id)); + } + openConfiguration() { + this.dialog.add(ConfigurationDashboard, { + items: registry.category("dashboard_items").getAll(), + disabledItems: this.state.disabledItems, + onApply: (disabledItems) => { + this.state.disabledItems = disabledItems; + browser.localStorage.setItem("disabledDashboardItems", JSON.stringify(disabledItems)); + }, + }); + } +} + +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..858cda6207e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,20 @@ + + + + + + + + + +
+ + + + + + +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js new file mode 100644 index 00000000000..883d465e6a1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,12 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static props = { + slots: { type: Object }, + size: { type: Number, optional: true }, + }; + static defaultProps = { + size: 1, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item.xml new file mode 100644 index 00000000000..dae593ce3be --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.xml @@ -0,0 +1,8 @@ + + + +
+ +
+
+
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..a91fd41a972 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,66 @@ +import { NumberCard } from "./number_card"; +import { PieChartCard } from "./pie_chart_card"; +import { registry } from "@web/core/registry"; + +const dashboardItemsRegistry = registry.category("dashboard_items"); + +dashboardItemsRegistry.add("average_quantity", { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (stats) => ({ + title: "Average amount of t-shirts / order", + value: stats.average_quantity, + }), +}); + +dashboardItemsRegistry.add("average_time", { + id: "average_time", + description: "Average time for an order", + Component: NumberCard, + props: (stats) => ({ + title: "Average time for an order (hours)", + value: stats.average_time, + }), +}); + +dashboardItemsRegistry.add("total_amount", { + id: "total_amount", + description: "Total amount", + Component: NumberCard, + props: (stats) => ({ + title: "Total amount of new orders", + value: stats.total_amount, + }), +}); + +dashboardItemsRegistry.add("nb_cancelled_orders", { + id: "nb_cancelled_orders", + description: "Cancelled orders this month", + Component: NumberCard, + props: (stats) => ({ + title: "Number of cancelled orders", + value: stats.nb_cancelled_orders, + }), +}); + +dashboardItemsRegistry.add("nb_new_orders", { + id: "nb_new_orders", + description: "New orders", + Component: NumberCard, + props: (stats) => ({ + title: "Number of new orders", + value: stats.nb_new_orders, + }), +}); + +dashboardItemsRegistry.add("orders_by_size", { + id: "orders_by_size", + description: "Orders by size", + Component: PieChartCard, + size: 2, + props: (stats) => ({ + title: "Shirt orders by size", + data: stats.orders_by_size, + }), +}); diff --git a/awesome_dashboard/static/src/dashboard/number_card.js b/awesome_dashboard/static/src/dashboard/number_card.js new file mode 100644 index 00000000000..0d1ae8deadf --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/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, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card.xml new file mode 100644 index 00000000000..799154889b9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card.xml @@ -0,0 +1,9 @@ + + + +
+
+
+
+ + diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart.js new file mode 100644 index 00000000000..50b5b07b847 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart.js @@ -0,0 +1,34 @@ +import { Component, onWillStart, onMounted, useRef, onPatched, onWillUnmount } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + data: { type: Object }, + }; + + setup() { + this.canvaRef = useRef("canvas"); + onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js")); + onMounted(() => this.renderChart()); + onPatched(() => this.renderChart()); + onWillUnmount(() => this.chart.destroy()); + } + + renderChart() { + if (this.chart) { + this.chart.destroy(); + } + this.chart = new Chart(this.canvaRef.el, { + type: "pie", + data: { + labels: Object.keys(this.props.data), + datasets: [ + { + data: Object.values(this.props.data), + }, + ], + }, + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart.xml new file mode 100644 index 00000000000..8b56b05acb1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart.xml @@ -0,0 +1,8 @@ + + + +
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card.js new file mode 100644 index 00000000000..ab83d398227 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card.js @@ -0,0 +1,13 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "./pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { + PieChart, + }; + static props = { + title: { type: String }, + data: { type: Object }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card.xml new file mode 100644 index 00000000000..fb45abd2c42 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card.xml @@ -0,0 +1,9 @@ + + + +
+
+ +
+ + diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..a734f8c01b2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,15 @@ +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js new file mode 100644 index 00000000000..abd53215be4 --- /dev/null +++ b/awesome_dashboard/static/src/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"; + +export const statisticsService = { + dependencies: [], + start() { + const statistics = reactive({}); + async function _fetchStats() { + const data = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, data); + } + _fetchStats(); + + setInterval(_fetchStats, 5000); + + return { + statistics, + }; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); From ec93f7a4c52959d725b836cb320178ac48bb07b3 Mon Sep 17 00:00:00 2001 From: "Tudor-Calin Panzaru (tupan)" Date: Wed, 28 Jan 2026 16:55:10 +0100 Subject: [PATCH 18/18] [FIX] estate_account: fix styling on EstateProperty class --- estate_account/models/estate_property.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 50b0277bee7..cb30fa70d2a 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -7,17 +7,13 @@ class EstateProperty(models.Model): def action_sold(self): for record in self: self.env["account.move"].create( - [ - { - "partner_id": record.buyer_id.id, - "move_type": "out_invoice", - "invoice_line_ids": [ - Command.create( - {"name": record.name, "quantity": 1, "price_unit": record.selling_price * 0.06} - ), - Command.create({"name": "Administrative fees", "quantity": 1, "price_unit": 100}), - ], - } - ] + { + "partner_id": record.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create({"name": record.name, "quantity": 1, "price_unit": record.selling_price * 0.06}), + Command.create({"name": "Administrative fees", "quantity": 1, "price_unit": 100}), + ], + } ) return super().action_sold()