From e08edd378e18ffef300b708c87eb46272b423159 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Mon, 19 Jan 2026 16:18:27 +0100 Subject: [PATCH 01/29] Chapter 1 --- estate/__init__.py | 0 estate/__manifest__.py | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..72404102db4 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,6 @@ +{ + 'name' : 'Real Estate', + 'author' : 'zavan', + 'depends' : ['base'], + 'application' : True +} \ No newline at end of file From 5a6148368a68221e897691bc99444b5087429c73 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Mon, 19 Jan 2026 17:10:56 +0100 Subject: [PATCH 02/29] Chapter 3 & 4 --- estate/__init__.py | 1 + estate/__manifest__.py | 5 ++++- estate/models/__init__.py | 1 + estate/models/estate_property.py | 19 +++++++++++++++++++ estate/security/ir.model.access.csv | 2 ++ 5 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__init__.py b/estate/__init__.py index e69de29bb2d..9a7e03eded3 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 72404102db4..eb2f133147f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,5 +2,8 @@ 'name' : 'Real Estate', 'author' : 'zavan', 'depends' : ['base'], - 'application' : True + 'application' : True, + 'data': [ + 'security/ir.model.access.csv' + ] } \ 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..0e2ef8ce2c9 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,19 @@ +from odoo import 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.Selections(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) \ 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..53f174c7535 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access.estate.property,model_estate_propert,base.group_user,1,1,1,1 \ No newline at end of file From 5508240ba75b499d108283e4c2d4972e4f735e01 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Tue, 20 Jan 2026 08:19:19 +0100 Subject: [PATCH 03/29] [FIX] estate : Fixed bugs for chapter 3 Imported the right modules and fixed some typos. The estate_property table now shows up in PostgresSQL :D --- estate/__init__.py | 2 +- estate/__manifest__.py | 4 ++-- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 4 ++-- estate/security/ir.model.access.csv | 2 +- estate/views/estate_property_views.xml | 0 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 estate/views/estate_property_views.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 eb2f133147f..833e1366335 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,6 +4,6 @@ 'depends' : ['base'], 'application' : True, 'data': [ - 'security/ir.model.access.csv' + ] -} \ 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/models/estate_property.py b/estate/models/estate_property.py index 0e2ef8ce2c9..1ffb826e362 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import models +from odoo import fields, models class EstateProperty(models.Model) : _name = 'estate.property' @@ -16,4 +16,4 @@ class EstateProperty(models.Model) : garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() - garden_orientation = fields.Selections(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) \ No newline at end of file + garden_orientation = fields.Selection(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 53f174c7535..507d04b27f9 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access.estate.property,model_estate_propert,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,access.estate.property,model_estate_propert,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..e69de29bb2d From b659879ac033c2a1be2c986004e1712ce429633f Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Tue, 20 Jan 2026 08:28:37 +0100 Subject: [PATCH 04/29] [FIX] estate : Fixed bug for chapter 4 Fixed typo for in acces rights file --- estate/__manifest__.py | 2 +- estate/security/ir.model.access.csv | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 833e1366335..006208635b5 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,6 +4,6 @@ 'depends' : ['base'], 'application' : True, 'data': [ - + 'security/ir.model.access.csv', ] } diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 507d04b27f9..f0cdfdcac54 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access.estate.property,model_estate_propert,base.group_user,1,1,1,1 +access_estate_property,access.estate.property,model_estate_property,base.group_user,1,1,1,1 From 2edd4dd22311c64de1a4eb86ca429294e743e153 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Tue, 20 Jan 2026 09:40:21 +0100 Subject: [PATCH 05/29] [ADD] estate : Added chapter 5 features Made the basic menus, added the Properties action and edited the properties --- estate/__manifest__.py | 3 +++ estate/models/estate_property.py | 12 +++++++++--- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 10 ++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 estate/views/estate_menus.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 006208635b5..3c607b27b9e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,5 +5,8 @@ 'application' : True, 'data': [ 'security/ir.model.access.csv', + + 'views/estate_property_views.xml', + 'views/estate_menus.xml' ] } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 1ffb826e362..40a133c5fdc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,6 @@ from odoo import fields, models +from datetime import date +from dateutil.relativedelta import relativedelta class EstateProperty(models.Model) : _name = 'estate.property' @@ -7,13 +9,17 @@ class EstateProperty(models.Model) : name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date(copy=False, default=fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[('new', 'New'), ('offer-received', 'Offer Received'), ('offer-accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], + copy=False, default="new", required=True) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..add5fd03a2e --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e69de29bb2d..60d64fab523 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,10 @@ + + + + Estate Property + estate.property + list,form + + + + From b93b8d1ce45ca683fa57397870d8bed6d1c32868 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Tue, 20 Jan 2026 10:58:59 +0100 Subject: [PATCH 06/29] [ADD] estate : Added Chapter 6 features Created the list, form & search views --- estate/__manifest__.py | 1 - estate/models/estate_property.py | 2 +- estate/views/estate_property_views.xml | 70 +++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 3c607b27b9e..316cfc70ef0 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,7 +5,6 @@ 'application' : True, 'data': [ 'security/ir.model.access.csv', - 'views/estate_property_views.xml', 'views/estate_menus.xml' ] diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 40a133c5fdc..12022d8b72c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -9,7 +9,7 @@ class EstateProperty(models.Model) : name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date(copy=False, default=fields.Date.today() + relativedelta(months=3)) + date_availability = fields.Date("Available From", copy=False, default=fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 60d64fab523..93eddf0c9a6 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,10 +1,76 @@ + + estate.property.form + estate.property + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + Estate Property estate.property list,form
- - From 11cb4e86ab86003bc6ac31f88b465433400f939b Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Tue, 20 Jan 2026 12:04:31 +0100 Subject: [PATCH 07/29] [ADD] estate : Chapter 7 features Added all the models & the links --- estate/__manifest__.py | 4 ++++ estate/models/__init__.py | 3 +++ estate/models/estate_property.py | 5 +++++ estate/models/estate_property_offer.py | 10 ++++++++++ estate/models/estate_property_tag.py | 7 +++++++ estate/models/estate_property_type.py | 7 +++++++ estate/security/ir.model.access.csv | 3 +++ estate/views/estate_menus.xml | 4 ++++ estate/views/estate_property_offer_views.xml | 14 ++++++++++++++ estate/views/estate_property_tag_views.xml | 8 ++++++++ estate/views/estate_property_type_views.xml | 8 ++++++++ estate/views/estate_property_views.xml | 19 +++++++++++++++++++ 12 files changed, 92 insertions(+) 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 316cfc70ef0..cbfed873cc2 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -3,8 +3,12 @@ 'author' : 'zavan', 'depends' : ['base'], 'application' : True, + 'license': 'LGPL-3', 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_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 12022d8b72c..dbeb16d1f88 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -23,3 +23,8 @@ class EstateProperty(models.Model) : state = fields.Selection( selection=[('new', 'New'), ('offer-received', 'Offer Received'), ('offer-accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], copy=False, default="new", required=True) + property_type_id = fields.Many2one("estate.property.type", string="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") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..8bba92fc4c9 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,10 @@ +from odoo import fields, models + +class EstatePropertyOffer(models.Model) : + _name = "estate.property.offer" + _description = "Estate property offer" + + price = fields.Float() + status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) + partner_id = fields.Many2one("res.partner", required=True) + property_id = fields.Many2one("estate.property", required=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..b1c7ed227b6 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,7 @@ +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..0bc3414ab5a --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,7 @@ +from odoo import fields, models + +class EstatePropertyType(models.Model) : + _name = "estate.property.type" + _description = "Real 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 f0cdfdcac54..e20ec4dd90b 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink access_estate_property,access.estate.property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access.estate.property.type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access.estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access.estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index add5fd03a2e..1081c03c37f 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,9 @@ + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..c15d1097581 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,14 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..782f335b5d3 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + Estate Property Tag + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..2a8cacb6cdb --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,8 @@ + + + + Estate Property Type + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 93eddf0c9a6..fcddf953957 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,8 +6,12 @@
+ + + + @@ -29,6 +33,17 @@ + + + + + + + + + + +
@@ -41,6 +56,7 @@ + @@ -62,6 +78,9 @@ + + + From 1ddbf21c49bf8864e6f66fbcd0f5ddf92b8946b3 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 21 Jan 2026 07:38:06 +0100 Subject: [PATCH 08/29] [FIX] estate : styling fixes To comply with the CI/CD --- estate/__manifest__.py | 8 ++++---- estate/models/estate_property.py | 6 +++--- estate/models/estate_property_offer.py | 3 ++- estate/models/estate_property_tag.py | 3 ++- estate/models/estate_property_type.py | 3 ++- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index cbfed873cc2..21a4c570fd4 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,8 +1,8 @@ { - 'name' : 'Real Estate', - 'author' : 'zavan', - 'depends' : ['base'], - 'application' : True, + 'name': 'Real Estate', + 'author': 'zavan', + 'depends': ['base'], + 'application': True, 'license': 'LGPL-3', 'data': [ 'security/ir.model.access.csv', diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index dbeb16d1f88..725fbf828bc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,8 +1,8 @@ from odoo import fields, models -from datetime import date from dateutil.relativedelta import relativedelta -class EstateProperty(models.Model) : + +class EstateProperty(models.Model): _name = 'estate.property' _description = "Real estate property" @@ -25,6 +25,6 @@ class EstateProperty(models.Model) : copy=False, default="new", required=True) property_type_id = fields.Many2one("estate.property.type", string="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) + 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") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 8bba92fc4c9..9ec56f96207 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,7 @@ from odoo import fields, models -class EstatePropertyOffer(models.Model) : + +class EstatePropertyOffer(models.Model): _name = "estate.property.offer" _description = "Estate property offer" diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index b1c7ed227b6..bba6d535c87 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -1,6 +1,7 @@ from odoo import fields, models -class EstatePropertyTag(models.Model) : + +class EstatePropertyTag(models.Model): _name = "estate.property.tag" _description = "Estate property tag" diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 0bc3414ab5a..99e14afbde3 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,6 +1,7 @@ from odoo import fields, models -class EstatePropertyType(models.Model) : + +class EstatePropertyType(models.Model): _name = "estate.property.type" _description = "Real estate property type" From a243c065e26970bc846a2e6111543967423332a0 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 21 Jan 2026 08:41:58 +0100 Subject: [PATCH 09/29] [ADD] estate : Added chapter 8 features Compute, inverse & onchange --- estate/models/estate_property.py | 25 +++++++++++++++++++- estate/models/estate_property_offer.py | 20 +++++++++++++++- estate/views/estate_property_offer_views.xml | 2 ++ estate/views/estate_property_views.xml | 5 +++- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 725fbf828bc..24cc7cfc2fb 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 @@ -28,3 +28,26 @@ class EstateProperty(models.Model): 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") + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Float(string="Best Offer", compute="_compute_best_price") + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + + @api.depends("offer_ids") + def _compute_best_price(self): + for r in self: + r.best_price = max((offer.price for offer in r.offer_ids), default=0) + + + @api.onchange("garden") + def _onchange_garden(self): + if(self.garden): + self.garden_area = 10 + self.garden_orientation = "north" + else: + self.garden_area = None + self.garden_orientation = None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 9ec56f96207..867ff7eba7c 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 odoo import api, fields, models +from dateutil.relativedelta import relativedelta class EstatePropertyOffer(models.Model): @@ -9,3 +10,20 @@ class EstatePropertyOffer(models.Model): status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) partner_id = fields.Many2one("res.partner", required=True) property_id = fields.Many2one("estate.property", required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date(compute="_compute_deadline", inverse="_inverse_validity", readonly=False) + + + @api.depends("validity", "create_date") + def _compute_deadline(self): + for r in self: + if r.create_date is not None: + r.date_deadline = r.create_date.date() + relativedelta(days=r.validity) + + + def _inverse_validity(self): + for r in self: + if r.create_date is not None: + r.validity = (r.date_deadline - r.create_date.date()).days + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index c15d1097581..16a7834bcb2 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -7,6 +7,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index fcddf953957..ee5d7ed9080 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -7,16 +7,18 @@
+ - + + @@ -31,6 +33,7 @@ + From f987789d281d9b89288c2606362ac82e0726657a Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 21 Jan 2026 09:21:02 +0100 Subject: [PATCH 10/29] [FIX] estate : Offer creation fix Fixed a bug where creating a new offer was not possible due to create_date property sometimes being a bool --- estate/models/estate_property_offer.py | 11 +++++------ estate/views/estate_property_views.xml | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 867ff7eba7c..d92dfa51338 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 dateutil.relativedelta import relativedelta +import datetime class EstatePropertyOffer(models.Model): @@ -17,13 +18,11 @@ class EstatePropertyOffer(models.Model): @api.depends("validity", "create_date") def _compute_deadline(self): for r in self: - if r.create_date is not None: - r.date_deadline = r.create_date.date() + relativedelta(days=r.validity) + create = r.create_date.date() if isinstance(r.create_date, datetime.datetime) else fields.Date.today() + r.date_deadline = create + relativedelta(days=r.validity) def _inverse_validity(self): for r in self: - if r.create_date is not None: - r.validity = (r.date_deadline - r.create_date.date()).days - - + create = r.create_date.date() if isinstance(r.create_date, datetime.datetime) else fields.Date.today() + r.validity = (r.date_deadline - create).days diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index ee5d7ed9080..d4534679f28 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,6 +5,10 @@ estate.property +
+
From fce113e733b7b701ecde084d3f4cd1bbcad98728 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 21 Jan 2026 10:10:22 +0100 Subject: [PATCH 11/29] [ADD] estate : Added chapter 9 features Accept & Refuse offers + Sell & Cancel properties --- estate/models/estate_property.py | 27 +++++++++++++++++-- estate/models/estate_property_offer.py | 28 ++++++++++++++++++-- estate/views/estate_property_offer_views.xml | 20 ++++++++++++++ estate/views/estate_property_views.xml | 3 ++- 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 24cc7cfc2fb..96cb6efcfd4 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models +from odoo import api, fields, models, exceptions from dateutil.relativedelta import relativedelta @@ -24,12 +24,13 @@ class EstateProperty(models.Model): selection=[('new', 'New'), ('offer-received', 'Offer Received'), ('offer-accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], copy=False, default="new", required=True) property_type_id = fields.Many2one("estate.property.type", string="Type") - buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False, readonly=True) 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") total_area = fields.Integer(compute="_compute_total_area") best_price = fields.Float(string="Best Offer", compute="_compute_best_price") + state = fields.Selection(default="new", readonly=True, selection=[('new', 'New'), ('sold', 'Sold'), ('cancel', 'Cancelled')]) @api.depends("garden_area", "living_area") def _compute_total_area(self): @@ -51,3 +52,25 @@ def _onchange_garden(self): else: self.garden_area = None self.garden_orientation = None + + + def sell_property(self): + for r in self: + if(self.state == 'sold'): + continue + elif(self.state == 'cancel'): + raise exceptions.UserError('Cannot sell a cancelled property') + else: + r.state = 'sold' + return True + + def cancel_property(self): + for r in self: + if(self.state == 'cancel'): + continue + elif(self.state == 'sold'): + raise exceptions.UserError('Cannot cancel a sold property') + else: + r.state = 'cancel' + return True + diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index d92dfa51338..26c777a089d 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models +from odoo import api, fields, models, exceptions from dateutil.relativedelta import relativedelta import datetime @@ -8,7 +8,7 @@ class EstatePropertyOffer(models.Model): _description = "Estate property offer" price = fields.Float() - status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) + status = fields.Selection(readonly=True, selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) partner_id = fields.Many2one("res.partner", required=True) property_id = fields.Many2one("estate.property", required=True) validity = fields.Integer(default=7) @@ -26,3 +26,27 @@ def _inverse_validity(self): for r in self: create = r.create_date.date() if isinstance(r.create_date, datetime.datetime) else fields.Date.today() r.validity = (r.date_deadline - create).days + + + def accept_offer(self): + for r in self: + if(r.status == 'accepted'): + continue + + for other in r.property_id.offer_ids: + if(other.status == 'accepted'): + raise exceptions.UserError("Cannot accept multiple offers for a single property") + + r.status = 'accepted' + r.property_id.buyer_id = r.partner_id + r.property_id.selling_price = r.price + return True + + + def refuse_offer(self): + for r in self: + if(r.status == 'accepted'): + r.property_id.buyer_id = None + r.property_id.selling_price = None + r.status = 'refused' + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 16a7834bcb2..5f6f7d2c89f 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,8 +9,28 @@ + + + + + + + + + + + + + + + + + + +
+ + Estate Property Type estate.property.type diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 281e9269031..da4200171b8 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,18 +6,18 @@
-
- + - - + @@ -36,14 +36,14 @@ - - + + - + @@ -62,7 +62,7 @@ estate.property.list estate.property - + @@ -70,7 +70,7 @@ - + @@ -84,13 +84,13 @@ - + - - + + @@ -99,5 +99,6 @@ Estate Property estate.property list,form + {'search_default_available': True} From b458351572c4b780e46ee99509d2ce9e97ba0ba2 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 21 Jan 2026 15:08:54 +0100 Subject: [PATCH 14/29] [ADD] estate : Chapter 12 first part Added the business logic to the CRUD methods. Waiting for help for the rest --- estate/models/__init__.py | 1 + estate/models/estate_property.py | 6 ++++++ estate/models/estate_property_offer.py | 12 ++++++++++++ estate/models/user.py | 6 ++++++ 4 files changed, 25 insertions(+) create mode 100644 estate/models/user.py diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..357c22f4671 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 user diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 42f0abb430f..4d47542a76b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -88,3 +88,9 @@ def _check_price(self): if(float_compare(r.selling_price, r.expected_price * 9 / 10, precision_digits=2) < 0): raise exceptions.ValidationError("The selling price cannot be below 90 percent of the expected price") + + @api.ondelete(at_uninstall=False) + def _unlink_if_new_or_cancelled_(self): + if any(r.state != 'new' and r.state != 'cancelled' for r in self): + raise exceptions.UserError("Can only delete new or cancelled properties") + diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 253da06798a..b529f68a765 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -19,6 +19,18 @@ class EstatePropertyOffer(models.Model): property_type_id = fields.Many2one(related="property_id.property_type_id") + @api.model + def create(self, vals_list): + prop = self.env['estate.property'].browse(vals_list[0]['property_id']) + + if(any(o.price > vals_list[0]['price'] for o in prop.offer_ids)): + raise exceptions.UserError("Cannot add an offer with a lower amount than an existing one") + + prop.state = "offer-received" + + return super().create(vals_list) + + @api.depends("validity", "create_date") def _compute_deadline(self): for r in self: diff --git a/estate/models/user.py b/estate/models/user.py new file mode 100644 index 00000000000..4ff2b755d92 --- /dev/null +++ b/estate/models/user.py @@ -0,0 +1,6 @@ +from odoo import fields, models + +class User(models.Model): + pass # _inherit = ["res.users"] + + # property_ids = fields.One2many("estate.property", "salesperson_id") From a700cb5a849e7284e2e45178fdba843851f9b2fa Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 21 Jan 2026 15:33:35 +0100 Subject: [PATCH 15/29] [ADD] estate : End of chapter 12, estate_account : start of chapter 13 Found the bug in Chapter 12 --- estate/__manifest__.py | 3 ++- estate/models/__init__.py | 2 +- estate/models/res_users.py | 6 ++++++ estate/models/user.py | 6 ------ estate/views/res_users_views.xml | 15 +++++++++++++++ estate_account/__init__.py | 1 + estate_account/__manifest__.py | 8 ++++++++ estate_account/models/estate_property.py | 8 ++++++++ 8 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 estate/models/res_users.py delete mode 100644 estate/models/user.py create mode 100644 estate/views/res_users_views.xml create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 21a4c570fd4..8271d22e15b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,6 +10,7 @@ 'views/estate_property_tag_views.xml', 'views/estate_property_offer_views.xml', 'views/estate_property_views.xml', - 'views/estate_menus.xml' + 'views/estate_menus.xml', + 'views/res_users_views.xml' ] } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 357c22f4671..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,4 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer -from . import user +from . import res_users diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..86599da2562 --- /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") diff --git a/estate/models/user.py b/estate/models/user.py deleted file mode 100644 index 4ff2b755d92..00000000000 --- a/estate/models/user.py +++ /dev/null @@ -1,6 +0,0 @@ -from odoo import fields, models - -class User(models.Model): - pass # _inherit = ["res.users"] - - # property_ids = fields.One2many("estate.property", "salesperson_id") diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..1e0afe33495 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form.inherit.gamification + res.users + + + + + + + + + + diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..69c2a6d0a64 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,8 @@ +{ + 'name': 'Real Estate Accounting', + 'author': 'zavan', + 'depends': ['estate', 'account'], + 'license': 'LGPL-3', + 'data': [ + ] +} diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..6abacc465bf --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,8 @@ +from odoo import fields, models + +class EstateProperty(models.Model): + _inherit = ["estate.property"] + + def sell_property(self): + print("WORKING") + return super().sell_property() From f93bbbcaa7335edc4845b0a4fdd27184f47b1962 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 21 Jan 2026 16:15:44 +0100 Subject: [PATCH 16/29] [ADD] estate_account : Added Chapter 13 features --- estate/models/estate_property.py | 2 ++ estate/models/estate_property_offer.py | 1 + estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 22 ++++++++++++++++++++-- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 estate_account/models/__init__.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4d47542a76b..d3b8f6bbf19 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -64,6 +64,8 @@ def sell_property(self): continue elif(self.state == 'cancelled'): raise exceptions.UserError('Cannot sell a cancelled property') + elif(self.state != 'offer-accepted'): + raise exceptions.UserError('Cannot sell a property with no accepted offer') else: r.state = 'sold' return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index b529f68a765..fb7cf8d5c80 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -56,6 +56,7 @@ def accept_offer(self): r.status = 'accepted' r.property_id.buyer_id = r.partner_id r.property_id.selling_price = r.price + r.property_id.state = "offer-accepted" return True diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 6abacc465bf..99a4f0b5399 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -1,8 +1,26 @@ -from odoo import fields, models +from odoo import fields, models, exceptions, Command class EstateProperty(models.Model): _inherit = ["estate.property"] def sell_property(self): - print("WORKING") + for r in self : + vals = { + 'partner_id': r.buyer_id.id, + 'move_type': 'out_invoice', + 'journal_id': 1, + 'invoice_line_ids': [ + Command.create({ + 'name': '6 percent of selling price', + 'quantity': 1, + 'price_unit': (r.selling_price * 6 / 100) + }), + Command.create({ + 'name': 'Administrative fees', + 'quantity': 1, + 'price_unit': 100 + }) + ] + } + move = self.env['account.move'].create(vals) return super().sell_property() From 4e39ac0c9bb1495295cbb19ee3fe82bbe70156a2 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 21 Jan 2026 16:47:01 +0100 Subject: [PATCH 17/29] [ADD] estate : Chapter 14 features Added kanban view to estate properties --- estate/views/estate_property_views.xml | 32 +++++++++++++++++++++++++- estate_account/__manifest__.py | 4 +--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index da4200171b8..a9a8528ea07 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -95,10 +95,40 @@ + + estate.property.kanban + estate.property + + + + + +
+ +
+
+ Expected Price : + +
+
+ Best Price : + +
+
+ Selling Price : + +
+ +
+
+
+
+
+ Estate Property estate.property - list,form + kanban,list,form {'search_default_available': True} diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index 69c2a6d0a64..8a89d41ac35 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -2,7 +2,5 @@ 'name': 'Real Estate Accounting', 'author': 'zavan', 'depends': ['estate', 'account'], - 'license': 'LGPL-3', - 'data': [ - ] + 'license': 'LGPL-3' } From 137f105a8bb3ce402d4a0b9c94404c85af35353e Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 21 Jan 2026 17:09:44 +0100 Subject: [PATCH 18/29] [FIX] estate : Implemented reviews --- estate/models/estate_property.py | 42 ++++++++++---------- estate/models/estate_property_offer.py | 36 ++++++++--------- estate/models/estate_property_type.py | 4 +- estate/views/estate_property_offer_views.xml | 1 - estate/views/estate_property_views.xml | 2 +- 5 files changed, 42 insertions(+), 43 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d3b8f6bbf19..7ae92a6091f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,8 +8,10 @@ class EstateProperty(models.Model): _description = "Real estate property" _order = "id desc" - _check_expected_price = models.Constraint('CHECK(expected_price > 0)', 'The expected price should always be positive') - _check_selling_price = models.Constraint('CHECK(selling_price >= 0)', 'The selling price should always be positive') + _sql_constraints = [ + ("check_expected_price", "CHECK(expected_price > 0)", "The expected price must be strictly positive"), + ("check_selling_price", "CHECK(selling_price >= 0)", "The offer price must be positive"), + ] name = fields.Char(required=True) description = fields.Text() @@ -44,8 +46,8 @@ def _compute_total_area(self): @api.depends("offer_ids") def _compute_best_price(self): - for r in self: - r.best_price = max((offer.price for offer in r.offer_ids), default=0) + for prop in self: + prop.best_price = max((offer.price for offer in prop.offer_ids), default=0) @api.onchange("garden") @@ -54,45 +56,43 @@ def _onchange_garden(self): self.garden_area = 10 self.garden_orientation = "north" else: - self.garden_area = None - self.garden_orientation = None + self.garden_area = 0 + self.garden_orientation = False def sell_property(self): - for r in self: - if(self.state == 'sold'): + for prop in self: + if(prop.state == 'sold'): continue - elif(self.state == 'cancelled'): + elif(prop.state == 'cancelled'): raise exceptions.UserError('Cannot sell a cancelled property') - elif(self.state != 'offer-accepted'): + elif(prop.state != 'offer-accepted'): raise exceptions.UserError('Cannot sell a property with no accepted offer') else: - r.state = 'sold' + prop.state = 'sold' return True def cancel_property(self): - for r in self: - if(self.state == 'cancelled'): + for prop in self: + if(prop.state == 'cancelled'): continue - elif(self.state == 'sold'): + elif(prop.state == 'sold'): raise exceptions.UserError('Cannot cancel a sold property') else: - r.state = 'cancelled' + prop.state = 'cancelled' return True @api.constrains("selling_price", "expected_price") def _check_price(self): - for r in self: - print(r.selling_price) - if(r.selling_price is None or float_is_zero(r.selling_price, precision_digits=2)): - continue - if(float_compare(r.selling_price, r.expected_price * 9 / 10, precision_digits=2) < 0): + for prop in self: + if(not(prop.selling_price is None or float_is_zero(prop.selling_price, precision_digits=2)) and \ + float_compare(prop.selling_price, prop.expected_price * 9 / 10, precision_digits=2) < 0): raise exceptions.ValidationError("The selling price cannot be below 90 percent of the expected price") @api.ondelete(at_uninstall=False) def _unlink_if_new_or_cancelled_(self): - if any(r.state != 'new' and r.state != 'cancelled' for r in self): + if any(prop.state != 'new' and prop.state != 'cancelled' for r in self): raise exceptions.UserError("Can only delete new or cancelled properties") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index fb7cf8d5c80..a9921d43273 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -33,37 +33,37 @@ def create(self, vals_list): @api.depends("validity", "create_date") def _compute_deadline(self): - for r in self: - create = r.create_date.date() if isinstance(r.create_date, datetime.datetime) else fields.Date.today() - r.date_deadline = create + relativedelta(days=r.validity) + for offer in self: + create = offer.create_date.date() if isinstance(offer.create_date, datetime.datetime) else fields.Date.today() + offer.date_deadline = create + relativedelta(days=offer.validity) def _inverse_validity(self): - for r in self: - create = r.create_date.date() if isinstance(r.create_date, datetime.datetime) else fields.Date.today() - r.validity = (r.date_deadline - create).days + for offer in self: + create = offer.create_date.date() if isinstance(offer.create_date, datetime.datetime) else fields.Date.today() + offer.validity = (offer.date_deadline - create).days def accept_offer(self): - for r in self: - if(r.status == 'accepted'): + for offer in self: + if(offer.status == 'accepted'): continue - for other in r.property_id.offer_ids: + for other in offer.property_id.offer_ids: if(other.status == 'accepted'): raise exceptions.UserError("Cannot accept multiple offers for a single property") - r.status = 'accepted' - r.property_id.buyer_id = r.partner_id - r.property_id.selling_price = r.price - r.property_id.state = "offer-accepted" + offer.status = 'accepted' + offer.property_id.buyer_id = offer.partner_id + offer.property_id.selling_price = offer.price + offer.property_id.state = "offer-accepted" return True def refuse_offer(self): - for r in self: - if(r.status == 'accepted'): - r.property_id.buyer_id = None - r.property_id.selling_price = None - r.status = 'refused' + for offer in self: + if(offer.status == 'accepted'): + offer.property_id.buyer_id = None + offer.property_id.selling_price = None + offer.status = 'refused' return True diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index ccfa7f020a0..60eaaca38a1 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -17,5 +17,5 @@ class EstatePropertyType(models.Model): @api.depends("offer_ids") def _compute_offer_count(self): - for r in self: - r.offer_count = len(r.offer_ids) + for prop_type in self: + prop_type.offer_count = len(prop_type.offer_ids) diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 6fc62fc6771..61e6c51230c 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,7 +9,6 @@ - + + + + + + + diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..bfccd6ea4ad --- /dev/null +++ b/awesome_owl/static/src/counter/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: 1 }); + } + + increment() { + this.state.value++; + this.props?.onChange(this.state.value); + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..5e0ec2135c0 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,9 @@ + + + + +

Counter:

+ +
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..e2124b07784 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,25 @@ -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"; + + someHtml = "
some content
" + markupedHtml = markup(this.someHtml); + + static components = { + Counter, + Card, + TodoList + }; + + setup() { + this.state = useState({ sum: 2 }); + } + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..32b66270c7f 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -3,8 +3,25 @@
- hello world + hello word +
+ + + This is working + + + + + + + + + + + +

The sum :

+
diff --git a/awesome_owl/static/src/todo_item/todo_item.js b/awesome_owl/static/src/todo_item/todo_item.js new file mode 100644 index 00000000000..412bacc74a4 --- /dev/null +++ b/awesome_owl/static/src/todo_item/todo_item.js @@ -0,0 +1,19 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todo_item"; + + static props = { + todo: {type: Object, shape: {id: Number, description: String, isCompleted: Boolean}}, + toggleState: Function, + removeTodo: Function + } + + complete() { + this.props.toggleState(this.props.todo.id) + } + + remove() { + this.props.removeTodo(this.props.todo.id) + } +} diff --git a/awesome_owl/static/src/todo_item/todo_item.xml b/awesome_owl/static/src/todo_item/todo_item.xml new file mode 100644 index 00000000000..68da38622b2 --- /dev/null +++ b/awesome_owl/static/src/todo_item/todo_item.xml @@ -0,0 +1,14 @@ + + + + +
+ + + . + + +
+
+ +
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..923b3468819 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,38 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "../todo_item/todo_item"; +import { useAutofocus } from "../util"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + + counter = 1; + + static components = { + TodoItem + } + + setup() { + this.todos = useState([]); + useAutofocus("input"); + } + + change(id) { + let found = this.todos.findIndex(t => t.id === id); + if(found >= 0) this.todos[found] = {...this.todos[found], isCompleted: !this.todos[found].isCompleted}; + } + + remove(id) { + let found = this.todos.findIndex(t => t.id === id); + if(found >= 0) this.todos.splice(found, 1); + } + + tryAdd(ev) { + if(ev.keyCode != 13) return; + + let txt = ev.srcElement.value; + if(txt === "") return; + + this.todos.push({id: this.counter++, description: txt, isCompleted: false}); + ev.srcElement.value = ""; + } +} 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..abc59016150 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,11 @@ + + + + + +
+ +
+
+ +
diff --git a/awesome_owl/static/src/util.js b/awesome_owl/static/src/util.js new file mode 100644 index 00000000000..c5a84892a3c --- /dev/null +++ b/awesome_owl/static/src/util.js @@ -0,0 +1,8 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(refName) { + let ref = useRef(refName); + onMounted(() => { + ref.el?.focus() + }) +} diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index 0cbdbda4558..b043cee2bd0 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -10,20 +10,6 @@
- - estate.property.tag.form - estate.property.tag - - - - - - - - - - - Estate Property Tag estate.property.tag From 0c0f056bffdae0dfdaee61600208d2a7be4a46e8 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Thu, 22 Jan 2026 17:18:01 +0100 Subject: [PATCH 22/29] [ADD] owl_dashboard : exercise 1, 2 & 3 --- awesome_dashboard/static/src/dashboard.js | 25 +++++++++++++++++++ awesome_dashboard/static/src/dashboard.scss | 3 +++ awesome_dashboard/static/src/dashboard.xml | 13 +++++++++- .../src/dashboard_item/dashboard_item.js | 10 ++++++++ .../src/dashboard_item/dashboard_item.scss | 10 ++++++++ .../src/dashboard_item/dashboard_item.xml | 10 ++++++++ 6 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 awesome_dashboard/static/src/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard_item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard_item/dashboard_item.scss create mode 100644 awesome_dashboard/static/src/dashboard_item/dashboard_item.xml diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js index c4fb245621b..40a74afc1f8 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard.js @@ -1,8 +1,33 @@ import { Component } from "@odoo/owl"; import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item/dashboard_item" class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; + + static components = { + Layout, + DashboardItem + } + + setup() { + this.action = useService("action") + } + + + openCustomers() { + this.action.doAction("base.action_partner_form") + } + + openLeads() { + this.action.doAction({ + type: 'ir.actions.act_window', + res_model: 'crm.lead', + views: [[false, 'list'], [false, 'form']] + }) + } } registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss new file mode 100644 index 00000000000..c56cbaa8630 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: azure; +} diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml index 1a2ac9a2fed..3f2af42c706 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard.xml @@ -2,7 +2,18 @@ - hello dashboard + + + + + + + This is a smol item + + + This is a BIG item + + diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..ee138c0d99c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.dashboard_item"; + + static props = { + size: {type:Number, default: 1}, + slots: {type: Object, optional: true} + } +} diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.scss b/awesome_dashboard/static/src/dashboard_item/dashboard_item.scss new file mode 100644 index 00000000000..ec97b911019 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.scss @@ -0,0 +1,10 @@ +.card { + display: flex; + flex-direction: row; + align-items: center; + background: white; + border-radius: 0.5rem; + border: none; + padding: 1rem; + margin: 2rem +} diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..12604f4ecc3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml @@ -0,0 +1,10 @@ + + + + +
+ +
+
+ +
From 2c62ea75c6a96beaaf2ecf33274f598a48ecbd53 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Fri, 23 Jan 2026 12:04:50 +0100 Subject: [PATCH 23/29] [ADD] owl_dashboard : exercises 4 -> 10 --- awesome_dashboard/static/src/dashboard.scss | 3 - .../static/src/{ => dashboard}/dashboard.js | 9 ++- .../static/src/dashboard/dashboard.scss | 12 ++++ .../static/src/{ => dashboard}/dashboard.xml | 12 ++-- .../dashboard_item/dashboard_item.js | 0 .../dashboard_item/dashboard_item.scss | 4 +- .../dashboard_item/dashboard_item.xml | 0 .../static/src/dashboard/dashboard_items.js | 68 +++++++++++++++++++ .../dashboard/dashboard_items/number_card.js | 10 +++ .../dashboard/dashboard_items/number_card.xml | 9 +++ .../dashboard_items/pie_char_card.js | 43 ++++++++++++ .../dashboard_items/pie_chart_card.xml | 9 +++ .../dashboard/services/statistics.service.js | 30 ++++++++ .../static/src/dashboard_action.js | 18 +++++ 14 files changed, 213 insertions(+), 14 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.scss rename awesome_dashboard/static/src/{ => dashboard}/dashboard.js (65%) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss rename awesome_dashboard/static/src/{ => dashboard}/dashboard.xml (59%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard_item/dashboard_item.js (100%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard_item/dashboard_item.scss (75%) rename awesome_dashboard/static/src/{ => dashboard}/dashboard_item/dashboard_item.xml (100%) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/services/statistics.service.js create mode 100644 awesome_dashboard/static/src/dashboard_action.js diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss deleted file mode 100644 index c56cbaa8630..00000000000 --- a/awesome_dashboard/static/src/dashboard.scss +++ /dev/null @@ -1,3 +0,0 @@ -.o_dashboard { - background-color: azure; -} diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js similarity index 65% rename from awesome_dashboard/static/src/dashboard.js rename to awesome_dashboard/static/src/dashboard/dashboard.js index 40a74afc1f8..7bb9baf111e 100644 --- a/awesome_dashboard/static/src/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -1,10 +1,11 @@ -import { Component } from "@odoo/owl"; +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 { items } from "./dashboard_items"; -class AwesomeDashboard extends Component { +export class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; static components = { @@ -13,7 +14,9 @@ class AwesomeDashboard extends Component { } setup() { + this.items = registry.category("awesome_dashboard").get("items"); this.action = useService("action") + this.stats = useState(useService("statistics").loadStatistics()); } @@ -30,4 +33,4 @@ class AwesomeDashboard extends Component { } } -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); +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..4f92096f4a3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,12 @@ +.o_dashboard { + background-color: azure; + display: flex; + width: 100%; + flex-direction: row; + flex-wrap: wrap; +} + +.green_value { + color: green; + font-size: 3rem; +} diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml similarity index 59% rename from awesome_dashboard/static/src/dashboard.xml rename to awesome_dashboard/static/src/dashboard/dashboard.xml index 3f2af42c706..26f76113d79 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -7,12 +7,12 @@ - - This is a smol item - - - This is a BIG item - + + + + + + diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js similarity index 100% rename from awesome_dashboard/static/src/dashboard_item/dashboard_item.js rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.scss b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss similarity index 75% rename from awesome_dashboard/static/src/dashboard_item/dashboard_item.scss rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss index ec97b911019..4aaf1fd807a 100644 --- a/awesome_dashboard/static/src/dashboard_item/dashboard_item.scss +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss @@ -1,10 +1,10 @@ .card { display: flex; - flex-direction: row; + flex-direction: column; align-items: center; background: white; border-radius: 0.5rem; border: none; padding: 1rem; - margin: 2rem + margin: 0.5rem } diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml similarity index 100% rename from awesome_dashboard/static/src/dashboard_item/dashboard_item.xml rename to awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml 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..92e0d45a272 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,68 @@ +import { PieChartCard } from "./dashboard_items/pie_char_card" +import { NumberCard } from "./dashboard_items/number_card" +import { registry } from "@web/core/registry"; + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity + }), + }, + { + id: "average_time", + description: "Average time", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time + }), + }, + { + id: "nb_new_orders", + description: "Number of new orders", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders + }), + }, + { + id: "nb_cancelled_orders", + description: "Number of cancelled orders", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders + }), + }, + { + id: "total_amount", + description: "Total amount of new orders", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount + }), + }, + { + id: "orders_by_size", + description: "Shirt orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "Shirt orders by size", + value: data.orders_by_size + }), + }, + ] + +registry.category("awesome_dashboard").add("items", items); diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js new file mode 100644 index 00000000000..ad2a98f9160 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js @@ -0,0 +1,10 @@ +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/dashboard_items/number_card.xml b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.xml new file mode 100644 index 00000000000..45454142519 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js new file mode 100644 index 00000000000..96bb730e390 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js @@ -0,0 +1,43 @@ +import { Component, onWillStart, useRef, onMounted, onWillPatch, useState } from "@odoo/owl"; +import { loadJS } from "@web/core/assets" + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard" + + static props = { + title: String, + value: {type: Object, optional: true} + } + + chart = null; + + drawChart() { + if(!this.props.value) return; + + if(this.chart) { + this.chart.data.datasets[0].data = Object.values(this.props.value); + this.chart.update(); + return; + } + + this.chart = new Chart(this.canvasRef.el, { + type: 'pie', + data: { + labels: Object.keys(this.props.value), + datasets: [ + { + label: 'Shirts', + data: Object.values(this.props.value) + } + ] + } + }) + } + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js")); + onMounted(() => this.drawChart()) + onWillPatch(() => this.drawChart()) + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml new file mode 100644 index 00000000000..c6b5c12d7a3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/services/statistics.service.js b/awesome_dashboard/static/src/dashboard/services/statistics.service.js new file mode 100644 index 00000000000..0f009e8c5e6 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/services/statistics.service.js @@ -0,0 +1,30 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +const fetchInterval = 3000; + +async function fetchStatistics(stats) { + const result = await rpc("/awesome_dashboard/statistics"); + + for(let entry of Object.entries(result)) { + if(stats[entry[0]] !== undefined) stats[entry[0]] = entry[1]; + } +} + +export const statisticsService = { + start() { + let stats = reactive({average_quantity: 0, average_time: 0, nb_cancelled_orders: 0, nb_new_orders: 0, total_amount: 0, orders_by_size: {}}); + + fetchStatistics(stats) + setInterval(() => fetchStatistics(stats), fetchInterval); + + return { + loadStatistics() { + return stats; + } + } + } +} + +registry.category("services").add("statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..f09712a4459 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,18 @@ +import { Component, xml } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { AwesomeDashboard } from "./dashboard/dashboard"; +import { registry } from "@web/core/registry"; + +class AwesomeDashboardLoader extends Component { + static components = { + LazyComponent, + AwesomeDashboard + }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); + + From 8cb31f7e11ccb92fb3b828c37821c68fa4822e38 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Fri, 23 Jan 2026 14:22:14 +0100 Subject: [PATCH 24/29] [ADD] owl_dashboard : Exercise 11 feature --- .../dashboard_items_dialog.js | 33 ++++++++++++++ .../dashboard_items_dialog.xml | 20 +++++++++ .../static/src/dashboard/dashboard.js | 10 +++-- .../static/src/dashboard/dashboard.scss | 1 + .../static/src/dashboard/dashboard.xml | 3 ++ .../dashboard_item/dashboard_item.scss | 3 +- .../services/dashboard_items.service.js | 43 +++++++++++++++++++ .../dashboard/services/statistics.service.js | 2 +- 8 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.js create mode 100644 awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml create mode 100644 awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js diff --git a/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.js b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.js new file mode 100644 index 00000000000..40637b0ac54 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.js @@ -0,0 +1,33 @@ +import { Dialog } from "@web/core/dialog/dialog"; +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export class DashboardItemDialog extends Component { + static template = "awesome_dashboard.dashboard_items_dialog"; + static components = { + Dialog, + }; + + setup() { + this.service = useService("dashboard_items"); + this.allItems = this.service.getAllItems(); + this.usedIds = this.service.getUsedIds(); + } + + isSelected(id) { + return this.usedIds.includes(id); + } + + select(id) { + return () => { + let found = this.usedIds.indexOf(id); + if(found >= 0) this.usedIds.splice(found, 1); + else this.usedIds.push(id); + } + } + + apply() { + this.service.setUsedIds(this.usedIds); + this.props?.close() + } +} diff --git a/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml new file mode 100644 index 00000000000..36f9e7d3a96 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml @@ -0,0 +1,20 @@ + + + + + + +
+ + +
+
+ + + +
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js index 7bb9baf111e..a207a442bed 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.js +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -3,7 +3,7 @@ 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 { items } from "./dashboard_items"; +import { DashboardItemDialog } from "./dash_item_dialog/dashboard_items_dialog"; export class AwesomeDashboard extends Component { static template = "awesome_dashboard.AwesomeDashboard"; @@ -14,12 +14,12 @@ export class AwesomeDashboard extends Component { } setup() { - this.items = registry.category("awesome_dashboard").get("items"); + this.items = useState(useService("dashboard_items").getUsedItems()); this.action = useService("action") this.stats = useState(useService("statistics").loadStatistics()); + this.dialog = useService("dialog"); } - openCustomers() { this.action.doAction("base.action_partner_form") } @@ -31,6 +31,10 @@ export class AwesomeDashboard extends Component { views: [[false, 'list'], [false, 'form']] }) } + + openItemsDialog() { + this.dialog.add(DashboardItemDialog) + } } 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 index 4f92096f4a3..6af4f389665 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.scss +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -4,6 +4,7 @@ width: 100%; flex-direction: row; flex-wrap: wrap; + align-content: start; } .green_value { diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml index 26f76113d79..7e68772189f 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -7,6 +7,9 @@ + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss index 4aaf1fd807a..450ca3e0105 100644 --- a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss @@ -6,5 +6,6 @@ border-radius: 0.5rem; border: none; padding: 1rem; - margin: 0.5rem + margin: 0.5rem; + height: fit-content; } diff --git a/awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js b/awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js new file mode 100644 index 00000000000..77cab8c07b3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js @@ -0,0 +1,43 @@ +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; + +const storageKey = "owl-dashboard-used-dashboard-items" + +function setUsedItems(result, allItems, usedIds) { + result.splice(0, result.length); + + for(let id of usedIds) { + let found = allItems.find((i) => i.id == id); + result.push(found); + } +} + +const dashboardItemsService = { + start() { + let fromStorage = localStorage.getItem(storageKey); + let usedIds = fromStorage ? JSON.parse(fromStorage) : []; + let allItems = registry.category("awesome_dashboard").get("items"); + let usedItems = reactive([]); + + setUsedItems(usedItems, allItems, usedIds); + + return { + getUsedItems() { + return usedItems; + }, + getAllItems() { + return allItems; + }, + getUsedIds() { + return usedIds.slice(); + }, + setUsedIds(ids) { + usedIds = ids; + localStorage.setItem(storageKey, JSON.stringify(ids)); + setUsedItems(usedItems, allItems, usedIds); + } + } + } +} + +registry.category("services").add("dashboard_items", dashboardItemsService); diff --git a/awesome_dashboard/static/src/dashboard/services/statistics.service.js b/awesome_dashboard/static/src/dashboard/services/statistics.service.js index 0f009e8c5e6..f1a9f55b38d 100644 --- a/awesome_dashboard/static/src/dashboard/services/statistics.service.js +++ b/awesome_dashboard/static/src/dashboard/services/statistics.service.js @@ -12,7 +12,7 @@ async function fetchStatistics(stats) { } } -export const statisticsService = { +const statisticsService = { start() { let stats = reactive({average_quantity: 0, average_time: 0, nb_cancelled_orders: 0, nb_new_orders: 0, total_amount: 0, orders_by_size: {}}); From 8f880ec776157f8458ce7ccc94ecd75c780cb0a7 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Mon, 26 Jan 2026 17:08:35 +0100 Subject: [PATCH 25/29] [ADD] awesome_clicker : Exercises 1 -> 15 --- .../clicker_systray/clicker_systray_item.js | 44 ++++++ .../clicker_systray/clicker_systray_item.scss | 9 ++ .../clicker_systray/clicker_systray_item.xml | 14 ++ .../static/src/clicker_value/clicker_value.js | 14 ++ .../src/clicker_value/clicker_value.xml | 6 + .../static/src/client_action/client_action.js | 34 +++++ .../src/client_action/client_action.xml | 29 ++++ .../static/src/models/clicker_model.js | 133 ++++++++++++++++++ .../static/src/models/client_reward.js | 69 +++++++++ .../static/src/services/clicker_service.js | 28 ++++ awesome_clicker/static/src/utility.js | 8 ++ 11 files changed, 388 insertions(+) create mode 100644 awesome_clicker/static/src/clicker_systray/clicker_systray_item.js create mode 100644 awesome_clicker/static/src/clicker_systray/clicker_systray_item.scss create mode 100644 awesome_clicker/static/src/clicker_systray/clicker_systray_item.xml create mode 100644 awesome_clicker/static/src/clicker_value/clicker_value.js create mode 100644 awesome_clicker/static/src/clicker_value/clicker_value.xml create mode 100644 awesome_clicker/static/src/client_action/client_action.js create mode 100644 awesome_clicker/static/src/client_action/client_action.xml create mode 100644 awesome_clicker/static/src/models/clicker_model.js create mode 100644 awesome_clicker/static/src/models/client_reward.js create mode 100644 awesome_clicker/static/src/services/clicker_service.js create mode 100644 awesome_clicker/static/src/utility.js diff --git a/awesome_clicker/static/src/clicker_systray/clicker_systray_item.js b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.js new file mode 100644 index 00000000000..fb5b1054df2 --- /dev/null +++ b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.js @@ -0,0 +1,44 @@ +import { Component, useExternalListener } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { useClicker } from "../utility"; +import { ClickerValue } from "../clicker_value/clicker_value"; + +export class ClickerSystrayItem extends Component { + static template = "awesome_clicker.ClickerSystrayItem"; + + static components = { + ClickerValue + } + + setup() { + this.clicker = useClicker(); + this.actionService = useService("action"); + + useExternalListener(document, "click", this.increment, {capture: true}) + } + + bigIncrement() { + this.clicker.increment(10); + } + + increment(ev) { + if(ev.target.closest("#clicker-big-increment-button")) return; + + this.clicker.increment(1); + } + + openClientAction() { + this.actionService.doAction({ + type: 'ir.actions.client', + tag: 'awesome_clicker.client_action', + target: 'new', + name: 'Clicker' + }) + } +} + +registry.category("systray").add("awesome_clicker.client_action", { + Component: ClickerSystrayItem, + sequence: 1 +}); diff --git a/awesome_clicker/static/src/clicker_systray/clicker_systray_item.scss b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.scss new file mode 100644 index 00000000000..a23a32cfdf8 --- /dev/null +++ b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.scss @@ -0,0 +1,9 @@ +.clicker-systray-item { + display: flex; + flex-direction: row; + align-items: center; +} + +.clicker-systray-item > * { + margin: 0 0.2rem; +} diff --git a/awesome_clicker/static/src/clicker_systray/clicker_systray_item.xml b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.xml new file mode 100644 index 00000000000..9c331d239bc --- /dev/null +++ b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.xml @@ -0,0 +1,14 @@ + + + + +
+ + + +
+
+ +
diff --git a/awesome_clicker/static/src/clicker_value/clicker_value.js b/awesome_clicker/static/src/clicker_value/clicker_value.js new file mode 100644 index 00000000000..48e77fddc0b --- /dev/null +++ b/awesome_clicker/static/src/clicker_value/clicker_value.js @@ -0,0 +1,14 @@ +import { Component } from "@odoo/owl"; +import { humanNumber } from "@web/core/utils/numbers" + +export class ClickerValue extends Component { + static template = "awesome_clicker.ClientValue"; + + static props = { + value: Number + } + + getValue() { + return humanNumber(this.props.value) + } +} diff --git a/awesome_clicker/static/src/clicker_value/clicker_value.xml b/awesome_clicker/static/src/clicker_value/clicker_value.xml new file mode 100644 index 00000000000..42368e2b7fb --- /dev/null +++ b/awesome_clicker/static/src/clicker_value/clicker_value.xml @@ -0,0 +1,6 @@ + + + + Clicks : + + diff --git a/awesome_clicker/static/src/client_action/client_action.js b/awesome_clicker/static/src/client_action/client_action.js new file mode 100644 index 00000000000..dffc5293cb9 --- /dev/null +++ b/awesome_clicker/static/src/client_action/client_action.js @@ -0,0 +1,34 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useClicker } from "../utility"; +import { ClickerValue } from "../clicker_value/clicker_value"; + +export class ClientAction extends Component { + static template = "awesome_clicker.ClientAction"; + + static components = { + ClickerValue + } + + setup() { + this.clicker = useClicker(); + } + + bigIncrement() { + this.clicker.increment(10_000); + } + + buyClickBot() { + this.clicker.buyClickBot(); + } + + buyBigClickBot() { + this.clicker.buyBigClickBot(); + } + + buyPower() { + this.clicker.buyPower(); + } +} + +registry.category("actions").add("awesome_clicker.client_action", ClientAction) diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml new file mode 100644 index 00000000000..1e74b4f1bba --- /dev/null +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -0,0 +1,29 @@ + + + + +
+ Clicker Client Action +
+ + +
+ +

Bots

+

+ x ClickBots (10 clicks/second) +

+ +

+ x BigClickBots (100 clicks/second) +

+ + +

Power

+

+ Power +

+ +
+
+
diff --git a/awesome_clicker/static/src/models/clicker_model.js b/awesome_clicker/static/src/models/clicker_model.js new file mode 100644 index 00000000000..c8dca83e6a1 --- /dev/null +++ b/awesome_clicker/static/src/models/clicker_model.js @@ -0,0 +1,133 @@ +import { Reactive } from "@web/core/utils/reactive"; +import { EventBus } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +const buyables = { + clickBot: { + minLevel: 1, + price: 1000, + name: "clickBots", + onBuy: "triggerInterval" + }, + bigClickBot: { + minLevel: 2, + price: 5000, + name: "bigClickBots", + onBuy: "triggerInterval" + }, + power: { + minLevel: 3, + price: 100_000, + name: "power", + onBuy: "" + } +} + +const allLevels = [ + {minClicks: 1_000, eventName: "MILESTONE_1k"}, + {minClicks: 5_000, eventName: "MILESTONE_5k"}, + {minClicks: 100_000, eventName: "MILESTONE_100k"} +] + +export class ClickerModel extends Reactive { + clicks = 0; + level = 0; + + clickBots = 0; + bigClickBots = 0; + power = 1; + clickBotInterval = null; + + bus = new EventBus(); + + constructor() { + super(); + } + + increment(val) { + this.clicks += val; + + for(let i = this.level; i < allLevels.length; i++) { + let level = allLevels[i]; + if(this.clicks >= level.minClicks) { + this.level = i + 1; + this.bus.trigger(level.eventName); + } + } + } + + buyClickBot() { + this.buy(buyables.clickBot); + } + + buyBigClickBot() { + this.buy(buyables.bigClickBot); + } + + buyPower() { + this.buy(buyables.power); + } + + getClickBot(count) { + this.get(buyables.clickBot, count); + } + + getBigClickBot(count) { + this.get(buyables.bigClickBot, count); + } + + getPower(count) { + this.get(buyables.power, count); + } + + buy(availability) { + if(this.clicks < availability.price || this.level < availability.minLevel) return; + + this.clicks -= availability.price; + this[availability.name]++; + + let onBuy = this[availability.onBuy]; + if(onBuy) onBuy(this); + } + + get(availability, count) { + if(this.level < availability.minLevel) return; + + this[availability.name] += count; + + let onBuy = this[availability.onBuy]; + if(onBuy) onBuy(this); + } + + triggerInterval(el) { + if(el.clickBotInterval === null) { + el.clickBotInterval = setInterval(() => { + el.clicks += el.clickBots * 10 * el.power; + el.clicks += el.bigClickBots * 100 * el.power; + }, 1000); + } + } +} + +registry.category("command_provider").add("clicker", { + provide: (env, options) => { + return [{ + action() { + env.services.action.doAction({ + type: 'ir.actions.client', + tag: 'awesome_clicker.client_action', + target: 'new', + name: 'Clicker' + }) + }, + category: "clicker", + name: "Open Clicker Game" + },{ + action() { + env.services.clicker.clicker.buyClickBot(); + }, + category: "Clicker", + name: "Buy 1 click bot" + }] + } +}) diff --git a/awesome_clicker/static/src/models/client_reward.js b/awesome_clicker/static/src/models/client_reward.js new file mode 100644 index 00000000000..33880416e14 --- /dev/null +++ b/awesome_clicker/static/src/models/client_reward.js @@ -0,0 +1,69 @@ +import { patch } from "@web/core/utils/patch" +import { FormController } from "@web/views/form/form_controller" +import { useClicker } from "../utility"; +import { useService } from "@web/core/utils/hooks"; + +export const rewards = [ + { + description: "Get 1 click bot", + apply(clicker) { + clicker.getClickBot(1); + }, + minLevel: 1, + maxLevel: 3, + }, + { + description: "Get 10 click bot", + apply(clicker) { + clicker.getClickBot(10); + }, + minLevel: 3, + maxLevel: 4, + }, + { + description: "Increase bot power!", + apply(clicker) { + clicker.getPower(1); + }, + minLevel: 3, + }, +]; + +export function getRandomReward(clicker) { + let choices = rewards.filter(r => (r.minLevel === undefined || r.minLevel <= clicker.level) + && (r.maxLevel === undefined || r.maxLevel >= clicker.level)); + + if(choices.length == 0) return null; + + return choices[Math.floor(Math.random() * choices.length)] +} + +const randomRewardChance = 1; + +patch(FormController.prototype, { + setup() { + super.setup(); + + if(Math.random() <= randomRewardChance) { + let clicker = useClicker(); + let reward = getRandomReward(clicker); + if(!reward) return; + + this.notification = useService("notification"); + const closeNotification = this.notification.add(`"${reward.description}"`, { + title: "Congrats, you won a reward ", + type: "success", + sticky: true, + buttons: [ + { + name: "Collect", + onClick: () => { + closeNotification(); + reward.apply(clicker); + } + }, + ], + }); + } + } +}) diff --git a/awesome_clicker/static/src/services/clicker_service.js b/awesome_clicker/static/src/services/clicker_service.js new file mode 100644 index 00000000000..a6633a38235 --- /dev/null +++ b/awesome_clicker/static/src/services/clicker_service.js @@ -0,0 +1,28 @@ +import { registry } from "@web/core/registry"; +import { ClickerModel } from "../models/clicker_model"; + +const allEvents = [ + {eventName: "MILESTONE_1k", description: "Milestone reached! You can now buy clickbots"}, + {eventName: "MILESTONE_5k", description: "Milestone reached! You can now buy big clickbots"}, + {eventName: "MILESTONE_100k", description: "Milestone reached! You can now buy power"} +] + +const clickerService = { + dependencies: ["effect"], + start(env, {effect}) { + let clicker = new ClickerModel(); + + for(let event of allEvents) { + clicker.bus.addEventListener(event.eventName, () => { + effect.add({ + type: 'rainbow_man', + message: event.description + }) + }) + } + + return { clicker } + } +} + +registry.category("services").add("clicker", clickerService); diff --git a/awesome_clicker/static/src/utility.js b/awesome_clicker/static/src/utility.js new file mode 100644 index 00000000000..a0ab2203426 --- /dev/null +++ b/awesome_clicker/static/src/utility.js @@ -0,0 +1,8 @@ +import { useService } from "@web/core/utils/hooks"; +import { useState } from "@odoo/owl"; + +export function useClicker() { + let service = useService("clicker"); + + return useState(service.clicker) +} From a6666483e9e2487846595049f7a9c8ae6ddc6f67 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Tue, 27 Jan 2026 14:56:38 +0100 Subject: [PATCH 26/29] [ADD] awesome_clicker : Exercises16 -> 21 --- .../clicker_systray/clicker_systray_item.js | 11 +- .../clicker_systray/clicker_systray_item.scss | 10 +- .../clicker_systray/clicker_systray_item.xml | 34 +++-- .../static/src/clicker_value/clicker_value.js | 1 + .../src/clicker_value/clicker_value.xml | 2 +- .../static/src/client_action/client_action.js | 16 ++- .../src/client_action/client_action.xml | 57 +++++---- .../static/src/models/clicker_migration.js | 27 ++++ .../static/src/models/clicker_model.js | 117 +++++++++++++++--- .../static/src/services/clicker_service.js | 14 ++- 10 files changed, 230 insertions(+), 59 deletions(-) create mode 100644 awesome_clicker/static/src/models/clicker_migration.js diff --git a/awesome_clicker/static/src/clicker_systray/clicker_systray_item.js b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.js index fb5b1054df2..3fa55915374 100644 --- a/awesome_clicker/static/src/clicker_systray/clicker_systray_item.js +++ b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.js @@ -3,12 +3,17 @@ import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { useClicker } from "../utility"; import { ClickerValue } from "../clicker_value/clicker_value"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { Dropdown } from "@web/core/dropdown/dropdown" +import { treeTypes } from "../models/clicker_model"; export class ClickerSystrayItem extends Component { static template = "awesome_clicker.ClickerSystrayItem"; static components = { - ClickerValue + ClickerValue, + Dropdown, + DropdownItem } setup() { @@ -36,6 +41,10 @@ export class ClickerSystrayItem extends Component { name: 'Clicker' }) } + + getTreeTypes() { + return treeTypes; + } } registry.category("systray").add("awesome_clicker.client_action", { diff --git a/awesome_clicker/static/src/clicker_systray/clicker_systray_item.scss b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.scss index a23a32cfdf8..a46b17cd8fe 100644 --- a/awesome_clicker/static/src/clicker_systray/clicker_systray_item.scss +++ b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.scss @@ -1,9 +1,3 @@ -.clicker-systray-item { - display: flex; - flex-direction: row; - align-items: center; -} - -.clicker-systray-item > * { - margin: 0 0.2rem; +.clicker-systray-separator { + margin: 0 0.2rem } diff --git a/awesome_clicker/static/src/clicker_systray/clicker_systray_item.xml b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.xml index 9c331d239bc..10f8a971ea4 100644 --- a/awesome_clicker/static/src/clicker_systray/clicker_systray_item.xml +++ b/awesome_clicker/static/src/clicker_systray/clicker_systray_item.xml @@ -2,13 +2,33 @@ -
- - - -
+ +

+ + + , + + + , + + +

+ + + + Open the clicker game + + + Buy a Clickbot + +

+ +

+

+ +

+
+
diff --git a/awesome_clicker/static/src/clicker_value/clicker_value.js b/awesome_clicker/static/src/clicker_value/clicker_value.js index 48e77fddc0b..9b4d0084461 100644 --- a/awesome_clicker/static/src/clicker_value/clicker_value.js +++ b/awesome_clicker/static/src/clicker_value/clicker_value.js @@ -5,6 +5,7 @@ export class ClickerValue extends Component { static template = "awesome_clicker.ClientValue"; static props = { + name: String, value: Number } diff --git a/awesome_clicker/static/src/clicker_value/clicker_value.xml b/awesome_clicker/static/src/clicker_value/clicker_value.xml index 42368e2b7fb..fd6f241acee 100644 --- a/awesome_clicker/static/src/clicker_value/clicker_value.xml +++ b/awesome_clicker/static/src/clicker_value/clicker_value.xml @@ -1,6 +1,6 @@ - Clicks : + diff --git a/awesome_clicker/static/src/client_action/client_action.js b/awesome_clicker/static/src/client_action/client_action.js index dffc5293cb9..e129f3e675e 100644 --- a/awesome_clicker/static/src/client_action/client_action.js +++ b/awesome_clicker/static/src/client_action/client_action.js @@ -2,12 +2,16 @@ import { Component } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { useClicker } from "../utility"; import { ClickerValue } from "../clicker_value/clicker_value"; +import { treeTypes } from "../models/clicker_model"; +import { Notebook } from "@web/core/notebook/notebook"; + export class ClientAction extends Component { static template = "awesome_clicker.ClientAction"; static components = { - ClickerValue + ClickerValue, + Notebook } setup() { @@ -15,7 +19,7 @@ export class ClientAction extends Component { } bigIncrement() { - this.clicker.increment(10_000); + this.clicker.increment(100_000); } buyClickBot() { @@ -29,6 +33,14 @@ export class ClientAction extends Component { buyPower() { this.clicker.buyPower(); } + + buyTree(fruitName) { + this.clicker.buyTree(fruitName); + } + + getTreeTypes() { + return treeTypes; + } } registry.category("actions").add("awesome_clicker.client_action", ClientAction) diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml index 1e74b4f1bba..760a10b09c8 100644 --- a/awesome_clicker/static/src/client_action/client_action.xml +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -3,27 +3,42 @@
- Clicker Client Action -
- - -
- -

Bots

-

- x ClickBots (10 clicks/second) -

- -

- x BigClickBots (100 clicks/second) -

- - -

Power

-

- Power -

- + +
+ + +

Bots

+

+ x ClickBots (10 clicks/second) +

+ +

+ x BigClickBots (100 clicks/second) +

+ + +

Power

+

+ Power +

+ +
+ + +

Trees

+
+

+ +

+ +
+ +

Fruits

+

+ x +

+
+
diff --git a/awesome_clicker/static/src/models/clicker_migration.js b/awesome_clicker/static/src/models/clicker_migration.js new file mode 100644 index 00000000000..00ed31912ed --- /dev/null +++ b/awesome_clicker/static/src/models/clicker_migration.js @@ -0,0 +1,27 @@ +export const currentVersion = 1; + +const migrations = [ + { + fromVersion: 1, + toVersion: 2, + apply(clicker) { + clicker.trees.push(0); + clicker.fruits.push(0); + } + } +] + +export function migrate(clicker) { + let didMigrate = false; + + for(let migration of migrations) { + if(migration.fromVersion === clicker.version) { + migration.apply(clicker); + clicker.version = migration.toVersion; + + didMigrate = true; + } + } + + return didMigrate; +} diff --git a/awesome_clicker/static/src/models/clicker_model.js b/awesome_clicker/static/src/models/clicker_model.js index c8dca83e6a1..d63b7e17ddf 100644 --- a/awesome_clicker/static/src/models/clicker_model.js +++ b/awesome_clicker/static/src/models/clicker_model.js @@ -1,34 +1,42 @@ import { Reactive } from "@web/core/utils/reactive"; import { EventBus } from "@odoo/owl"; import { registry } from "@web/core/registry"; +import { currentVersion } from "./clicker_migration"; -const buyables = { +export const treeTypes = [ + "pear", "cherry", "peach" +] + +let buyables = { clickBot: { minLevel: 1, price: 1000, name: "clickBots", - onBuy: "triggerInterval" + onBuy: "triggetClickBotInterval" }, bigClickBot: { minLevel: 2, price: 5000, name: "bigClickBots", - onBuy: "triggerInterval" + onBuy: "triggetClickBotInterval" }, power: { minLevel: 3, price: 100_000, name: "power", onBuy: "" - } + }, } const allLevels = [ {minClicks: 1_000, eventName: "MILESTONE_1k"}, {minClicks: 5_000, eventName: "MILESTONE_5k"}, - {minClicks: 100_000, eventName: "MILESTONE_100k"} + {minClicks: 100_000, eventName: "MILESTONE_100k"}, + {minClicks: 1_000_000, eventName: "MILESTONE_1M"} ] +export const clickerModelKey = "clicker-model-key"; + export class ClickerModel extends Reactive { clicks = 0; level = 0; @@ -38,10 +46,38 @@ export class ClickerModel extends Reactive { power = 1; clickBotInterval = null; + trees = new Array(treeTypes.length).fill(0); + fruits = new Array(treeTypes.length).fill(0); + treeInterval = null; + bus = new EventBus(); - constructor() { + version = 0; + + constructor(def = {}) { super(); + + this.clicks = def.clicks ?? 0; + this.level = def.level ?? 0; + this.clickBots = def.clickBots ?? 0; + this.bigClickBots = def.bigClickBots ?? 0; + this.power = def.power ?? 1; + this.version = def.version ?? currentVersion; + + if(def.fruits) { + for(let i = 0; i < this.fruits.length; i++) { + this.fruits[i] = def.fruits[i] ?? 0; + } + } + + if(def.trees) { + for(let i = 0; i < this.trees.length; i++) { + this.trees[i] = def.trees[i] ?? 0; + } + } + + if(this.clickBots > 0 || this.bigClickBots > 0) this.triggetClickBotInterval(this); + if(this.trees.find(t => t > 0)) this.triggerTreeInterval(this); } increment(val) { @@ -80,37 +116,84 @@ export class ClickerModel extends Reactive { this.get(buyables.power, count); } - buy(availability) { - if(this.clicks < availability.price || this.level < availability.minLevel) return; + buyTree(fruitName) { + let index = treeTypes.indexOf(fruitName); + if(index < 0) return; + + if(this.clicks < 1_000_000 || this.level < 4) return; + + this.clicks -= 1_000_000; + this.trees[index]++; + this.triggerTreeInterval(this); + } + + buy(buyable) { + if(this.clicks < buyable.price || this.level < buyable.minLevel) return; - this.clicks -= availability.price; - this[availability.name]++; + this.clicks -= buyable.price; + this[buyable.name]++; - let onBuy = this[availability.onBuy]; + let onBuy = this[buyable.onBuy]; if(onBuy) onBuy(this); } - get(availability, count) { - if(this.level < availability.minLevel) return; + get(buyable, count) { + if(this.level < buyable.minLevel) return; - this[availability.name] += count; + this[buyable.name] += count; - let onBuy = this[availability.onBuy]; + let onBuy = this[buyable.onBuy]; if(onBuy) onBuy(this); } - triggerInterval(el) { + getTreeCount(fruitName) { + let index = treeTypes.indexOf(fruitName); + if(index < 0) return 0; + + return this.trees[index]; + } + + getAllTreesCount() { + return this.trees.reduce((acc, t) => acc += t, 0); + } + + getAllFruitsCount() { + return this.fruits.reduce((acc, t) => acc += t, 0); + } + + getFruitCount(fruitName) { + let index = treeTypes.indexOf(fruitName); + if(index < 0) return 0; + + return this.fruits[index]; + } + + triggetClickBotInterval(el) { if(el.clickBotInterval === null) { el.clickBotInterval = setInterval(() => { el.clicks += el.clickBots * 10 * el.power; el.clicks += el.bigClickBots * 100 * el.power; + + localStorage.setItem(clickerModelKey, JSON.stringify(el)); }, 1000); } } + + triggerTreeInterval(el) { + if(el.treeInterval === null) { + el.treeInterval = setInterval(() => { + for(let i = 0; i < treeTypes.length; i++) { + el.fruits[i] += el.trees[i]; + } + + localStorage.setItem(clickerModelKey, JSON.stringify(el)); + }, 3000); + } + } } registry.category("command_provider").add("clicker", { - provide: (env, options) => { + provide: (env) => { return [{ action() { env.services.action.doAction({ diff --git a/awesome_clicker/static/src/services/clicker_service.js b/awesome_clicker/static/src/services/clicker_service.js index a6633a38235..ea9dda6de73 100644 --- a/awesome_clicker/static/src/services/clicker_service.js +++ b/awesome_clicker/static/src/services/clicker_service.js @@ -1,16 +1,26 @@ import { registry } from "@web/core/registry"; import { ClickerModel } from "../models/clicker_model"; +import { clickerModelKey } from "../models/clicker_model"; +import { migrate } from "../models/clicker_migration"; const allEvents = [ {eventName: "MILESTONE_1k", description: "Milestone reached! You can now buy clickbots"}, {eventName: "MILESTONE_5k", description: "Milestone reached! You can now buy big clickbots"}, - {eventName: "MILESTONE_100k", description: "Milestone reached! You can now buy power"} + {eventName: "MILESTONE_100k", description: "Milestone reached! You can now buy power"}, + {eventName: "MILESTONE_1M", description: "Milestone reached! You can now buy trees"} ] const clickerService = { dependencies: ["effect"], start(env, {effect}) { - let clicker = new ClickerModel(); + let stored = localStorage.getItem(clickerModelKey); + let clicker; + + if(stored) { + clicker = new ClickerModel(JSON.parse(stored)); + if(migrate(clicker)) localStorage.setItem(clickerModelKey, JSON.stringify(clicker)); + } + else clicker = new ClickerModel(); for(let event of allEvents) { clicker.bus.addEventListener(event.eventName, () => { From 82ce0fcde0fd97ee210d55e3c2ea644a279336e4 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Tue, 27 Jan 2026 17:04:09 +0100 Subject: [PATCH 27/29] [ADD] estate : Define module data All the exercises except the Command one --- estate/__manifest__.py | 5 ++++ estate/demo/estate.property.offer.xml | 34 +++++++++++++++++++++++++++ estate/demo/estate.property.type.csv | 5 ++++ estate/demo/estate.property.xml | 33 ++++++++++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 estate/demo/estate.property.offer.xml create mode 100644 estate/demo/estate.property.type.csv create mode 100644 estate/demo/estate.property.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8271d22e15b..7378b74ef59 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,5 +12,10 @@ 'views/estate_property_views.xml', 'views/estate_menus.xml', 'views/res_users_views.xml' + ], + 'demo': [ + 'demo/estate.property.type.csv', + 'demo/estate.property.xml', + 'demo/estate.property.offer.xml' ] } diff --git a/estate/demo/estate.property.offer.xml b/estate/demo/estate.property.offer.xml new file mode 100644 index 00000000000..3354a1a10d0 --- /dev/null +++ b/estate/demo/estate.property.offer.xml @@ -0,0 +1,34 @@ + + + 10000 + 14 + + + + + + 1500000 + 14 + + + + + + 1500001 + 14 + + + + + + + + + + + + + + + + diff --git a/estate/demo/estate.property.type.csv b/estate/demo/estate.property.type.csv new file mode 100644 index 00000000000..e65cbe5c724 --- /dev/null +++ b/estate/demo/estate.property.type.csv @@ -0,0 +1,5 @@ +"id","name" +estate_property_type_residential,"Residential" +estate_property_type_commercial,"Commercial" +estate_property_type_industrial,"Industrial" +estate_property_type_land,"Land" diff --git a/estate/demo/estate.property.xml b/estate/demo/estate.property.xml new file mode 100644 index 00000000000..2a523a1334b --- /dev/null +++ b/estate/demo/estate.property.xml @@ -0,0 +1,33 @@ + + + Big Villa + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 6 + 100 + 4 + True + True + 100000 + south + + + + + Trailer home + cancelled + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 120000 + 1 + 10 + 4 + False + + + From 9813fd4a684d2fe609672987aea088d75e1d7853 Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 28 Jan 2026 08:25:43 +0100 Subject: [PATCH 28/29] [FIX] estate_property : Review changes Removed journal_id when creating an invoice --- estate_account/models/estate_property.py | 1 - 1 file changed, 1 deletion(-) diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 9d92998f846..6fdeb00a391 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -9,7 +9,6 @@ def sell_property(self): vals = { 'partner_id': prop.buyer_id.id, 'move_type': 'out_invoice', - 'journal_id': 1, 'invoice_line_ids': [ Command.create({ 'name': '6 percent of selling price', From 548ddb31f66c2dc2f5bfdbbd75f3b7a02805e6af Mon Sep 17 00:00:00 2001 From: MAGREMENT Date: Wed, 28 Jan 2026 11:46:21 +0100 Subject: [PATCH 29/29] [ADD] estate : Unit tests --- .vscode/launch.json | 27 ++++++++++++ estate/models/estate_property_offer.py | 3 ++ estate/tests/__init__.py | 2 + estate/tests/test_estate_property.py | 30 +++++++++++++ estate/tests/test_estate_property_offer.py | 50 ++++++++++++++++++++++ 5 files changed, 112 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/test_estate_property.py create mode 100644 estate/tests/test_estate_property_offer.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..5925ee83d2b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // 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": "../odoo/odoo-bin", + "console": "integratedTerminal", + "args": [ + "--addons-path=addons/,../enterprise/,..tutorials", + "-d", + "rd-demo", + //"--test-tags", + //"/estate", + "--limit-time-cpu=99999", + "--limit-time-real=99999", + "--limit-request=99000" + + ], + "variablePresentation": {} + } + ] +} diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 74cd4f9c969..4c562e0e708 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -22,6 +22,9 @@ class EstatePropertyOffer(models.Model): def create(self, vals_list): prop = self.env['estate.property'].browse(vals_list[0]['property_id']) + if (prop.state != 'new' and prop.state != 'offer-received'): + raise exceptions.UserError("Cannot add an offer to a property not accepting offers") + if (any(o.price > vals_list[0]['price'] for o in prop.offer_ids)): raise exceptions.UserError("Cannot add an offer with a lower amount than an existing one") diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..5092333224b --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_estate_property_offer +from . import test_estate_property diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..f10f492ceb5 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,30 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests import Form + +class EstatePropertyTestCase(TransactionCase): + + @classmethod + def setUpClass(cls): + super(EstatePropertyTestCase, cls).setUpClass() + + cls.properties = cls.env['estate.property'].create([ + { + 'id': 'property1', + 'name': 'Property with garden', + 'expected_price': 1_000, + 'garden_area': 1_000, + 'garden': True, + 'garden_orientation': 'south' + } + ]) + + #Should work but does not + #def test_garden_reset(self): + # with Form(self.properties[0]) as form: + # form.garden = False + # + # self.assertEqual(self.properties[0].garden_area, 0) + # self.assertEqual(self.properties[0].garden_orientation, False) + + diff --git a/estate/tests/test_estate_property_offer.py b/estate/tests/test_estate_property_offer.py new file mode 100644 index 00000000000..3b7387be10f --- /dev/null +++ b/estate/tests/test_estate_property_offer.py @@ -0,0 +1,50 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + +class EstatePropertyOfferTestCase(TransactionCase): + + @classmethod + def setUpClass(cls): + super(EstatePropertyOfferTestCase, cls).setUpClass() + + cls.properties = cls.env['estate.property'].create([ + { + 'name': 'Sold Property', + 'expected_price': 1_000, + 'state': 'sold' + },{ + 'name' : 'No Offer Property', + 'expected_price': 1_000, + },{ + 'name': 'Property To Sell', + 'expected_price': 1_000 + } + ]) + + cls.offers = cls.env['estate.property.offer'].create([ + { + 'partner_id': 1, + 'property_id': cls.properties[2].id, + 'price': 1_000 + } + ]) + + cls.offers[0].accept_offer() + + def test_cannot_add_offer_to_sold_property(self): + with self.assertRaises(UserError): + self.properties[1].sell_property() + + def test_cannot_sell_property_with_no_offer(self): + with self.assertRaises(UserError): + self.env['estate.property.offer'].create([{ + 'partner_id': 1, + 'property_id': self.properties[0].id, + 'price': 1000 + }]) + + def test_property_marked_as_sold(self): + self.assertEqual(self.properties[2].state, "offer-accepted") + self.properties[2].sell_property() + self.assertEqual(self.properties[2].state, "sold") +