From 029b4b54f1509caff926e5d929c072b84a9b99e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Tue, 21 Apr 2026 15:04:21 +0200 Subject: [PATCH 01/22] [ADD] estate: chapters 1 -> 4 --- estate/__init__.py | 1 + estate/__manifest__.py | 18 ++++++++++++++++++ estate/data/ir.model.access.csv | 2 ++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 20 ++++++++++++++++++++ estate/views/estate_property_views.xml | 10 ++++++++++ 6 files changed, 52 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/data/ir.model.access.csv create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..a737a32f420 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,18 @@ +{ + 'name': "Estate", + 'version': '69.0', + 'depends': ['base'], + 'author': "Stef Ossé", + 'category': 'Category', + 'description': """ + A specialized application for **estate management**. + """, + # data files always loaded at installation + 'data': [ + 'data/ir.model.access.csv', + 'views/estate_property_views.xml', + ], + # data files containing optionally loaded demonstration data + 'demo': [], + 'application': True +} \ No newline at end of file diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv new file mode 100644 index 00000000000..d9d6ba57cc5 --- /dev/null +++ b/estate/data/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 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..2648b816c8b --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,20 @@ +from odoo import models, fields + + +class Property(models.Model): + _name = 'estate.property' + _description = 'Test Model for the Estate App' + + name = fields.Char("Name", required=True) + description = fields.Text("Description") + postcode = fields.Char("Postcode") + date_availability = fields.Date("Date Availability") + expected_price = fields.Float("Expected Price", required=True) + selling_price = fields.Float("Selling Price") + bedrooms = fields.Integer("Bedrooms") + living_area = fields.Integer("Living Area") + facades = fields.Integer("Facades") + garage = fields.Boolean("Garage") + garden = fields.Boolean("Garden") + garden_area = fields.Integer("Garden Area") + garden_orientation = fields.Selection(string="Orientation", selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")]) \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..593c6f15af6 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,10 @@ + + + + + Estate Property + estate.property + list,form + + + \ No newline at end of file From 7999a7eb6557c3d59e3f7705f0ea08cfe88ed755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Tue, 21 Apr 2026 16:32:18 +0200 Subject: [PATCH 02/22] [IMP] estate: chapter 5 --- estate/__init__.py | 2 +- estate/__manifest__.py | 3 ++- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 17 ++++++++++++----- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 14 ++++++-------- 6 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 estate/views/estate_menus.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 a737a32f420..6ff891356db 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,8 +11,9 @@ 'data': [ 'data/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], # data files containing optionally loaded demonstration data 'demo': [], 'application': True -} \ No newline at end of file +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 2648b816c8b..4c251b37bb3 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,7 @@ -from odoo import models, fields +import datetime +from odoo import models, fields +import odoo.tools.date_utils as date_utils class Property(models.Model): _name = 'estate.property' @@ -8,13 +10,18 @@ class Property(models.Model): name = fields.Char("Name", required=True) description = fields.Text("Description") postcode = fields.Char("Postcode") - date_availability = fields.Date("Date Availability") + date_availability = fields.Date("Date Availability", copy=False, default=date_utils.add(fields.Date.today() + date_utils.relativedelta(months=3))) expected_price = fields.Float("Expected Price", required=True) - selling_price = fields.Float("Selling Price") - bedrooms = fields.Integer("Bedrooms") + selling_price = fields.Float("Selling Price", readonly=True, copy=False) + + bedrooms = fields.Integer("Bedrooms", default=2) living_area = fields.Integer("Living Area") facades = fields.Integer("Facades") garage = fields.Boolean("Garage") + garden = fields.Boolean("Garden") garden_area = fields.Integer("Garden Area") - garden_orientation = fields.Selection(string="Orientation", selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")]) \ No newline at end of file + garden_orientation = fields.Selection(string="Orientation", selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")]) + + active = fields.Boolean("Active", default=True) + state = fields.Selection(string="State", selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")]) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..54bcf49b572 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 593c6f15af6..1d2a3aaa4cd 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,10 +1,8 @@ - - - Estate Property - estate.property - list,form - - - \ No newline at end of file + + Properties + estate.property + list,form + + From 5f79ad20b6a9173f401326723b8af3bf8d30b59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Wed, 22 Apr 2026 13:39:12 +0200 Subject: [PATCH 03/22] [IMP] estate: chapter 6 --- estate/__manifest__.py | 17 ++-- estate/models/estate_property.py | 65 +++++++++----- estate/{data => security}/ir.model.access.csv | 2 +- estate/views/estate_menus.xml | 10 +-- estate/views/estate_property_views.xml | 88 +++++++++++++++++-- 5 files changed, 142 insertions(+), 40 deletions(-) rename estate/{data => security}/ir.model.access.csv (62%) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6ff891356db..d1510438c65 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,15 +1,18 @@ { - 'name': "Estate", - 'version': '69.0', - 'depends': ['base'], - 'author': "Stef Ossé", + 'name': 'Estate', + 'version': '19.0.1.0.0', + 'depends': [ + 'base', + ], + 'author': 'Stef Ossé', + 'license': 'LGPL-3', 'category': 'Category', - 'description': """ + 'description': ''' A specialized application for **estate management**. - """, + ''', # data files always loaded at installation 'data': [ - 'data/ir.model.access.csv', + '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 4c251b37bb3..55c6830c141 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,27 +1,48 @@ -import datetime - from odoo import models, fields import odoo.tools.date_utils as date_utils + class Property(models.Model): _name = 'estate.property' - _description = 'Test Model for the Estate App' - - name = fields.Char("Name", required=True) - description = fields.Text("Description") - postcode = fields.Char("Postcode") - date_availability = fields.Date("Date Availability", copy=False, default=date_utils.add(fields.Date.today() + date_utils.relativedelta(months=3))) - expected_price = fields.Float("Expected Price", required=True) - selling_price = fields.Float("Selling Price", readonly=True, copy=False) - - bedrooms = fields.Integer("Bedrooms", default=2) - living_area = fields.Integer("Living Area") - facades = fields.Integer("Facades") - garage = fields.Boolean("Garage") - - garden = fields.Boolean("Garden") - garden_area = fields.Integer("Garden Area") - garden_orientation = fields.Selection(string="Orientation", selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")]) - - active = fields.Boolean("Active", default=True) - state = fields.Selection(string="State", selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")]) + _description = 'Estate Property' + + name = fields.Char(string='Title', required=True) + description = fields.Text() + postcode = fields.Char() + + date_availability = fields.Date( + copy=False, + default=lambda x: date_utils.add( + fields.Date.today() + date_utils.relativedelta(months=3) + ), + ) + + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + 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'), + ] + ) diff --git a/estate/data/ir.model.access.csv b/estate/security/ir.model.access.csv similarity index 62% rename from estate/data/ir.model.access.csv rename to estate/security/ir.model.access.csv index d9d6ba57cc5..32389642d4f 100644 --- a/estate/data/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 +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 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 54bcf49b572..6e4c74ce951 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,8 +1,8 @@ - + - - - + + + - \ No newline at end of file + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1d2a3aaa4cd..75d075c17c0 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,8 +1,86 @@ - + - - Properties - estate.property - list,form + + Properties + estate.property + list,form + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.form + estate.property + + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + +
From 4d2f502867a9b79ab08c93a235ac3c2722c4cfde Mon Sep 17 00:00:00 2001 From: Mathilde Pascal Date: Wed, 22 Apr 2026 15:07:18 +0200 Subject: [PATCH 04/22] [IMP] estate: Make it so that only ERP manager can delete property. --- estate/__manifest__.py | 2 +- estate/security/ir.model.access.csv | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index d1510438c65..d1529d63f17 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Estate', - 'version': '19.0.1.0.0', + 'version': '19.0.1.1.0', 'depends': [ 'base', ], diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 32389642d4f..1fa5db2ac68 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,3 @@ 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 +estate_property_access_manager,estate.property.manager,model_estate_property,base.group_system,1,1,1,1 +estate_property_access_user,estate.property.user,model_estate_property,base.group_user,1,1,1,0 From b9c45e8f9c8c031144cdb7b364f7b1f2ff05b03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Wed, 22 Apr 2026 15:49:37 +0200 Subject: [PATCH 05/22] [IMP] estate: chapter 7 --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 3 ++ estate/models/estate_property.py | 41 +++++++++++++------- estate/models/estate_property_offer.py | 13 +++++++ estate/models/estate_property_tag.py | 8 ++++ estate/models/estate_property_type.py | 8 ++++ estate/security/ir.model.access.csv | 6 +++ estate/views/estate_menus.xml | 4 ++ estate/views/estate_property_offer_views.xml | 30 ++++++++++++++ estate/views/estate_property_tag_views.xml | 23 +++++++++++ estate/views/estate_property_type_views.xml | 23 +++++++++++ estate/views/estate_property_views.xml | 30 ++++++++++---- 12 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index d1529d63f17..2b2a2e4db26 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -14,6 +14,9 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ], # data files containing optionally loaded demonstration data diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..09b2099fe84 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 \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 55c6830c141..d6f7a0adb31 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -3,46 +3,59 @@ class Property(models.Model): - _name = 'estate.property' - _description = 'Estate Property' + _name = "estate.property" + _description = "Estate Property" - name = fields.Char(string='Title', required=True) + # Basics + name = fields.Char(string="Title", required=True) description = fields.Text() - postcode = fields.Char() + tag_ids = fields.Many2many(comodel_name="estate.property.tag", string="Tags") + # Sales info + type_id = fields.Many2one(comodel_name="estate.property.type", string="Type") + postcode = fields.Char() date_availability = fields.Date( copy=False, default=lambda x: date_utils.add( fields.Date.today() + date_utils.relativedelta(months=3) ), ) - + offer_ids = fields.One2many(comodel_name="estate.property.offer", inverse_name="property_id") expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) + buyer_id = fields.Many2one(comodel_name="res.partner", string="Buyer", copy=False) + salesman_id = fields.Many2one( + comodel_name="res.users", + string="Salesman", + default=lambda self: self.env.user.id, + ) + # General information bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() + # Garden information garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection( selection=[ - ('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West'), + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), ] ) + # State active = fields.Boolean(default=True) state = fields.Selection( selection=[ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('cancelled', 'Cancelled'), + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), ] ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..d2cd97f1998 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,13 @@ +from odoo import models, fields + + +class PropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer' + + price = fields.Float() + status = fields.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..476e1358668 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class PropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'Estate Property Tag' + + name = fields.Char(string="Type", required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..ca90536c048 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class PropertyType(models.Model): + _name = 'estate.property.type' + _description = 'Estate Property Type' + + name = fields.Char(string="Type", required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 1fa5db2ac68..872396306db 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +1,9 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink estate_property_access_manager,estate.property.manager,model_estate_property,base.group_system,1,1,1,1 estate_property_access_user,estate.property.user,model_estate_property,base.group_user,1,1,1,0 +estate_property_type_access_manager,estate.property.type.manager,model_estate_property_type,base.group_system,1,1,1,1 +estate_property_type_access_user,estate.property.type.user,model_estate_property_type,base.group_user,1,1,1,0 +estate_property_tag_access_manager,estate.property.tag.manager,model_estate_property_tag,base.group_system,1,1,1,1 +estate_property_tag_access_user,estate.property.tag.user,model_estate_property_tag,base.group_user,1,1,1,0 +estate_property_offer_access_manager,estate.property.offer.manager,model_estate_property_offer,base.group_system,1,1,1,1 +estate_property_offer_access_user,estate.property.offer.user,model_estate_property_offer,base.group_user,1,1,1,0 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 6e4c74ce951..10adff8f39c 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..c1b2c84299d --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,30 @@ + + + + + estate.property.offer.view.form + estate.property.offer + +
+ + + + + +
+
+
+ + + estate.property.offer.view.list + estate.property.offer + + + + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..d78f3009b18 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,23 @@ + + + + Property Tag + estate.property.tag + list,form + + + + estate.property.tag.form + estate.property.tag + +
+
+

+ +

+
+
+
+
+ +
\ No newline at end of file diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..8101054bad3 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,23 @@ + + + + Property Type + estate.property.type + list,form + + + + estate.property.type.form + estate.property.type + +
+
+

+ +

+
+
+
+
+ +
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 75d075c17c0..ffc6cfd2c33 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,12 +1,12 @@ - Properties + All Properties estate.property list,form - + estate.property.list estate.property @@ -23,22 +23,25 @@ - + estate.property.form estate.property
+

- +

+ - + + @@ -51,13 +54,24 @@ - + - + + + + + + + + + + + + @@ -66,7 +80,7 @@ - + estate.property.search estate.property From 4909fb84c615fa16a1d413932513babe75a36f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Wed, 22 Apr 2026 15:58:37 +0200 Subject: [PATCH 06/22] [LINT] estate: fix the linting errors --- estate/models/__init__.py | 2 +- estate/models/estate_property_offer.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 09b2099fe84..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,4 +1,4 @@ from . import estate_property from . import estate_property_type from . import estate_property_tag -from . import estate_property_offer \ No newline at end of file +from . import estate_property_offer diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index d2cd97f1998..bee5f90daba 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -2,12 +2,17 @@ class PropertyOffer(models.Model): - _name = 'estate.property.offer' - _description = 'Estate Property Offer' + _name = "estate.property.offer" + _description = "Estate Property Offer" price = fields.Float() - status = fields.Selection([('accepted', 'Accepted'),('refused', 'Refused'),], copy=False) - - partner_id = fields.Many2one('res.partner', required=True) - property_id = fields.Many2one('estate.property', required=True) + status = fields.Selection( + [ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + copy=False, + ) + partner_id = fields.Many2one("res.partner", required=True) + property_id = fields.Many2one("estate.property", required=True) From 9a3a6606ea9e7e849ba3e6f6b01a694a19d563bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Wed, 22 Apr 2026 17:02:37 +0200 Subject: [PATCH 07/22] [IMP] estate: solve chapter 8 --- estate/models/estate_property.py | 66 +++++++++++++------- estate/models/estate_property_offer.py | 24 ++++++- estate/security/ir.model.access.csv | 2 +- estate/views/estate_property_offer_views.xml | 4 ++ estate/views/estate_property_views.xml | 2 + 5 files changed, 71 insertions(+), 27 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d6f7a0adb31..9d54c79d591 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import models, fields, api import odoo.tools.date_utils as date_utils @@ -6,37 +6,19 @@ class Property(models.Model): _name = "estate.property" _description = "Estate Property" - # Basics + # General information name = fields.Char(string="Title", required=True) description = fields.Text() tag_ids = fields.Many2many(comodel_name="estate.property.tag", string="Tags") - - # Sales info type_id = fields.Many2one(comodel_name="estate.property.type", string="Type") postcode = fields.Char() - date_availability = fields.Date( - copy=False, - default=lambda x: date_utils.add( - fields.Date.today() + date_utils.relativedelta(months=3) - ), - ) - offer_ids = fields.One2many(comodel_name="estate.property.offer", inverse_name="property_id") - expected_price = fields.Float(required=True) - selling_price = fields.Float(readonly=True, copy=False) - buyer_id = fields.Many2one(comodel_name="res.partner", string="Buyer", copy=False) - salesman_id = fields.Many2one( - comodel_name="res.users", - string="Salesman", - default=lambda self: self.env.user.id, - ) - # General information bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() + total_area = fields.Integer(compute="_compute_total_area") - # Garden information garden = fields.Boolean() garden_area = fields.Integer() garden_orientation = fields.Selection( @@ -48,8 +30,25 @@ class Property(models.Model): ] ) - # State - active = fields.Boolean(default=True) + # Sales info + date_availability = fields.Date( + copy=False, + default=lambda x: date_utils.add( + fields.Date.today() + date_utils.relativedelta(months=3) + ), + ) + offer_ids = fields.One2many( + comodel_name="estate.property.offer", inverse_name="property_id" + ) + expected_price = fields.Float(required=True) + best_offer = fields.Integer(compute="_compute_best_offer") + selling_price = fields.Float(readonly=True, copy=False) + buyer_id = fields.Many2one(comodel_name="res.partner", string="Buyer", copy=False) + salesman_id = fields.Many2one( + comodel_name="res.users", + string="Salesman", + default=lambda self: self.env.user.id, + ) state = fields.Selection( selection=[ ("new", "New"), @@ -59,3 +58,24 @@ class Property(models.Model): ("cancelled", "Cancelled"), ] ) + active = fields.Boolean(default=True) + + @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.price") + def _compute_best_offer(self): + best = max(self.offer_ids.mapped("price")) + for record in self: + record.best_offer = best + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = "north" + else: + self.garden_area = 0 + self.garden_orientation = None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index bee5f90daba..d618796cbd9 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,10 +1,11 @@ -from odoo import models, fields +import datetime +from odoo import models, fields, api class PropertyOffer(models.Model): _name = "estate.property.offer" _description = "Estate Property Offer" - + create_date = fields.Date(default=datetime.date.today()) price = fields.Float() status = fields.Selection( [ @@ -13,6 +14,23 @@ class PropertyOffer(models.Model): ], copy=False, ) - + validity = fields.Integer(default=7) + deadline = fields.Date(compute="_compute_deadline", inverse="_compute_validity") partner_id = fields.Many2one("res.partner", required=True) property_id = fields.Many2one("estate.property", required=True) + + @api.depends("validity", "create_date") + def _compute_deadline(self): + for record in self: + create_date = ( + record.create_date if record.create_date else datetime.date.today() + ) + record.deadline = create_date + datetime.timedelta(days=record.validity) + + @api.depends("deadline", "create_date") + def _compute_validity(self): + for record in self: + create_date = ( + record.create_date if record.create_date else datetime.date.today() + ) + record.validity = (record.deadline - create_date).days diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 872396306db..a76733405b1 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -2,7 +2,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink estate_property_access_manager,estate.property.manager,model_estate_property,base.group_system,1,1,1,1 estate_property_access_user,estate.property.user,model_estate_property,base.group_user,1,1,1,0 estate_property_type_access_manager,estate.property.type.manager,model_estate_property_type,base.group_system,1,1,1,1 -estate_property_type_access_user,estate.property.type.user,model_estate_property_type,base.group_user,1,1,1,0 +estate_property_type_access_user,estate.property.type.user,model_estate_property_type,base.group_user,1,0,0,0 estate_property_tag_access_manager,estate.property.tag.manager,model_estate_property_tag,base.group_system,1,1,1,1 estate_property_tag_access_user,estate.property.tag.user,model_estate_property_tag,base.group_user,1,1,1,0 estate_property_offer_access_manager,estate.property.offer.manager,model_estate_property_offer,base.group_system,1,1,1,1 diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index c1b2c84299d..ce4e168744a 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + + @@ -22,6 +24,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index ffc6cfd2c33..4aa7c0a349d 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -47,6 +47,7 @@ + @@ -61,6 +62,7 @@ + From e54ae312d9ac8bd08ada00696d4cb2a87839722b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Wed, 22 Apr 2026 17:13:24 +0200 Subject: [PATCH 08/22] [FIX] estate: default best offer to 0 when there are no offers --- estate/models/estate_property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9d54c79d591..9b386cfb642 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -67,7 +67,7 @@ def _compute_total_area(self): @api.depends("offer_ids.price") def _compute_best_offer(self): - best = max(self.offer_ids.mapped("price")) + best = 0 if not self.offer_ids else max(self.offer_ids.mapped("price")) for record in self: record.best_offer = best From 540d9b1f74a342621f6b6696da8e6514f47e481f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Wed, 22 Apr 2026 17:18:09 +0200 Subject: [PATCH 09/22] [FIX] estate: default best offer to 0 when there are no offers --- estate/models/estate_property.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9b386cfb642..c611e4ae46c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -51,9 +51,6 @@ class Property(models.Model): ) state = fields.Selection( selection=[ - ("new", "New"), - ("offer_received", "Offer Received"), - ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled"), ] @@ -67,7 +64,7 @@ def _compute_total_area(self): @api.depends("offer_ids.price") def _compute_best_offer(self): - best = 0 if not self.offer_ids else max(self.offer_ids.mapped("price")) + best = max([0, *self.offer_ids.mapped("price")]) for record in self: record.best_offer = best From 186b3e19116c8787bddeb2d61c6b01aec60eb649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Thu, 23 Apr 2026 10:30:30 +0200 Subject: [PATCH 10/22] [IMP] estate: chapter 9 Followed the web framework 101 tutorial and this is my solution for chapter 9 [LINT] estate: chapter 9 --- estate/models/estate_property.py | 43 +++++++++++++++----- estate/models/estate_property_offer.py | 27 ++++++++++++ estate/views/estate_property_offer_views.xml | 2 + estate/views/estate_property_views.xml | 14 +++++-- 4 files changed, 73 insertions(+), 13 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index c611e4ae46c..93791d0c8c3 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ from odoo import models, fields, api import odoo.tools.date_utils as date_utils +from odoo.exceptions import UserError class Property(models.Model): @@ -43,7 +44,9 @@ class Property(models.Model): expected_price = fields.Float(required=True) best_offer = fields.Integer(compute="_compute_best_offer") selling_price = fields.Float(readonly=True, copy=False) - buyer_id = fields.Many2one(comodel_name="res.partner", string="Buyer", copy=False) + buyer_id = fields.Many2one( + comodel_name="res.partner", string="Buyer", copy=False, readonly=True + ) salesman_id = fields.Many2one( comodel_name="res.users", string="Salesman", @@ -51,28 +54,48 @@ class Property(models.Model): ) state = fields.Selection( selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled"), - ] + ], + readonly=True, + default="new", ) active = fields.Boolean(default=True) - @api.depends("garden_area", 'living_area') + @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.price") def _compute_best_offer(self): - best = max([0, *self.offer_ids.mapped("price")]) + offers = [offer.price for offer in self.offer_ids if offer.status != "refused"] + best = max([0, *offers]) for record in self: record.best_offer = best @api.onchange("garden") def _onchange_garden(self): - if self.garden: - self.garden_area = 10 - self.garden_orientation = "north" - else: - self.garden_area = 0 - self.garden_orientation = None + for record in self: + if record.garden: + record.garden_area = 10 + record.garden_orientation = "north" + else: + record.garden_area = 0 + record.garden_orientation = None + + def action_sold(self): + for record in self: + if record.state == "cancelled": + raise UserError("A cancelled property cannot be sold") + record.state = "sold" + + def action_cancel(self): + for record in self: + if record.state == "sold": + raise UserError("A sold property cannot be cancelled") + + record.state = "cancelled" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index d618796cbd9..36763e11ee1 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,7 @@ import datetime + from odoo import models, fields, api +from odoo.exceptions import UserError class PropertyOffer(models.Model): @@ -34,3 +36,28 @@ def _compute_validity(self): record.create_date if record.create_date else datetime.date.today() ) record.validity = (record.deadline - create_date).days + + def action_accept(self): + for record in self: + if record.status == "refused": + raise UserError("A refused offer cannot be accepted") + if record.property_id.state == "cancelled": + raise UserError("An offer on a cancelled property cannot be accepted") + if record.property_id.state == "sold": + raise UserError("An offer on a sold property cannot be accepted") + + record.status = "accepted" + record.property_id.buyer_id = record.partner_id + record.property_id.selling_price = record.price + record.property_id.state = "sold" + + def action_refuse(self): + for record in self: + if record.status == "accepted": + raise UserError("An accepted offer cannot be refused") + if record.property_id.state == "cancelled": + raise UserError("An offer on a cancelled property cannot be refused") + if record.property_id.state == "sold": + raise UserError("An offer on a sold property cannot be refused") + + record.status = "refused" diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index ce4e168744a..129a84adc0b 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -27,6 +27,8 @@ + + + +
+

+ +

+
+ + + + + + + + + + + +
+ + estate.property.type.view.list + estate.property.type + + + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e71b627c950..31ee2ac7b45 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,6 +1,6 @@ - + All Properties estate.property list,form @@ -11,14 +11,18 @@ estate.property - + - + @@ -30,8 +34,11 @@
-
@@ -41,14 +48,14 @@ - + - - + @@ -68,14 +75,14 @@ - + - + @@ -95,15 +102,23 @@ estate.property - + + + + + + + + + From b12ddbd2ff4218e706a7a2ba43716659beeb5d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Mon, 27 Apr 2026 09:28:37 +0200 Subject: [PATCH 14/22] [FIX] estate: chapter 11 - runbot build error --- estate/models/estate_property_offer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 4e129408e9b..ebe7e9f1da4 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -59,10 +59,11 @@ def _compute_actions_visible(self): @api.model def write(self, vals): - super().write(vals) + result = super().write(vals) for record in self: if record.property_id.state not in ["sold", "cancelled"]: record.property_id.state = "offer_received" if record.status != "accepted" else "offer_accepted" + return result def action_accept(self): for record in self: From 47b19ae0e78e54154a941f1d0974b064ceb3e1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Mon, 27 Apr 2026 13:41:14 +0200 Subject: [PATCH 15/22] [IMP] estate: chapter 12 --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 7 +++++++ estate/models/estate_property_offer.py | 14 ++++++++++---- estate/models/res_users.py | 11 +++++++++++ estate/views/estate_property_views.xml | 19 +++++++++++-------- estate/views/res_users_views.xml | 15 +++++++++++++++ 7 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 603cccbafea..992b85ee631 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -17,6 +17,7 @@ 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', + 'views/res_users_views.xml', 'views/estate_menus.xml', ], # data files containing optionally loaded demonstration data diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 649eaac393b..0db8267c57f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -102,6 +102,13 @@ def _onchange_garden(self): record.garden_area = 0 record.garden_orientation = None + @api.ondelete(at_uninstall=False) + def delete(self): + for record in self: + if record.state not in ["new", "cancelled"]: + raise UserError("Only new and cancelled properties can be deleted.") + return super().unlink() + def action_sold(self): for record in self: if record.state == "cancelled": diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index ebe7e9f1da4..561c026c36d 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -2,7 +2,7 @@ from odoo import models, fields, api -from odoo.exceptions import UserError +from odoo.exceptions import UserError, AccessError class PropertyOffer(models.Model): @@ -57,13 +57,19 @@ def _compute_actions_visible(self): and record.status == "proposed" ) - @api.model def write(self, vals): - result = super().write(vals) + best_price = max(self.property_id.offer_ids.mapped("price"), default=0) for record in self: + if record.price < best_price: + raise UserError("You cannot go under the price of the current best offer") + if record.property_id.state not in ["sold", "cancelled"]: record.property_id.state = "offer_received" if record.status != "accepted" else "offer_accepted" - return result + else: + raise AccessError("You cannot create an offer on a sold or cancelled property.") + + return super().write(vals) + def action_accept(self): for record in self: diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..92d953e8541 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesman_id", + domain=[("state", "not in", ["sold", "cancelled"])] + ) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 31ee2ac7b45..2c82e864f16 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -16,13 +16,15 @@ decoration-bf="state == 'offer_accepted'" decoration-muted="state == 'sold'" > - - - - - - - + + + + + + + + + @@ -107,7 +109,8 @@ - + + + + res.users.form.estate.extention + res.users + + + + + + + + + + \ No newline at end of file From 403076587d15663a83a8f5a342e36b3c9b58dbef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Mon, 27 Apr 2026 13:47:14 +0200 Subject: [PATCH 16/22] [LINT] estate: chapter 12 --- estate/models/estate_property_offer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 561c026c36d..fd9792487e5 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -70,7 +70,6 @@ def write(self, vals): return super().write(vals) - def action_accept(self): for record in self: if record.status == "refused": From 15777fc385c89185e046febcfe687c86786ec531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Mon, 27 Apr 2026 15:25:31 +0200 Subject: [PATCH 17/22] [ADD] estate_account: chapter 13 [LINT] estate_account: chapter 13 --- estate/models/estate_property.py | 4 +-- estate/views/estate_property_views.xml | 4 +-- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 20 ++++++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_account.py | 43 +++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_account.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0db8267c57f..5febe297d7f 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -109,13 +109,13 @@ def delete(self): raise UserError("Only new and cancelled properties can be deleted.") return super().unlink() - def action_sold(self): + def sold_action(self): for record in self: if record.state == "cancelled": raise UserError("A cancelled property cannot be sold") record.state = "sold" - def action_cancel(self): + def cancel_action(self): for record in self: if record.state == "sold": raise UserError("A sold property cannot be cancelled") diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 2c82e864f16..3cf15c5f309 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -36,9 +36,9 @@
-
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..b6c39be4d83 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': 'Estate Accounts', + 'version': '19.0.1.1.0', + 'depends': [ + 'base', + 'account', + ], + 'author': 'Stef Ossé', + 'license': 'LGPL-3', + 'category': 'Category', + 'description': ''' + An extention for the application for **estate management**. + ''', + # data files always loaded at installation + 'data': [ + ], + # data files containing optionally loaded demonstration data + 'demo': [], + 'application': True +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..02b688798a3 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_account diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py new file mode 100644 index 00000000000..a4db8afab0c --- /dev/null +++ b/estate_account/models/estate_account.py @@ -0,0 +1,43 @@ +from odoo import models +from odoo.orm.commands import Command + + +class EstateAccount(models.Model): + _inherit = "estate.property" + + def sold_action(self): + for order in self: + selling_price, buyer_id = max( + ( + (x.price, x.partner_id) + for x in order.offer_ids + if x.status != "refused" + ), + default=(None, None), + key=lambda x: x[0], + ) + + invoice_vals = { + "move_type": "out_invoice", + "partner_id": buyer_id.id, + "invoice_line_ids": [ + Command.create( + { + "name": "Down Payment (6% of the selling price)", + "quantity": 1, + "price_unit": 0.06 * selling_price, + } + ), + Command.create( + { + "name": "Administrative Fees", + "quantity": 1, + "price_unit": 100, + } + ), + ], + } + + self.env["account.move"].create(invoice_vals) + + return super().sold_action() From 0748f46eb6b4c3e12fe2275ccb175bf1ae7ada33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Mon, 27 Apr 2026 17:11:36 +0200 Subject: [PATCH 18/22] [IMP] estate: chapter 14 [LINT] estate: chapter 15 --- estate/models/estate_property.py | 16 +++--- estate/models/estate_property_offer.py | 8 ++- estate/models/estate_property_tag.py | 3 ++ estate/models/estate_property_type.py | 4 ++ estate/models/res_users.py | 2 + estate/views/estate_property_tag_views.xml | 4 +- estate/views/estate_property_type_views.xml | 2 +- estate/views/estate_property_views.xml | 55 ++++++++++++++++++--- estate_account/models/estate_account.py | 4 ++ 9 files changed, 80 insertions(+), 18 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 5febe297d7f..f8329e0775e 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,17 +5,17 @@ class Property(models.Model): + # Private attributes _name = "estate.property" _description = "Estate Property" _order = "id desc" - # General information + # Field declarations name = fields.Char(string="Title", required=True) description = fields.Text() tag_ids = fields.Many2many("estate.property.tag") type_id = fields.Many2one("estate.property.type") postcode = fields.Char() - bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() @@ -31,7 +31,6 @@ class Property(models.Model): ("west", "West"), ] ) - # Sales info date_availability = fields.Date( copy=False, default=lambda x: fields.Date.today() + date_utils.relativedelta(months=3), @@ -57,6 +56,7 @@ class Property(models.Model): ) active = fields.Boolean(default=True) + # SQL constraints and indexes _check_expected_price = models.Constraint( "CHECK(expected_price > 0)", "The expected price should be a positive number." ) @@ -65,6 +65,7 @@ class Property(models.Model): "CHECK(selling_price > 0)", "The selling_price should be a positive number." ) + # Compute, inverse and search methods @api.depends("garden_area", "living_area") def _compute_total_area(self): for record in self: @@ -76,6 +77,7 @@ def _compute_best_offer(self): offers = record.offer_ids.filtered(lambda offer: offer.status != "refused") record.best_offer = max(offers.mapped("price"), default=0) + # Constrains methods and onchange methods @api.constrains("selling_price") def _check_selling_price(self): for record in self: @@ -102,20 +104,22 @@ def _onchange_garden(self): record.garden_area = 0 record.garden_orientation = None + # CRUD methods @api.ondelete(at_uninstall=False) - def delete(self): + def _delete(self): for record in self: if record.state not in ["new", "cancelled"]: raise UserError("Only new and cancelled properties can be deleted.") return super().unlink() - def sold_action(self): + # Action methods + def action_sold(self): for record in self: if record.state == "cancelled": raise UserError("A cancelled property cannot be sold") record.state = "sold" - def cancel_action(self): + def action_cancel(self): for record in self: if record.state == "sold": raise UserError("A sold property cannot be cancelled") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index fd9792487e5..2eed448295b 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,15 +1,16 @@ import datetime - from odoo import models, fields, api from odoo.exceptions import UserError, AccessError class PropertyOffer(models.Model): + # Private attributes _name = "estate.property.offer" _description = "Estate Property Offer" _order = "price desc" + # Field declarations create_date = fields.Date(default=lambda self: datetime.date.today()) price = fields.Float() status = fields.Selection( @@ -30,10 +31,12 @@ class PropertyOffer(models.Model): related="property_id.type_id", comodel_name="estate.property.type" ) + # SQL constraints and indexes _check_price = models.Constraint( "CHECK(price > 0)", "The price should be positive." ) + # Compute, inverse and search methods @api.depends("validity", "create_date") def _compute_deadline(self): for record in self: @@ -57,6 +60,8 @@ def _compute_actions_visible(self): and record.status == "proposed" ) + # Constrains methods and onchange methods + # CRUD methods def write(self, vals): best_price = max(self.property_id.offer_ids.mapped("price"), default=0) for record in self: @@ -70,6 +75,7 @@ def write(self, vals): return super().write(vals) + # Action methods def action_accept(self): for record in self: if record.status == "refused": diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index e2f502cf5ee..bb25d247299 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -2,13 +2,16 @@ class PropertyTag(models.Model): + # Private attributes _name = 'estate.property.tag' _description = 'Estate Property Tag' _order = 'name' + # Field declarations name = fields.Char(required=True) color = fields.Integer() + # SQL constraints and indexes _check_name = models.Constraint( "unique(name)", "Tag name should be unique" ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index f109c38d8e5..a7797c00da2 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -2,20 +2,24 @@ class PropertyType(models.Model): + # Private attributes _name = 'estate.property.type' _description = 'Estate Property Type' _order = 'sequence' + # Field declarations name = fields.Char(string="Type", required=True) sequence = fields.Integer() properties_id = fields.One2many(comodel_name="estate.property", inverse_name="type_id") offer_ids = fields.One2many(comodel_name="estate.property.offer", inverse_name="property_type_id") offer_count = fields.Integer(compute="_compute_offer_count") + # SQL constraints and indexes _check_name = models.Constraint( 'unique(name)', "The property type name should be unique" ) + # Compute, inverse and search methods @api.depends('offer_ids') def _compute_offer_count(self): for record in self: diff --git a/estate/models/res_users.py b/estate/models/res_users.py index 92d953e8541..a94ade46a75 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -2,8 +2,10 @@ class ResUsers(models.Model): + # Private attributes _inherit = "res.users" + # Field declarations property_ids = fields.One2many( "estate.property", "salesman_id", diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index 812924f9660..aeb218fbf8f 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -7,7 +7,7 @@ - estate.property.tag.form + estate.property.tag.view.form estate.property.tag @@ -21,7 +21,7 @@ - estate.property.tag.list + estate.property.tag.view.list estate.property.tag diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index d6c5aa20fda..f613387e7a3 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -7,7 +7,7 @@ - estate.property.type.form + estate.property.type.view.form estate.property.type diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3cf15c5f309..9d89a3b84d5 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,13 +3,12 @@ All Properties estate.property - list,form + list,kanban,form - estate.property.list + estate.property.view.list estate.property - - + @@ -29,16 +28,56 @@ + + estate.property.kanban + estate.property + + + + + +
+
+ + + +
+
+
+ Expected Price: € + + +
+ Best Offer: € + +
+ +
+ Selling Price: € + +
+
+
+
+ +
+
+
+
+
+
+
+ - estate.property.form + estate.property.view.form estate.property
-
@@ -100,7 +139,7 @@
- estate.property.search + estate.property.view.search estate.property diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py index a4db8afab0c..7fa2b46c158 100644 --- a/estate_account/models/estate_account.py +++ b/estate_account/models/estate_account.py @@ -1,4 +1,5 @@ from odoo import models +from odoo.exceptions import UserError from odoo.orm.commands import Command @@ -17,6 +18,9 @@ def sold_action(self): key=lambda x: x[0], ) + if not buyer_id or not selling_price: + raise UserError("You cannot sell without a buyer") + invoice_vals = { "move_type": "out_invoice", "partner_id": buyer_id.id, From 9afdf54c5d78d1e139c87a2ba452dc2631f92b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Tue, 28 Apr 2026 15:00:13 +0200 Subject: [PATCH 19/22] [IMP] estate: implement requested changes --- estate/models/estate_property_offer.py | 49 ++++++++++++++++--------- estate_account/models/estate_account.py | 19 ++++------ 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 2eed448295b..cbc560e7a6a 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -2,6 +2,7 @@ from odoo import models, fields, api from odoo.exceptions import UserError, AccessError +from odoo.tools import float_utils class PropertyOffer(models.Model): @@ -62,28 +63,44 @@ def _compute_actions_visible(self): # Constrains methods and onchange methods # CRUD methods - def write(self, vals): - best_price = max(self.property_id.offer_ids.mapped("price"), default=0) - for record in self: - if record.price < best_price: + def create(self, vals): + for record in vals: + property = self.env["estate.property"].browse(record["property_id"]) + best_price = float(max(property.offer_ids.mapped("price"), default=0.0)) + + if ("price" not in record + or float_utils.float_compare( + float(record["price"]), best_price, precision_digits=2 + ) < 0): raise UserError("You cannot go under the price of the current best offer") - if record.property_id.state not in ["sold", "cancelled"]: - record.property_id.state = "offer_received" if record.status != "accepted" else "offer_accepted" + if property.state not in ["sold", "offer_accepted", "cancelled"]: + property.state = "offer_received" else: raise AccessError("You cannot create an offer on a sold or cancelled property.") + return super().create(vals) + + def write(self, vals): + if "price" in vals: + for record in self: + best_price = float(max(record.property_id.offer_ids.mapped("price"), default=0.0)) + + if (float_utils.float_compare(float(vals["price"]), best_price, precision_digits=2) < 0): + raise UserError("You cannot go under the price of the current best offer") + + if record.property_id.state not in ["sold", "cancelled"]: + record.property_id.state = "offer_received" + else: + raise AccessError("You cannot create an offer on a sold or cancelled property.") + return super().write(vals) # Action methods def action_accept(self): for record in self: - if record.status == "refused": - raise UserError("A refused offer cannot be accepted") - if record.property_id.state == "cancelled": - raise UserError("An offer on a cancelled property cannot be accepted") - if record.property_id.state == "sold": - raise UserError("An offer on a sold property cannot be accepted") + if record.status == "refused" or record.property_id.state in ['cancelled', 'sold']: + raise UserError("Invalid Action!") record.status = "accepted" record.property_id.buyer_id = record.partner_id @@ -92,11 +109,7 @@ def action_accept(self): def action_refuse(self): for record in self: - if record.status == "accepted": - raise UserError("An accepted offer cannot be refused") - if record.property_id.state == "cancelled": - raise UserError("An offer on a cancelled property cannot be refused") - if record.property_id.state == "sold": - raise UserError("An offer on a sold property cannot be refused") + if record.status == "accepted" or record.property_id.state in ["cancelled", "sold"]: + raise UserError("Invalid Action") record.status = "refused" diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py index 7fa2b46c158..2201bbb5dcc 100644 --- a/estate_account/models/estate_account.py +++ b/estate_account/models/estate_account.py @@ -8,18 +8,13 @@ class EstateAccount(models.Model): def sold_action(self): for order in self: - selling_price, buyer_id = max( - ( - (x.price, x.partner_id) - for x in order.offer_ids - if x.status != "refused" - ), - default=(None, None), - key=lambda x: x[0], - ) - - if not buyer_id or not selling_price: - raise UserError("You cannot sell without a buyer") + best_offer = order.offer_ids.filtered(lambda x: x.status == "accepted").sorted("price DESC")[:1] + + if not best_offer: + raise UserError("You cannot sell without an accepted offer") + + buyer_id = best_offer[0].buyer_id + selling_price = best_offer[0].selling_price invoice_vals = { "move_type": "out_invoice", From d0ccf91f6310f9cae274d027f4f074f0400bbcb7 Mon Sep 17 00:00:00 2001 From: "David Van Droogenbroeck (DROD)" Date: Tue, 28 Apr 2026 15:11:43 +0200 Subject: [PATCH 20/22] [IMP] estate: add basic test cases This commit is here to introduce the testing framework of Odoo. Try running the tests using `--test-tags :TestEstateProperty`. Doc: https://www.odoo.com/documentation/18.0/developer/reference/backend/testing.html?highlight=tests#invocation The tests were made such that the first one should work but the second one should fail. Your job is to ensure both tests pass in the end. You should update the behaviour of the appropriate models. If you want, you can also add a small test of your own to get a feel for it. --- estate/tests/__init__.py | 1 + estate/tests/test_estate_property.py | 42 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 estate/tests/__init__.py create mode 100644 estate/tests/test_estate_property.py diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..18f3a50c3e1 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_estate_property \ No newline at end of file diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..7202f8b67a0 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,42 @@ +from odoo.exceptions import ValidationError +from odoo.tests import TransactionCase +from odoo import Command + + +class TestEstateProperty(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.estate = cls.env['estate.property'].create({ + 'name': 'Super test estate', + 'expected_price': 100000.0, + 'state': 'new', + }) + cls.test_partner = cls.env['res.partner'].create({ + 'name': 'Maman ours', + }) + + def test_estate_best_price(self): + ''' + Ensure best price is correctly updated when an offer is received. + ''' + self.assertEqual(self.estate.best_price, 0.0) + self.estate.offer_ids = [Command.create({ + 'price': 125000.0, + 'partner_id': self.test_partner.id, + })] + self.assertEqual(self.estate.best_price, 125000.0) + + def test_accept_offer_south_facing_garden(self): + ''' + Ensure offers for estates with south-facing gardens can only be accepted if above expected + price. + ''' + self.estate.expected_price = 500000 + self.estate.offer_ids = [Command.create({ + 'price': 475000.0, + 'partner_id': self.test_partner.id, + })] + with self.assertRaises(ValidationError): + self.estate.offer_ids.accept_offer() From 228602285eb4bae35c789d378845040e3ac68a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Tue, 28 Apr 2026 16:38:06 +0200 Subject: [PATCH 21/22] [IMP] estate: implement requested changes [LINT] estate: runbot linting errors --- estate/models/estate_property.py | 3 ++- estate/models/estate_property_offer.py | 28 +++++++++++++------------- estate/tests/__init__.py | 2 +- estate/tests/test_estate_property.py | 12 +++++------ 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f8329e0775e..254b01444ed 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -29,7 +29,8 @@ class Property(models.Model): ("south", "South"), ("east", "East"), ("west", "West"), - ] + ], + default='south' ) date_availability = fields.Date( copy=False, diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index cbc560e7a6a..8699121436f 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -64,20 +64,18 @@ def _compute_actions_visible(self): # Constrains methods and onchange methods # CRUD methods def create(self, vals): - for record in vals: - property = self.env["estate.property"].browse(record["property_id"]) + properties = self.env["estate.property"].browse(record["property_id"] for record in vals) + + if any(properties.mapped(lambda x: x.state in ["sold", "offer_accepted", "cancelled"])): + raise AccessError("You cannot create an offer on a sold/cancelled property or when there\'s already an offer accepted.") + + for property, record in zip(properties, vals): best_price = float(max(property.offer_ids.mapped("price"), default=0.0)) - if ("price" not in record - or float_utils.float_compare( - float(record["price"]), best_price, precision_digits=2 - ) < 0): + if "price" not in record or float_utils.float_compare(float(record["price"]), best_price, precision_digits=2) < 0: raise UserError("You cannot go under the price of the current best offer") - if property.state not in ["sold", "offer_accepted", "cancelled"]: - property.state = "offer_received" - else: - raise AccessError("You cannot create an offer on a sold or cancelled property.") + property.state = "offer_received" return super().create(vals) @@ -86,7 +84,7 @@ def write(self, vals): for record in self: best_price = float(max(record.property_id.offer_ids.mapped("price"), default=0.0)) - if (float_utils.float_compare(float(vals["price"]), best_price, precision_digits=2) < 0): + if float_utils.float_compare(float(vals["price"]), best_price, precision_digits=2) < 0: raise UserError("You cannot go under the price of the current best offer") if record.property_id.state not in ["sold", "cancelled"]: @@ -100,7 +98,10 @@ def write(self, vals): def action_accept(self): for record in self: if record.status == "refused" or record.property_id.state in ['cancelled', 'sold']: - raise UserError("Invalid Action!") + raise UserError("Can\'t accept a refused offer or accept an offer on a sold/cancelled property.") + + if float_utils.float_compare(record.price, record.property_id.expected_price, precision_digits=2) < 0: + raise UserError("You can\'t buy below the expected price") record.status = "accepted" record.property_id.buyer_id = record.partner_id @@ -110,6 +111,5 @@ def action_accept(self): def action_refuse(self): for record in self: if record.status == "accepted" or record.property_id.state in ["cancelled", "sold"]: - raise UserError("Invalid Action") - + raise UserError("Can\'t refuse an accepted offer or refuse an offer on a sold/cancelled property.") record.status = "refused" diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py index 18f3a50c3e1..576617cccff 100644 --- a/estate/tests/__init__.py +++ b/estate/tests/__init__.py @@ -1 +1 @@ -from . import test_estate_property \ No newline at end of file +from . import test_estate_property diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py index 7202f8b67a0..87805eb87d6 100644 --- a/estate/tests/test_estate_property.py +++ b/estate/tests/test_estate_property.py @@ -1,4 +1,4 @@ -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError from odoo.tests import TransactionCase from odoo import Command @@ -17,16 +17,16 @@ def setUpClass(cls): 'name': 'Maman ours', }) - def test_estate_best_price(self): + def test_estate_best_offer(self): ''' Ensure best price is correctly updated when an offer is received. ''' - self.assertEqual(self.estate.best_price, 0.0) + self.assertEqual(self.estate.best_offer, 0.0) self.estate.offer_ids = [Command.create({ 'price': 125000.0, 'partner_id': self.test_partner.id, })] - self.assertEqual(self.estate.best_price, 125000.0) + self.assertEqual(self.estate.best_offer, 125000.0) def test_accept_offer_south_facing_garden(self): ''' @@ -38,5 +38,5 @@ def test_accept_offer_south_facing_garden(self): 'price': 475000.0, 'partner_id': self.test_partner.id, })] - with self.assertRaises(ValidationError): - self.estate.offer_ids.accept_offer() + with self.assertRaises(UserError): + self.estate.offer_ids.action_accept() From fe454f7e3e68b2dc1cbeb34645fe4d30408376e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stef=20Oss=C3=A9?= Date: Wed, 29 Apr 2026 17:29:53 +0200 Subject: [PATCH 22/22] [IMP] awesome_owl: solutions chapter 1 --- awesome_owl/static/src/card/card.js | 25 +++++++++++ awesome_owl/static/src/card/card.xml | 17 +++++++ awesome_owl/static/src/counter/counter.js | 20 +++++++++ awesome_owl/static/src/counter/counter.xml | 11 +++++ awesome_owl/static/src/playground.js | 22 ++++++++- awesome_owl/static/src/playground.xml | 17 +++++-- awesome_owl/static/src/todo/todo_item.js | 11 +++++ awesome_owl/static/src/todo/todo_item.xml | 12 +++++ awesome_owl/static/src/todo/todo_list.js | 52 ++++++++++++++++++++++ awesome_owl/static/src/todo/todo_list.xml | 15 +++++++ awesome_owl/static/src/utils.js | 8 ++++ 11 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml create mode 100644 awesome_owl/static/src/todo/todo_item.js create mode 100644 awesome_owl/static/src/todo/todo_item.xml create mode 100644 awesome_owl/static/src/todo/todo_list.js create mode 100644 awesome_owl/static/src/todo/todo_list.xml create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..b6d13c8bcfd --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,25 @@ +import {Component, useState} from '@odoo/owl' + +export class Card extends Component { + static props = { + title: String, + slots: { type: Object }, + } + + setup(){ + this.state = useState({ + isOpen: true + }) + + this.props.isOpen = true + this.toggleOpen = this.toggleOpen.bind(this) + } + + toggleOpen(){ + console.log(this.state.isOpen); + this.state.isOpen = !this.state.isOpen + } + + static template = "awesome_owl.card" + +} \ No newline at end of file diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..89da97b9a52 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,17 @@ + + + +
+
+

+ +

+
+ +
+ +
+
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..eb4ab394670 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,20 @@ +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?.(); + } + +} \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..a3a20f40261 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,11 @@ + + + + +
+
Counter:
+ +
+
+ +
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..70f3b81bdbc 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/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + static props = {}; + + setup() { + this.state = useState({ + sum: 2, + todos:[] + }) + + this.incrementSum = this.incrementSum.bind(this) + } + + incrementSum() { + this.state.sum++; + } + + + static components = {Counter, Card, TodoList}; } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..7ae12682be8 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,9 +1,20 @@ - -
- hello world +
+

