From d13533282f4feeb206c339511ba47ebfc540fa46 Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Tue, 22 Apr 2025 10:59:25 +0200 Subject: [PATCH 01/17] [ADD] Chapter 2: A New Application --- estate/__init__.py | 0 estate/__manifest__.py | 23 +++++++++++++++++++++++ 2 files changed, 23 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 0000000000..e69de29bb2 diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 0000000000..3bcd23beed --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +{ + "name": "Real Estate", + + 'summary': """ + Starting module for "Discover the JS framework, chapter 1: Owl components" + """, + + 'description': """ + Starting module for "Discover the JS framework, chapter 1: Owl components" + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com", + + 'category': 'Tutorials/Real Estate', + 'version': '0.1', + + 'application': True, + 'installable': True, + + 'depends': ['base'] +} \ No newline at end of file From 26444e0f623a90e139ae6cbb6e02dae2ae2a7c98 Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Tue, 22 Apr 2025 11:56:06 +0200 Subject: [PATCH 02/17] [IMP] Chapter 3: Models And Basic Fields --- estate/__init__.py | 1 + estate/models/__init__.py | 3 +++ estate/models/estate_property.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py index e69de29bb2..9a7e03eded 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/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 0000000000..f2db223b7d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +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 0000000000..a710ac3b03 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = "estate_property" + _description = "Estate property" + + name = fields.Char(string='Name of the Property',required=True) + description = fields.Text(string='Description') + postcode = fields.Char(string='Postcode') + date_availability = fields.Date(string='Date of Availability') + expected_price = fields.Float(string='Expected Selling Price',required=True) + selling_price = fields.Float(string='Selling Price') + bedrooms = fields.Integer(string='Number of Bedrooms') + living_area = fields.Integer(string='Number of Living Areas') + facades = fields.Integer(string='Number of Facades') + garage = fields.Boolean(string='Has a garage') + garden = fields.Boolean(string='Has a garden') + garden_area = fields.Integer(string='Number of garden Areas') + garden_orientation =fields.Selection( + string='Orientation of the Garden', + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ]) \ No newline at end of file From a0927fe27835ad55360b30f8540e7b0c0ae6dfec Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Tue, 22 Apr 2025 13:44:53 +0200 Subject: [PATCH 03/17] [IMP] Chapter 4: Security - A Brief Introduction --- estate/__manifest__.py | 4 ++++ estate/security/ir.model.access.csv | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 3bcd23beed..945473e0c3 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -19,5 +19,9 @@ 'application': True, 'installable': True, + 'data': [ + 'security/ir.model.access.csv', + ], + 'depends': ['base'] } \ 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 0000000000..d2bf3f44ed --- /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 +state.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From 636c48fb2f9f871e329c0a357cebfa650c26d385 Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Tue, 22 Apr 2025 14:41:25 +0200 Subject: [PATCH 04/17] [IMP] Chapter 5: Finally, Some UI To Play With --- estate/__manifest__.py | 2 ++ estate/models/estate_property.py | 30 +++++++++++++++++++++----- estate/views/estate_menus.xml | 9 ++++++++ estate/views/estate_property_views.xml | 8 +++++++ 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 945473e0c3..195c5039c7 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -21,6 +21,8 @@ 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'depends': ['base'] diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a710ac3b03..af7dcb3d1d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from dateutil import relativedelta from odoo import fields, models @@ -10,10 +11,14 @@ class EstateProperty(models.Model): name = fields.Char(string='Name of the Property',required=True) description = fields.Text(string='Description') postcode = fields.Char(string='Postcode') - date_availability = fields.Date(string='Date of Availability') + date_availability = fields.Date( + string='Date of Availability', + copy=False, + default=fields.Date.today() + relativedelta.relativedelta(months=3) + ) expected_price = fields.Float(string='Expected Selling Price',required=True) - selling_price = fields.Float(string='Selling Price') - bedrooms = fields.Integer(string='Number of Bedrooms') + selling_price = fields.Float(string='Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer(string='Number of Bedrooms', default=2) living_area = fields.Integer(string='Number of Living Areas') facades = fields.Integer(string='Number of Facades') garage = fields.Boolean(string='Has a garage') @@ -25,5 +30,20 @@ class EstateProperty(models.Model): ('north', 'North'), ('south', 'South'), ('east', 'East'), - ('west', 'West') - ]) \ No newline at end of file + ('west', 'West'), + ], + ) + active = fields.Boolean(default=True) + state = fields.Selection( + string='State', + selection=[ + ('new', 'New'), + ('received', 'Offer Received'), + ('accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + required=True, + copy=False, + default='new', + ) \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 0000000000..fea710d74d --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ 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 0000000000..5700390f31 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Properties + estate_property + list,form + + \ No newline at end of file From 71cbb29b8a21898a3d9314d2ceedcd584dc73441 Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Tue, 22 Apr 2025 16:38:25 +0200 Subject: [PATCH 05/17] [IMP] Chapter 6: Basic Views --- estate/__init__.py | 2 +- estate/__manifest__.py | 37 +++++------- estate/models/__init__.py | 4 +- estate/models/estate_property.py | 63 ++++++++++---------- estate/views/estate_menus.xml | 2 +- estate/views/estate_property_views.xml | 82 ++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 57 deletions(-) diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded..0650744f6b 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 195c5039c7..adb27a1e32 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,29 +1,22 @@ -# -*- coding: utf-8 -*- { "name": "Real Estate", - - 'summary': """ + "summary": """ Starting module for "Discover the JS framework, chapter 1: Owl components" """, - - 'description': """ + "description": """ Starting module for "Discover the JS framework, chapter 1: Owl components" """, - - 'author': "Odoo", - 'website': "https://www.odoo.com", - - 'category': 'Tutorials/Real Estate', - 'version': '0.1', - - 'application': True, - 'installable': True, - - 'data': [ - 'security/ir.model.access.csv', - 'views/estate_property_views.xml', - 'views/estate_menus.xml', + "author": "Odoo", + "website": "https://www.odoo.com", + "license": "LGPL-3", + "category": "Tutorials/Real Estate", + "version": "0.1", + "application": True, + "installable": True, + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_menus.xml", ], - - 'depends': ['base'] -} \ No newline at end of file + "depends": ["base"], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f2db223b7d..5e1963c9d2 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - -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 af7dcb3d1d..a63a87f4ae 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- - from dateutil import relativedelta + from odoo import fields, models @@ -8,42 +7,46 @@ class EstateProperty(models.Model): _name = "estate_property" _description = "Estate property" - name = fields.Char(string='Name of the Property',required=True) - description = fields.Text(string='Description') - postcode = fields.Char(string='Postcode') + name = fields.Char(string="Name of the Property", required=True) + description = fields.Text(string="Description") + postcode = fields.Char(string="Postcode") date_availability = fields.Date( - string='Date of Availability', - copy=False, - default=fields.Date.today() + relativedelta.relativedelta(months=3) + string="Date of Availability", + copy=False, + default=fields.Date.today() + relativedelta.relativedelta(months=3), ) - expected_price = fields.Float(string='Expected Selling Price',required=True) - selling_price = fields.Float(string='Selling Price', readonly=True, copy=False) - bedrooms = fields.Integer(string='Number of Bedrooms', default=2) - living_area = fields.Integer(string='Number of Living Areas') - facades = fields.Integer(string='Number of Facades') - garage = fields.Boolean(string='Has a garage') - garden = fields.Boolean(string='Has a garden') - garden_area = fields.Integer(string='Number of garden Areas') - garden_orientation =fields.Selection( - string='Orientation of the Garden', + expected_price = fields.Float( + string="Expected Selling Price", required=True + ) + selling_price = fields.Float( + string="Selling Price", readonly=True, copy=False + ) + bedrooms = fields.Integer(string="Number of Bedrooms", default=2) + living_area = fields.Integer(string="Number of Living Areas") + facades = fields.Integer(string="Number of Facades") + garage = fields.Boolean(string="Has a garage") + garden = fields.Boolean(string="Has a garden") + garden_area = fields.Integer(string="Number of garden Areas") + garden_orientation = fields.Selection( + string="Orientation of the Garden", selection=[ - ('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West'), + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), ], ) active = fields.Boolean(default=True) state = fields.Selection( - string='State', + string="State", selection=[ - ('new', 'New'), - ('received', 'Offer Received'), - ('accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('cancelled', 'Cancelled'), + ("new", "New"), + ("received", "Offer Received"), + ("accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), ], required=True, copy=False, - default='new', - ) \ No newline at end of file + default="new", + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index fea710d74d..d36d3ef47c 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 5700390f31..1a7650a6a0 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,4 +5,86 @@ estate_property list,form + + + estate_property_list + estate_property + + + + + + + + + + + + + + + estate_property_form + estate_property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + utm.stage.view.search + estate_property + + + + + + + + + + + + + + + + \ No newline at end of file From 80ed9c69e23fca22c8caa805285f26897367a90a Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Wed, 23 Apr 2025 09:54:06 +0200 Subject: [PATCH 06/17] [IMP] Chapter 7: Relations Between Models --- estate/models/__init__.py | 7 +++- estate/models/estate_property.py | 24 ++++++++++++-- estate/models/estate_property_offer.py | 25 ++++++++++++++ estate/models/estate_property_tag.py | 8 +++++ estate/models/estate_property_type.py | 8 +++++ estate/security/ir.model.access.csv | 5 ++- estate/views/estate_menus.xml | 12 ++++++- estate/views/estate_property_views.xml | 45 +++++++++++++++++++++++--- 8 files changed, 125 insertions(+), 9 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 diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2..3683ff97b6 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,6 @@ -from . import estate_property +from . import ( + estate_property, + estate_property_offer, + estate_property_tag, + estate_property_type, +) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index a63a87f4ae..b41cf0be09 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -16,10 +16,13 @@ class EstateProperty(models.Model): default=fields.Date.today() + relativedelta.relativedelta(months=3), ) expected_price = fields.Float( - string="Expected Selling Price", required=True + string="Expected Selling Price", + required=True, ) selling_price = fields.Float( - string="Selling Price", readonly=True, copy=False + string="Selling Price", + readonly=True, + copy=False, ) bedrooms = fields.Integer(string="Number of Bedrooms", default=2) living_area = fields.Integer(string="Number of Living Areas") @@ -50,3 +53,20 @@ class EstateProperty(models.Model): copy=False, default="new", ) + + property_type_id = fields.Many2one( + "estate_property_type", + string="Property Type", + ) + users_id = fields.Many2one( + "res.users", + string="Salesman", + default=lambda self: self.env.user, + ) + partner_id = fields.Many2one("res.partner", string="Buyer") + tag_ids = fields.Many2many("estate_property_tag", string="Property Tags") + offer_ids = fields.One2many( + "estate_property_offer", + "property_id", + string="Offers", + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 0000000000..8c85b2c082 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,25 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = "estate_property_offer" + _description = "Estate property Offer" + + price = fields.Float(string="Price") + status = fields.Selection( + selection=[ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + copy=False, + ) + partner_id = fields.Many2one( + "res.partner", + string="Buyer", + required=True, + ) + property_id = fields.Many2one( + "estate_property", + string="Property", + required=True, + ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 0000000000..4d15403bf1 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate_property_tag" + _description = "Estate property Tag" + + name = fields.Char(string="Name") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 0000000000..15a5405823 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate_property_type" + _description = "Estate property Type" + + name = fields.Char(string="Name") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index d2bf3f44ed..df0924ec88 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 -state.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +state.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index d36d3ef47c..6e88ad6bbe 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -3,7 +3,17 @@ - + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1a7650a6a0..f101ceadcb 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,11 +1,23 @@ - Properties + Property estate_property list,form + + Property Types + estate_property_type + list,form + + + + Property Tags + estate_property_tag + list,form + + estate_property_list estate_property @@ -22,6 +34,18 @@ + + estate_property_offer_list + estate_property_offer + + + + + + + + + estate_property_form estate_property @@ -32,14 +56,16 @@ + + - + @@ -56,14 +82,25 @@ + + + + + + + + + + + - - utm.stage.view.search + + estate_property_view_search estate_property From 6d622042dd08841cc7bd9aa1b367b198c13fba2a Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Wed, 23 Apr 2025 10:16:36 +0200 Subject: [PATCH 07/17] [REF] refactor _names for the models and split the views in multiples files depending on their model --- estate/__manifest__.py | 3 ++ estate/models/estate_property.py | 8 ++--- estate/models/estate_property_offer.py | 4 +-- estate/models/estate_property_tag.py | 2 +- estate/models/estate_property_type.py | 2 +- estate/security/ir.model.access.csv | 6 ++-- 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 | 32 +++----------------- 10 files changed, 48 insertions(+), 39 deletions(-) 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 adb27a1e32..77e127ff27 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -16,6 +16,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", ], "depends": ["base"], diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index b41cf0be09..6a4f636745 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -4,7 +4,7 @@ class EstateProperty(models.Model): - _name = "estate_property" + _name = "estate.property" _description = "Estate property" name = fields.Char(string="Name of the Property", required=True) @@ -55,7 +55,7 @@ class EstateProperty(models.Model): ) property_type_id = fields.Many2one( - "estate_property_type", + "estate.property.type", string="Property Type", ) users_id = fields.Many2one( @@ -64,9 +64,9 @@ class EstateProperty(models.Model): default=lambda self: self.env.user, ) partner_id = fields.Many2one("res.partner", string="Buyer") - tag_ids = fields.Many2many("estate_property_tag", string="Property Tags") + tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") offer_ids = fields.One2many( - "estate_property_offer", + "estate.property.offer", "property_id", string="Offers", ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 8c85b2c082..65f6fddcf0 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -2,7 +2,7 @@ class EstatePropertyOffer(models.Model): - _name = "estate_property_offer" + _name = "estate.property.offer" _description = "Estate property Offer" price = fields.Float(string="Price") @@ -19,7 +19,7 @@ class EstatePropertyOffer(models.Model): required=True, ) property_id = fields.Many2one( - "estate_property", + "estate.property", string="Property", required=True, ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 4d15403bf1..01cc95b488 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -2,7 +2,7 @@ class EstatePropertyTag(models.Model): - _name = "estate_property_tag" + _name = "estate.property.tag" _description = "Estate property Tag" name = fields.Char(string="Name") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 15a5405823..2040e351ce 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -2,7 +2,7 @@ class EstatePropertyType(models.Model): - _name = "estate_property_type" + _name = "estate.property.type" _description = "Estate property Type" name = fields.Char(string="Name") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index df0924ec88..1117b62e4d 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -state.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 -estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 -estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 0000000000..9ca7d67296 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,14 @@ + + + + estate_property_offer_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 0000000000..c5909f1c74 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate.property.tag + list,form + + \ No newline at end of file diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 0000000000..8df8d6fa1c --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,8 @@ + + + + Property Types + estate.property.type + list,form + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index f101ceadcb..72da18c382 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -2,25 +2,13 @@ Property - estate_property - list,form - - - - Property Types - estate_property_type - list,form - - - - Property Tags - estate_property_tag + estate.property list,form estate_property_list - estate_property + estate.property @@ -34,21 +22,9 @@ - - estate_property_offer_list - estate_property_offer - - - - - - - - - estate_property_form - estate_property + estate.property
@@ -101,7 +77,7 @@ estate_property_view_search - estate_property + estate.property From b5ec785ca7995898833e3bd80ca73b577866f73e Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Wed, 23 Apr 2025 11:17:20 +0200 Subject: [PATCH 08/17] [IMP] Chapter 8: Computed Fields And Onchanges --- estate/models/estate_property.py | 45 ++++++++++++++++++++------ estate/models/estate_property_offer.py | 22 ++++++++++++- estate/views/estate_property_views.xml | 4 ++- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 6a4f636745..27d45426e8 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ from dateutil import relativedelta -from odoo import fields, models +from odoo import api, fields, models class EstateProperty(models.Model): @@ -11,12 +11,12 @@ class EstateProperty(models.Model): description = fields.Text(string="Description") postcode = fields.Char(string="Postcode") date_availability = fields.Date( - string="Date of Availability", + string="Available From", copy=False, default=fields.Date.today() + relativedelta.relativedelta(months=3), ) expected_price = fields.Float( - string="Expected Selling Price", + string="Expected Price", required=True, ) selling_price = fields.Float( @@ -24,14 +24,14 @@ class EstateProperty(models.Model): readonly=True, copy=False, ) - bedrooms = fields.Integer(string="Number of Bedrooms", default=2) - living_area = fields.Integer(string="Number of Living Areas") - facades = fields.Integer(string="Number of Facades") - garage = fields.Boolean(string="Has a garage") - garden = fields.Boolean(string="Has a garden") - garden_area = fields.Integer(string="Number of garden Areas") + bedrooms = fields.Integer(string="Number Bedrooms", default=2) + living_area = fields.Integer(string="Living Area (sqm)") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area (sqm)") garden_orientation = fields.Selection( - string="Orientation of the Garden", + string="Garden Orientation", selection=[ ("north", "North"), ("south", "South"), @@ -70,3 +70,28 @@ class EstateProperty(models.Model): "property_id", string="Offers", ) + + total_area = fields.Integer(string="Total Area (sqm)", compute="_compute_area") + best_price = fields.Integer(string="Best Offer", compute="_compute_price") + + @api.depends("living_area", "garden_area") + def _compute_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids") + def _compute_price(self): + for record in self: + record.best_price = max([*record.offer_ids.mapped("price"), 0]) + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden is False: + self.garden_orientation = False + self.garden_area = 0 + return + + if self.garden_orientation is False: + self.garden_orientation = "north" + + self.garden_area = 10 diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 65f6fddcf0..f8d4aecaf3 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from dateutil import relativedelta + +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -23,3 +25,21 @@ class EstatePropertyOffer(models.Model): string="Property", required=True, ) + + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_deadline", + inverse="_inverse_deadline", + ) + + @api.depends("validity") + def _compute_deadline(self): + for record in self: + record.date_deadline = record.create_date + relativedelta.relativedelta( + days=record.validity + ) + + def _inverse_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 72da18c382..ba5069c86e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -40,8 +40,9 @@ + - + @@ -55,6 +56,7 @@ + From 6bb0af71ab8f9452efdacf28ad47bcfd92629831 Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Wed, 23 Apr 2025 13:16:42 +0200 Subject: [PATCH 09/17] [IMP] Chapter 9: Ready For Some Action? --- estate/models/estate_property.py | 17 ++++++++---- estate/models/estate_property_offer.py | 27 +++++++++++++++++++- estate/views/estate_property_offer_views.xml | 10 ++++++++ estate/views/estate_property_views.xml | 6 ++++- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 27d45426e8..92ee6748c4 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -15,10 +15,7 @@ class EstateProperty(models.Model): copy=False, default=fields.Date.today() + relativedelta.relativedelta(months=3), ) - expected_price = fields.Float( - string="Expected Price", - required=True, - ) + expected_price = fields.Float(string="Expected Price", required=True) selling_price = fields.Float( string="Selling Price", readonly=True, @@ -63,7 +60,7 @@ class EstateProperty(models.Model): string="Salesman", default=lambda self: self.env.user, ) - partner_id = fields.Many2one("res.partner", string="Buyer") + partner_id = fields.Many2one("res.partner", string="Buyer", readonly=True) tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") offer_ids = fields.One2many( "estate.property.offer", @@ -95,3 +92,13 @@ def _onchange_garden(self): self.garden_orientation = "north" self.garden_area = 10 + + def action_sell(self): + for record in self: + record.state = "sold" + return True + + def action_cancel(self): + for record in self: + record.state = "cancelled" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index f8d4aecaf3..74f5a48bfe 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,6 @@ from dateutil import relativedelta -from odoo import api, fields, models +from odoo import api, exceptions, fields, models class EstatePropertyOffer(models.Model): @@ -43,3 +43,28 @@ def _compute_deadline(self): def _inverse_deadline(self): for record in self: record.validity = (record.date_deadline - record.create_date.date()).days + + def action_refuse_offer(self): + for record in self: + record.status = "refused" + return True + + def action_accept_offer(self): + for record in self: + record.status = "accepted" + record._onchange_status() + + return True + + @api.onchange("status") + def _onchange_status(self): + if self.status != "accepted": + return + + if self.property_id.state in ("accepted", "sold"): + self.status = False + raise exceptions.UserError("An offer as already been accepted.") + + self.property_id.selling_price = self.price + self.property_id.state = "accepted" + self.property_id.partner_id = self.partner_id diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 9ca7d67296..3d1b49b75c 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -8,6 +8,16 @@ + + + + + + + + + + + + + + +
+ +
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index eaf6070eec..17ab5003c2 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,20 +4,24 @@ Property estate.property list,form + {'search_default_available': True}
estate_property_list estate.property - + - + @@ -26,20 +30,29 @@ estate_property_form estate.property -
+
-

- + - + @@ -58,15 +71,16 @@ - - + + - + @@ -81,16 +95,16 @@ - + estate_property_view_search estate.property - + - + Date: Fri, 25 Apr 2025 10:39:20 +0200 Subject: [PATCH 12/17] [FIX] estate: reorder manifest to prevent crash when installing due to dependency between the estate_property_type_view and the estate_property_offer_view. --- estate/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 0a40d4be05..f6fb32c2e7 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -16,9 +16,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_property_type_views.xml", "views/estate_menus.xml", ], "depends": ["base"], From 432bfd6c6b97b5acb1d4e794b1eee2e48983d03d Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Fri, 25 Apr 2025 14:19:48 +0200 Subject: [PATCH 13/17] [IMP] estate: Chapter 12: Inheritance --- estate/__manifest__.py | 37 +++---- estate/models/__init__.py | 1 + estate/models/estate_property.py | 105 +++++++++++--------- estate/models/estate_property_offer.py | 89 +++++++++++------ estate/models/estate_property_tag.py | 12 +-- estate/models/estate_property_type.py | 22 ++-- estate/models/res_users.py | 12 +++ estate/views/estate_property_type_views.xml | 1 - estate/views/res_users_views.xml | 15 +++ 9 files changed, 176 insertions(+), 118 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 f6fb32c2e7..09c14ecb8e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,25 +1,26 @@ { - "name": "Real Estate", - "summary": """ + 'name': 'Real Estate', + 'summary': """ Starting module for "Server framework 101" """, - "description": """ + 'description': """ Starting module for "Server framework 101" """, - "author": "Odoo", - "website": "https://www.odoo.com", - "license": "LGPL-3", - "category": "Tutorials/Real Estate", - "version": "0.1", - "application": True, - "installable": True, - "data": [ - "security/ir.model.access.csv", - "views/estate_property_views.xml", - "views/estate_property_tag_views.xml", - "views/estate_property_offer_views.xml", - "views/estate_property_type_views.xml", - "views/estate_menus.xml", + 'author': 'Odoo', + 'website': 'https://www.odoo.com', + 'license': 'LGPL-3', + 'category': 'Tutorials/Real Estate', + 'version': '0.1', + 'application': True, + 'installable': True, + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_menus.xml', + 'views/res_users_views.xml', ], - "depends": ["base"], + 'depends': ['base'], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8f2187ee09..fea9f441d6 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_offer from . import estate_property_tag from . import estate_property_type +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 73c2ece5e3..cb81da19ae 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,93 +1,100 @@ from dateutil import relativedelta -from odoo import api, exceptions, fields, models +from odoo import _, api, exceptions, fields, models from odoo.tools import float_utils class EstateProperty(models.Model): - _name = "estate.property" - _description = "Estate property" - _order = "id desc" + _name = 'estate.property' + _description = 'Estate property' + _order = 'id desc' - name = fields.Char(string="Name of the Property", required=True) - description = fields.Text(string="Description") - postcode = fields.Char(string="Postcode") + name = fields.Char(string='Name of the Property', required=True) + description = fields.Text(string='Description') + postcode = fields.Char(string='Postcode') date_availability = fields.Date( - string="Available From", copy=False, default=fields.Date.today() + relativedelta.relativedelta(months=3) + string='Available From', + copy=False, + default=fields.Date.today() + relativedelta.relativedelta(months=3), ) - expected_price = fields.Float(string="Expected Price", required=True) - selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) - bedrooms = fields.Integer(string="Number Bedrooms", default=2) - living_area = fields.Integer(string="Living Area (sqm)") - facades = fields.Integer(string="Facades") - garage = fields.Boolean(string="Garage") - garden = fields.Boolean(string="Garden") - garden_area = fields.Integer(string="Garden Area (sqm)") + expected_price = fields.Float(string='Expected Price', required=True) + selling_price = fields.Float(string='Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer(string='Number Bedrooms', default=2) + living_area = fields.Integer(string='Living Area (sqm)') + facades = fields.Integer(string='Facades') + garage = fields.Boolean(string='Garage') + garden = fields.Boolean(string='Garden') + garden_area = fields.Integer(string='Garden Area (sqm)') garden_orientation = fields.Selection( - string="Garden Orientation", - selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")], + string='Garden Orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], ) active = fields.Boolean(default=True) state = fields.Selection( - string="State", + string='State', selection=[ - ("new", "New"), - ("received", "Offer Received"), - ("accepted", "Offer Accepted"), - ("sold", "Sold"), - ("cancelled", "Cancelled"), + ('new', 'New'), + ('received', 'Offer Received'), + ('accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), ], required=True, copy=False, - default="new", + default='new', ) - property_type_id = fields.Many2one("estate.property.type", string="Property Type") - users_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user) - partner_id = fields.Many2one("res.partner", string="Buyer", readonly=True) - tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") - offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") - total_area = fields.Integer(string="Total Area (sqm)", compute="_compute_total_area") - best_price = fields.Integer(string="Best Offer", compute="_compute_price") + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + users_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) + partner_id = fields.Many2one('res.partner', string='Buyer', readonly=True) + tag_ids = fields.Many2many('estate.property.tag', string='Property Tags') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + total_area = fields.Integer(string='Total Area (sqm)', compute='_compute_total_area') + best_price = fields.Integer(string='Best Offer', compute='_compute_price') _sql_constraints = [ - ("check_expected_price", "CHECK(expected_price > 0)", "The expected price must strictly be positive."), - ("check_selling_price", "CHECK(selling_price > 0)", "The selling price must be positive."), + ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must strictly be positive.'), + ('check_selling_price', 'CHECK(selling_price > 0)', 'The selling price must be positive.'), ] - @api.depends("living_area", "garden_area") + @api.ondelete(at_uninstall=False) + def prevent_delete_based_on_state(self): + if any(record.state in {'new', 'cancelled'} for record in self): + raise exceptions.UserError(_('A new or cancelled property cannot be deleted.')) + + @api.depends('living_area', 'garden_area') def _compute_total_area(self): for record in self: - record.write({"total_area": record.living_area + record.garden_area}) + record.write({'total_area': record.living_area + record.garden_area}) - @api.depends("offer_ids.price") + @api.depends('offer_ids.price') def _compute_price(self): for record in self: - record.write({"best_price": max([*record.offer_ids.mapped("price"), 0])}) + record.write({'best_price': max([*record.offer_ids.mapped('price'), 0])}) - @api.onchange("garden") + @api.onchange('garden') def _onchange_garden(self): if self.garden is False: - self.write({"garden_orientation": False, "garden_area": 0}) + self.write({'garden_orientation': False, 'garden_area': 0}) return - self.write({"garden_area": 10, "garden_orientation": self.garden_orientation or "north"}) + self.write({'garden_area': 10, 'garden_orientation': self.garden_orientation or 'north'}) def action_sell(self): - if "cancelled" in self.mapped("state"): - raise exceptions.UserError("A property cancelled cannot be set as sold.") + if 'cancelled' in self.mapped('state'): + raise exceptions.UserError(_('A property cancelled cannot be set as sold.')) - self.write({"state": "sold"}) + self.write({'state': 'sold'}) return True def action_cancel(self): - if "sold" in self.mapped("state"): - raise exceptions.UserError("A property sold cannot be set as cancelled.") + if 'sold' in self.mapped('state'): + raise exceptions.UserError(_('A property sold cannot be set as cancelled.')) - self.write({"state": "cancelled"}) + self.write({'state': 'cancelled'}) return True - @api.constrains("selling_price") + @api.constrains('selling_price') def _check_price(self): for record in self: if float_utils.float_compare(record.selling_price, record.expected_price * 0.9) > 0: - raise exceptions.ValidationError("The selling cannot be lower than 90%% of the expected price.") + raise exceptions.ValidationError(_('The selling cannot be lower than 90%% of the expected price.')) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 2671199324..b260a4095e 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,65 +1,88 @@ +import math + from dateutil import relativedelta -from odoo import api, exceptions, fields, models +from odoo import _, api, exceptions, fields, models class EstatePropertyOffer(models.Model): - _name = "estate.property.offer" - _description = "Estate property Offer" - _order = "price desc" - - price = fields.Float(string="Price") - status = fields.Selection(selection=[("accepted", "Accepted"), ("refused", "Refused")], copy=False) - partner_id = fields.Many2one("res.partner", string="Buyer", required=True) - property_id = fields.Many2one("estate.property", string="Property", required=True) - validity = fields.Integer(string="Validity (days)", default=7) - date_deadline = fields.Date(string="Deadline", compute="_compute_deadline", inverse="_inverse_deadline") + _name = 'estate.property.offer' + _description = 'Estate property Offer' + _order = 'price desc' + + price = fields.Float(string='Price') + status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) + partner_id = fields.Many2one('res.partner', string='Buyer', required=True) + property_id = fields.Many2one('estate.property', string='Property', required=True) + validity = fields.Integer(string='Validity (days)', default=7) + date_deadline = fields.Date(string='Deadline', compute='_compute_deadline', inverse='_inverse_deadline') property_type_id = fields.Many2one( - "estate.property.type", - string="Property Type", - related="property_id.property_type_id", + string='Property Type', + related='property_id.property_type_id', + store=True, ) _sql_constraints = [ - ("check_price", "CHECK(price > 0)", "The price must be strictly positive."), + ('check_price', 'CHECK(price > 0)', 'The price must be strictly positive.'), ] - @api.depends("validity") + @api.model_create_multi + def create(self, vals_list): + properties = { + p.id: p + for p in self.env['estate.property'].browse([ + e.get('property_id') for e in vals_list if e.get('property_id') + ]) + } + + for vals in vals_list: + property = properties.get(vals.get('property_id'), '') + + if not properties or 'price' not in vals: + continue + + if vals['price'] < min([*property.offer_ids.mapped('price'), math.inf]) and len(property.offer_ids) > 1: + raise exceptions.UserError(_('An offer cannot have a lower price then an existing offer.')) + + if property.state == 'new': + property.write({'state': 'received'}) + + return super().create(vals_list) + + @api.depends('validity') def _compute_deadline(self): for record in self: create_date = record.create_date or fields.Date.today() - record.write({"date_deadline": create_date + relativedelta.relativedelta(days=record.validity)}) + record.write({'date_deadline': create_date + relativedelta.relativedelta(days=record.validity)}) def _inverse_deadline(self): for record in self: create_date = record.create_date or fields.Date.today() - record.write({"validity": (record.date_deadline - create_date.date()).days}) + record.write({'validity': (record.date_deadline - create_date.date()).days}) def action_refuse_offer(self): for record in self: - record.write({"status": "refused"}) + record.write({'status': 'refused'}) return True def action_accept_offer(self): for record in self: - record.write({"status": "accepted"}) + record.write({'status': 'accepted'}) record._onchange_status() return True - @api.onchange("status") + @api.onchange('status') def _onchange_status(self): - if self.status != "accepted": + if self.status != 'accepted': return - if self.property_id.state in {"accepted", "sold"}: - self.write({"status": False}) - raise exceptions.UserError("An offer as already been accepted.") - - self.property_id.write( - { - "selling_price": self.price, - "state": "accepted", - "partner_id": self.partner_id, - } - ) + if self.property_id.state in {'accepted', 'sold'}: + self.write({'status': False}) + raise exceptions.UserError(_('An offer as already been accepted.')) + + self.property_id.write({ + 'selling_price': self.price, + 'state': 'accepted', + 'partner_id': self.partner_id, + }) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 81175624d3..22d7298362 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -2,13 +2,13 @@ class EstatePropertyTag(models.Model): - _name = "estate.property.tag" - _description = "Estate property Tag" - _order = "name" + _name = 'estate.property.tag' + _description = 'Estate property Tag' + _order = 'name' - name = fields.Char(string="Name") - color = fields.Integer(string="Color") + name = fields.Char(string='Name') + color = fields.Integer(string='Color') _sql_constraints = [ - ("check_name", "UNIQUE(name)", "A tag must be unique."), + ('check_name', 'UNIQUE(name)', 'A tag must be unique.'), ] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 9b34d6d526..aa35238944 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -2,21 +2,21 @@ class EstatePropertyType(models.Model): - _name = "estate.property.type" - _description = "Estate property Type" - _order = "name" + _name = 'estate.property.type' + _description = 'Estate property Type' + _order = 'name' - name = fields.Char(string="Name") - property_ids = fields.One2many("estate.property", "property_type_id", string="Properties") - sequence = fields.Integer("Sequence", default=1, help="Used to order stages. Lower is better.") - offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers") - offer_count = fields.Integer(string="Number of offers", compute="_compute_offer_count") + name = fields.Char(string='Name') + property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties') + sequence = fields.Integer('Sequence', default=1, help='Used to order stages. Lower is better.') + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers') + offer_count = fields.Integer(string='Number of offers', compute='_compute_offer_count') _sql_constraints = [ - ("check_name", "UNIQUE(name)", "A type must be unique."), + ('check_name', 'UNIQUE(name)', 'A type must be unique.'), ] - @api.depends("offer_ids") + @api.depends('offer_ids') def _compute_offer_count(self): for record in self: - record.write({"offer_count": len(record.offer_ids)}) + record.write({'offer_count': len(record.offer_ids)}) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 0000000000..bd5d4f5fad --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class Users(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'estate.property', + 'users_id', + string='Seller of', + domain=[('state', 'in', ('new', 'received'))], + ) diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 042cc5cba1..bbdf257478 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -36,7 +36,6 @@ - diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 0000000000..637badea29 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + estate.inherited.model.form.inherit + res.users + + + + + + + + + + From 3a34a973e26f1371a7f625002387ec5dfc166844 Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Mon, 28 Apr 2025 09:23:05 +0200 Subject: [PATCH 14/17] [IMP] estate,estate_account: Chapter 13: Interact With Other Modules --- estate/models/estate_property.py | 4 ++-- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 8 ++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 21 +++++++++++++++++++++ 5 files changed, 33 insertions(+), 2 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_property.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index cb81da19ae..ceeec8f1a5 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -96,5 +96,5 @@ def action_cancel(self): @api.constrains('selling_price') def _check_price(self): for record in self: - if float_utils.float_compare(record.selling_price, record.expected_price * 0.9) > 0: - raise exceptions.ValidationError(_('The selling cannot be lower than 90%% of the expected price.')) + if float_utils.float_compare(record.selling_price, record.expected_price * 0.9, precision_rounding=3) == -1: + raise exceptions.ValidationError(_('The selling cannot be lower than 90% of the expected price.')) diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /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 0000000000..4ff2c52e17 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,8 @@ +{ + 'name': 'Real Estate Account', + 'license': 'LGPL-3', + 'category': 'Tutorials/Real Eastate Invoicing', + 'version': '0.1', + 'auto_install': True, + 'depends': ['estate', 'account'], +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 0000000000..5e1963c9d2 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 0000000000..d06da6279e --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,21 @@ +from odoo import Command, models + + +class InheritedEstateProperty(models.Model): + _inherit = 'estate.property' + + def action_sell(self): + self.env['account.move'].create({ + 'partner_id': self.partner_id.id, + 'move_type': 'out_invoice', + 'line_ids': [ + Command.create({ + 'name': '6% of the selling price', + 'quantity': 1, + 'price_unit': 0.06 * self.selling_price, + }), + Command.create({'name': 'Administrative fees', 'quantity': 1, 'price_unit': 100.0}), + ], + }) + + return super().action_sell() From 9589b317b9f6972bb7bf54c0641da620c5e7a623 Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Mon, 28 Apr 2025 10:50:42 +0200 Subject: [PATCH 15/17] [IMP] estate: Chapter 14: A Brief History Of QWeb --- estate/views/estate_property_views.xml | 33 +++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 17ab5003c2..1019c7ba12 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,7 +3,7 @@ Property estate.property - list,form + list,form,kanban {'search_default_available': True} @@ -26,6 +26,37 @@ + + estate_property_kaban + estate.property + + + + + + + + + +
+ +
Expected Price: +
+
Best Offer: +
+
Selling Price: +
+ +
+
+
+
+ +
+
+ estate_property_form estate.property From ad217c7362316c3999d7953b3995c746e2530b01 Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Mon, 28 Apr 2025 15:56:08 +0200 Subject: [PATCH 16/17] [FIX] estate,estate_ccount: fixes following second review --- estate/models/estate_property.py | 24 ++++++----- estate/models/estate_property_offer.py | 44 ++++++++------------ estate/views/estate_property_offer_views.xml | 2 +- estate/views/estate_property_tag_views.xml | 4 +- estate/views/estate_property_type_views.xml | 4 +- estate/views/estate_property_views.xml | 11 ++--- estate_account/models/estate_property.py | 4 +- 7 files changed, 42 insertions(+), 51 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ceeec8f1a5..86e8f8192a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,7 @@ from dateutil import relativedelta -from odoo import _, api, exceptions, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError from odoo.tools import float_utils @@ -49,31 +50,31 @@ class EstateProperty(models.Model): tag_ids = fields.Many2many('estate.property.tag', string='Property Tags') offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') total_area = fields.Integer(string='Total Area (sqm)', compute='_compute_total_area') - best_price = fields.Integer(string='Best Offer', compute='_compute_price') + best_price = fields.Float(string='Best Offer', compute='_compute_price') _sql_constraints = [ ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must strictly be positive.'), - ('check_selling_price', 'CHECK(selling_price > 0)', 'The selling price must be positive.'), + ('check_selling_price', 'CHECK(selling_price >= 0)', 'The selling price must be positive.'), ] @api.ondelete(at_uninstall=False) def prevent_delete_based_on_state(self): if any(record.state in {'new', 'cancelled'} for record in self): - raise exceptions.UserError(_('A new or cancelled property cannot be deleted.')) + raise UserError(_('A new or cancelled property cannot be deleted.')) @api.depends('living_area', 'garden_area') def _compute_total_area(self): for record in self: - record.write({'total_area': record.living_area + record.garden_area}) + record.total_area = record.living_area + record.garden_area @api.depends('offer_ids.price') def _compute_price(self): for record in self: - record.write({'best_price': max([*record.offer_ids.mapped('price'), 0])}) + record.write({'best_price': max(record.offer_ids.mapped('price'), default=0)}) @api.onchange('garden') def _onchange_garden(self): - if self.garden is False: + if not self.garden: self.write({'garden_orientation': False, 'garden_area': 0}) return @@ -81,14 +82,14 @@ def _onchange_garden(self): def action_sell(self): if 'cancelled' in self.mapped('state'): - raise exceptions.UserError(_('A property cancelled cannot be set as sold.')) + raise UserError(_('A property cancelled cannot be set as sold.')) self.write({'state': 'sold'}) return True def action_cancel(self): if 'sold' in self.mapped('state'): - raise exceptions.UserError(_('A property sold cannot be set as cancelled.')) + raise UserError(_('A property sold cannot be set as cancelled.')) self.write({'state': 'cancelled'}) return True @@ -96,5 +97,8 @@ def action_cancel(self): @api.constrains('selling_price') def _check_price(self): for record in self: + if not record.selling_price: + continue + if float_utils.float_compare(record.selling_price, record.expected_price * 0.9, precision_rounding=3) == -1: - raise exceptions.ValidationError(_('The selling cannot be lower than 90% of the expected price.')) + raise ValidationError(_('The selling cannot be lower than 90% of the expected price.')) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index b260a4095e..bd0ba0227e 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,8 +1,7 @@ -import math - from dateutil import relativedelta -from odoo import _, api, exceptions, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError class EstatePropertyOffer(models.Model): @@ -10,7 +9,7 @@ class EstatePropertyOffer(models.Model): _description = 'Estate property Offer' _order = 'price desc' - price = fields.Float(string='Price') + price = fields.Float(string='Price', required=True) status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) partner_id = fields.Many2one('res.partner', string='Buyer', required=True) property_id = fields.Many2one('estate.property', string='Property', required=True) @@ -30,19 +29,17 @@ class EstatePropertyOffer(models.Model): def create(self, vals_list): properties = { p.id: p - for p in self.env['estate.property'].browse([ - e.get('property_id') for e in vals_list if e.get('property_id') - ]) + for p in self.env['estate.property'].browse([e['property_id'] for e in vals_list if e.get('property_id')]) } for vals in vals_list: property = properties.get(vals.get('property_id'), '') - if not properties or 'price' not in vals: - continue + if not property: + raise ValidationError(_('Creating an offer without a property is not possible.')) - if vals['price'] < min([*property.offer_ids.mapped('price'), math.inf]) and len(property.offer_ids) > 1: - raise exceptions.UserError(_('An offer cannot have a lower price then an existing offer.')) + if (price := vals.get('price')) and price < property.best_price: + raise UserError(_('An offer cannot have a lower price then an existing offer.')) if property.state == 'new': property.write({'state': 'received'}) @@ -67,22 +64,15 @@ def action_refuse_offer(self): def action_accept_offer(self): for record in self: - record.write({'status': 'accepted'}) - record._onchange_status() - - return True + if record.property_id.state in {'accepted', 'sold'}: + raise UserError(_('An offer as already been accepted.')) - @api.onchange('status') - def _onchange_status(self): - if self.status != 'accepted': - return + record.write({'status': 'accepted'}) - if self.property_id.state in {'accepted', 'sold'}: - self.write({'status': False}) - raise exceptions.UserError(_('An offer as already been accepted.')) + record.property_id.write({ + 'selling_price': record.price, + 'state': 'accepted', + 'partner_id': record.partner_id, + }) - self.property_id.write({ - 'selling_price': self.price, - 'state': 'accepted', - 'partner_id': self.partner_id, - }) + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 18b20817b2..9e86162faf 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -11,7 +11,7 @@ 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 index 090288da28..8fcc48a996 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -10,7 +10,7 @@ estate_property_tag_form estate.property.tag - +

@@ -24,7 +24,7 @@ estate_property_tag_list estate.property.tag - + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index bbdf257478..0514565cc7 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -10,7 +10,7 @@ estate_property_type_list estate.property.type - + @@ -21,7 +21,7 @@ estate_property_type_form estate.property.type - +

diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1019c7ba12..ade7ecd7cc 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -39,7 +39,7 @@ - +
Expected Price: @@ -105,7 +105,6 @@ - @@ -139,14 +138,10 @@ + domain="[('state', 'in', ['new', 'received'])]" /> + context="{'group_by':'postcode'}" /> diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index d06da6279e..7826c9520d 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -5,6 +5,8 @@ class InheritedEstateProperty(models.Model): _inherit = 'estate.property' def action_sell(self): + res = super().action_sell() + self.env['account.move'].create({ 'partner_id': self.partner_id.id, 'move_type': 'out_invoice', @@ -18,4 +20,4 @@ def action_sell(self): ], }) - return super().action_sell() + return res From 8edaa13f9d7cbd71baeee84f2ddd1be36638316b Mon Sep 17 00:00:00 2001 From: Leo Leclerc Date: Fri, 2 May 2025 17:11:13 +0200 Subject: [PATCH 17/17] [IMP] awesome_owl: Chapter 1: Owl components --- awesome_owl/static/src/card/card.js | 19 ++++++ awesome_owl/static/src/card/card.xml | 19 ++++++ awesome_owl/static/src/counter/counter.js | 21 +++++++ awesome_owl/static/src/counter/counter.xml | 13 ++++ awesome_owl/static/src/playground.js | 22 ++++++- awesome_owl/static/src/playground.xml | 18 +++++- awesome_owl/static/src/todolist/todoitem.js | 25 ++++++++ awesome_owl/static/src/todolist/todoitem.xml | 24 ++++++++ awesome_owl/static/src/todolist/todolist.js | 62 ++++++++++++++++++++ awesome_owl/static/src/todolist/todolist.xml | 15 +++++ awesome_owl/static/src/utils.js | 9 +++ 11 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml create mode 100644 awesome_owl/static/src/todolist/todoitem.js create mode 100644 awesome_owl/static/src/todolist/todoitem.xml create mode 100644 awesome_owl/static/src/todolist/todolist.js create mode 100644 awesome_owl/static/src/todolist/todolist.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 0000000000..1f6a47e5fe --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,19 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + + static props = { + title: { type: String }, + slots: { type: Object }, + // content: { type: String }, removed following 13. Generic Card with slots + }; + + setup() { + this.opened = useState({ value: true }); + } + + toggleOpened() { + this.opened.value = !this.opened.value; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 0000000000..aa2afa46d5 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,19 @@ + + + + +
+
+
+

+ +
+

+ + +

+
+
+
+ +
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 0000000000..6bb9afe1c5 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,21 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + + static props = { + onChange: { type: Function, optional: true }, + }; + + setup() { + this.state = useState({ value: 0 }); + } + + increment() { + this.state.value++; + + if (this.props.onChange) { + this.props.onChange(); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 0000000000..d3737ead14 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,13 @@ + + + + +
+
+

Counter:

+ +
+
+
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07b..beac32852c 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,23 @@ -/** @odoo-module **/ - -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Card } from "./card/card"; +import { Counter } from "./counter/counter"; +import { TodoList } from "./todolist/todolist"; export class Playground extends Component { + static components = { Counter, Card, TodoList }; + static template = "awesome_owl.playground"; + + static props = {}; + + html1 = '
some content
'; + html2 = markup('
some content
'); + + setup() { + this.sum = useState({ value: 0 }); + } + + incrementSum() { + this.sum.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f..5b474706a6 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,9 +1,23 @@ - +
- hello world + hello world, The sum is +
+
+ + +
+
+ some content + + + + +
+
+
diff --git a/awesome_owl/static/src/todolist/todoitem.js b/awesome_owl/static/src/todolist/todoitem.js new file mode 100644 index 0000000000..5d44e1e2de --- /dev/null +++ b/awesome_owl/static/src/todolist/todoitem.js @@ -0,0 +1,25 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todoitem"; + + static props = { + todo: { + type: Object, + shape: { + id: Number, + description: String, + isCompleted: Boolean, + }, + }, + removeTodo: { type: Function }, + }; + + toggleState(ev) { + if (!ev.target) { + return; + } + + this.props.todo.isCompleted = ev.target.checked; + } +} diff --git a/awesome_owl/static/src/todolist/todoitem.xml b/awesome_owl/static/src/todolist/todoitem.xml new file mode 100644 index 0000000000..406330865a --- /dev/null +++ b/awesome_owl/static/src/todolist/todoitem.xml @@ -0,0 +1,24 @@ + + + + + +
+ . +
+ +
+ +
diff --git a/awesome_owl/static/src/todolist/todolist.js b/awesome_owl/static/src/todolist/todolist.js new file mode 100644 index 0000000000..d9f6e3b779 --- /dev/null +++ b/awesome_owl/static/src/todolist/todolist.js @@ -0,0 +1,62 @@ +import { Component, useRef, useState } from "@odoo/owl"; +import { useAutofocus } from "../utils"; +import { TodoItem } from "./todoitem"; + +export class TodoList extends Component { + static components = { TodoItem }; + + static template = "awesome_owl.todolist"; + + static props = {}; + + setup() { + this.id = 1; // could be calculated every iteration but would get very slow with a lot of todos + this.todos = useState([ + // removed following 9. Adding a todo + // { id: 2, description: "write tutorial", isCompleted: true }, + // { id: 3, description: "buy milk", isCompleted: false }, + ]); + this.inputRef = useRef("add_task"); + useAutofocus("add_task"); + } + + addTodo(ev) { + if (ev.keyCode != 13) { + return; + } + + if (!this.inputRef.el) { + return; + } + + const desc = this.inputRef.el.value; + + if (desc === "") { + return; + } + + this.todos.push({ + id: this.id, + description: desc, + isCompleted: false, + }); + + this.inputRef.el.value = ""; + this.id++; + } + + removeTodo(ev) { + // is there not a better way ??? this doesn't feel good + const elemId = parseInt(ev.target.parentNode.childNodes[1].childNodes[0].textContent); + + if (elemId < 0 || elemId >= this.id) { + return; + } + + const index = this.todos.findIndex((elem) => elem.id === elemId); + if (index >= 0) { + // remove the element at index from list + this.todos.splice(index, 1); + } + } +} diff --git a/awesome_owl/static/src/todolist/todolist.xml b/awesome_owl/static/src/todolist/todolist.xml new file mode 100644 index 0000000000..f3df4b49c7 --- /dev/null +++ b/awesome_owl/static/src/todolist/todolist.xml @@ -0,0 +1,15 @@ + + + + +
+
+ +
+ +
+
+
+
+ +
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 0000000000..a56f6749b2 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,9 @@ +import { useEffect, useRef } from "@odoo/owl"; + +export function useAutofocus(name) { + const ref = useRef(name); + useEffect( + (el) => el && el.focus(), + () => [ref.el] + ); +}