Welcome everyone

+

Click the button below and be amazed!

+ +
As you can see next to this card is a counter, don't hesitate to use it.
+
+ + + + +
+

The sum of the counters is:

+
+
diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js new file mode 100644 index 00000000000..e2fa22b835b --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,11 @@ +import {Component} from "@odoo/owl" + +export class TodoItem extends Component{ + static template = "awesome_owl.todo_item" + static props = { + todo: {type: {id: Number, description: String, isCompleted: Boolean}}, + toggleCompleted: {type: Function, optional:true}, + deleteTodo: {type: Function, optional:true}, + } + +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml new file mode 100644 index 00000000000..a9000a9df1a --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,12 @@ + + + + + + . + + + +
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js new file mode 100644 index 00000000000..64b83446f0d --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,52 @@ +import {Component, useState, useRef, onMounted} from '@odoo/owl' +import {TodoItem} from "./todo_item"; +import {useAutofocus} from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static props = {}; + + setup() { + this.state = useState({ + todos: [], + ids: 1 + }) + useAutofocus("input"); + + this.addTodo = this.addTodo.bind(this); + this.toggleCompleted = this.toggleCompleted.bind(this); + this.deleteTodo = this.deleteTodo.bind(this); + } + + addTodo(event) { + if (event.keyCode === 13) { + this.state.todos.push({id: this.state.ids, description: event.target.value, isCompleted: false}); + this.state.ids++; + event.target.value = ""; + } + } + + toggleCompleted(todo_id){ + const todo_pos = this.findTodo(todo_id); + if (todo_pos < 0) return; + const todo = this.state.todos[todo_pos] + todo.isCompleted = !todo.isCompleted; + } + + deleteTodo(todo_id){ + const todo_pos = this.findTodo(todo_id); + if (todo_pos < 0) return + this.state.todos.splice(todo_pos, 1); + } + + findTodo(todo_id){ + let i = 0; + const num_todos = this.state.todos.length; + while (i < num_todos && this.state.todos[i].id !== todo_id) i += 1; + if (i === num_todos) return -1; + + return i; + } + + static components = {TodoItem}; +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml new file mode 100644 index 00000000000..b3f1b586980 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,15 @@ + + + +
+

Todo List

+ +
+ + + +
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..921fc11b52c --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,8 @@ +import {useRef, onMounted} from "@odoo/owl" + +export function useAutofocus(reference) { + const myRef = useRef(reference) + onMounted(() => { + myRef.el.focus() + }); +} \ No newline at end of file