From 10bb488e738e699b9aa78120d134e8039da109a7 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Mon, 20 Jul 2026 17:44:19 +0200 Subject: [PATCH 01/25] [ADD] estate: created manifest and init for estate module This the foundation of the module. This part implements the instructions of chapter 2 from server framework 101. --- estate/__init__.py | 0 estate/__manifest__.py | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..2a433b21577 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,5 @@ +{ + 'name': "Estate", + 'depends': ['base'], + 'application': True, +} \ No newline at end of file From a12ad1abdfbc62690b46b1639f63909110d64b25 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Wed, 22 Jul 2026 09:41:52 +0200 Subject: [PATCH 02/25] [IMP] estate: Create estate_propety model. Creating estate_property model as part of chapter 3 - ORM from server framework 101. --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 6 ++++++ 3 files changed, 8 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 e69de29bb2d..9a7e03eded3 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/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..27b6271c1d8 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,6 @@ +from odoo import models + +class Property(models.Model): + _name = 'estate.property' + _description = "Estate Property" + From 1b4148a3d62b63c30bd55c3bbab54b3733ca85a0 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Wed, 22 Jul 2026 10:33:53 +0200 Subject: [PATCH 03/25] [IMP] estate: Create basic fields. Following chapter 3 - model fields from Server Framework 101 --- estate/models/estate_property.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 27b6271c1d8..6f92dd8d24b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,22 @@ -from odoo import models +from odoo import fields, models class Property(models.Model): _name = 'estate.property' _description = "Estate Property" + name = fields.Char() + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float() + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string="Garden Orientation", + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] + ) \ No newline at end of file From 97541c85f7503d66c8afb8d8bd5b4c7227f064a2 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Wed, 22 Jul 2026 10:39:55 +0200 Subject: [PATCH 04/25] [IMP] estate: Make name and expected_price not nullable. Following chapter 3 - common attributes from SF101. --- estate/models/estate_property.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 6f92dd8d24b..457396f06cc 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -4,11 +4,11 @@ class Property(models.Model): _name = 'estate.property' _description = "Estate Property" - name = fields.Char() + name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() date_availability = fields.Date() - expected_price = fields.Float() + expected_price = fields.Float(required=True) selling_price = fields.Float() bedrooms = fields.Integer() living_area = fields.Integer() From 73c54055efe21c2477f7f8863ce449a45aa6452b Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Wed, 22 Jul 2026 11:23:49 +0200 Subject: [PATCH 05/25] [IMP] estate: Added security file for access rights. Following chapetr 4 - security from SF101. The access rights define which user/users/group of users can access some data. --- estate/__manifest__.py | 5 ++++- estate/security/ir.model.access.csv | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 2a433b21577..14bb80e3e58 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,4 +2,7 @@ 'name': "Estate", 'depends': ['base'], 'application': True, -} \ No newline at end of file + 'data': [ + 'security/ir.model.access.csv', + ] +} diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..d9d6ba57cc5 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 From 61a59ffab8f82e7aae743d54cb3f000dae13455e Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Wed, 22 Jul 2026 14:02:27 +0200 Subject: [PATCH 06/25] [IMP] estate: Estate property view action First action implemented as on chapter 5 - action from SF101. --- estate/__manifest__.py | 2 ++ estate/views/estate_property_views.xml | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 14bb80e3e58..4694ed647a2 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,5 +4,7 @@ 'application': True, 'data': [ 'security/ir.model.access.csv', + + 'views/estate_property_views.xml', ] } diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..7447425ba9b --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Estate Property + estate.property + tree,form + + From 6466c67c1f57511a400d1b14e944929a9624334e Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Wed, 22 Jul 2026 14:49:53 +0200 Subject: [PATCH 07/25] [IMP] estate: Three level menu for Estate Property. Tree is no longer recognized as a valid action view mode keyword so I replaced it with list. Also created a three level menu view as per chapter 5 -menus from SF101. --- estate/__manifest__.py | 1 + estate/views/estate_menus.xml | 7 +++++++ estate/views/estate_property_views.xml | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 estate/views/estate_menus.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 4694ed647a2..526d285e084 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -6,5 +6,6 @@ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_menus.xml', ] } diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..a5b56f0451b --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 7447425ba9b..a0d024b94d6 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,6 +3,6 @@ Estate Property estate.property - tree,form + list,form From bc296dd5a5561b2104d64626dd5315467cd47c1b Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Wed, 22 Jul 2026 16:13:39 +0200 Subject: [PATCH 08/25] [LINT] estate: New line at the end of all files. Failed linting because of missing new lines at the end of files. Also needed two lines between top level declarations and the actual code. THis changes now. --- estate/__init__.py | 2 +- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/models/__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 457396f06cc..77ababbe144 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ from odoo import fields, models + class Property(models.Model): _name = 'estate.property' _description = "Estate Property" @@ -19,4 +20,4 @@ class Property(models.Model): garden_orientation = fields.Selection( string="Garden Orientation", selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] - ) \ No newline at end of file + ) From a347f49828940f718665d0b670608f47b21987ca Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Wed, 22 Jul 2026 17:20:10 +0200 Subject: [PATCH 09/25] [IMP] estate: Fields, Attributes And View Finished chapter 5 from the SF101 tutorial. --- estate/models/estate_property.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 77ababbe144..408bc9e29af 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -8,10 +8,13 @@ class Property(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date( + default=lambda self: fields.Date.add(fields.Date.today(), months=3), + copy=False + ) expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + selling_price = fields.Float(readonly=True) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() @@ -21,3 +24,12 @@ class Property(models.Model): string="Garden Orientation", selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] ) + active = fields.Boolean(default=True) + state = fields.Selection( + string="State", + selection=[('new', 'New'), ('offer received', 'Offer Received'), + ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], + required=True, + copy=False, + default='new' + ) From 9c1834caa7e53c01808a05a9455fc342e7f55226 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Thu, 23 Jul 2026 11:11:04 +0200 Subject: [PATCH 10/25] [IMP] estate: List, Form, Search view Finished chapter 6 - basic views from SF101. In the list view we can see some attributes of each property. There is also a form for each property. Also now the user can filter some field, group by postcode and see the available properties. --- estate/views/estate_property_views.xml | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index a0d024b94d6..462f6898e10 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,5 +1,82 @@ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + Estate Property estate.property From 52d72916de791dec8cff87dd351f4c9d190a399d Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Thu, 23 Jul 2026 14:51:07 +0200 Subject: [PATCH 11/25] [IMP] estate: Many2one Fields Implemented the Many2one part from chapter 7 of sf101. Now we have property types and views for them. Any property has another tab as well where we can see the salesman and the buyer of the property. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 2 +- estate/models/estate_property.py | 5 ++++- estate/models/estate_property_type.py | 8 ++++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_menus.xml | 6 +++++- estate/views/estate_property_type_view.xml | 8 ++++++++ estate/views/estate_property_views.xml | 14 +++++++++++--- 8 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_type_view.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 526d285e084..6c5abdea879 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,7 @@ 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_type_view.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml', ] diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..76e779e73b0 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property +from . import estate_property, estate_property_type diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 408bc9e29af..fbfa3d25759 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -9,7 +9,7 @@ class Property(models.Model): description = fields.Text() postcode = fields.Char() date_availability = fields.Date( - default=lambda self: fields.Date.add(fields.Date.today(), months=3), + default=lambda self: fields.Date.add(fields.Date.today(), months=3), copy=False ) expected_price = fields.Float(required=True) @@ -33,3 +33,6 @@ class Property(models.Model): copy=False, default='new' ) + property_type_id=fields.Many2one("estate.property.type", string="Property Type") + salesman = fields.Many2one("res.users", default=lambda self: self.env.user) + buyer = fields.Many2one("res.partner", copy=False) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..5604ae28ce6 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyType(models.Model): + _name = 'estate.property.type' + _description = "Estate Property Type" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index d9d6ba57cc5..1b712a3f909 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 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index a5b56f0451b..e22d4ada6af 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,7 +1,11 @@ + - + + + + diff --git a/estate/views/estate_property_type_view.xml b/estate/views/estate_property_type_view.xml new file mode 100644 index 00000000000..6384061b8ee --- /dev/null +++ b/estate/views/estate_property_type_view.xml @@ -0,0 +1,8 @@ + + + + Property Types + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 462f6898e10..fabe5d0cd79 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -13,7 +13,7 @@ - + @@ -34,6 +34,7 @@ + @@ -55,6 +56,12 @@ + + + + + + @@ -66,7 +73,8 @@ estate.property - + + @@ -78,7 +86,7 @@ - Estate Property + Properties estate.property list,form From 96dc3612a6264e4422e71cc7aa3d7dfc126d5186 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Thu, 23 Jul 2026 15:08:20 +0200 Subject: [PATCH 12/25] [IMP] estate: Many2many tags Implemented Many2many from Chapter 7 of SF101. Now we can assign tags to each property. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 2 +- estate/models/estate_property.py | 1 + estate/models/estate_property_tag.py | 8 ++++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_menus.xml | 1 + estate/views/estate_property_tag_views.xml | 8 ++++++++ estate/views/estate_property_views.xml | 3 +++ 8 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/views/estate_property_tag_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 6c5abdea879..57c8eb1cded 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,7 @@ 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_tag_views.xml', 'views/estate_property_type_view.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml', diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 76e779e73b0..6c1ae061713 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property, estate_property_type +from . import estate_property, estate_property_type, estate_property_tag diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index fbfa3d25759..e09a2663492 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -36,3 +36,4 @@ class Property(models.Model): property_type_id=fields.Many2one("estate.property.type", string="Property Type") salesman = fields.Many2one("res.users", default=lambda self: self.env.user) buyer = fields.Many2one("res.partner", copy=False) + tag_ids = fields.Many2many("estate.property.tag") diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..1bdf4804d18 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyTag(models.Model): + _name = 'estate.property.tag' + _description = "Estate Property Tag" + + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 1b712a3f909..57df98b8327 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index e22d4ada6af..02c9438c2e3 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -6,6 +6,7 @@ +
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..0ec3dc550e0 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index fabe5d0cd79..ae459a4527e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -32,6 +32,9 @@ + + + From fba2091dac15fa5d69bc44eebae2fb17f0f63962 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Thu, 23 Jul 2026 15:55:32 +0200 Subject: [PATCH 13/25] [IMP] estate: One2Many fields Offers under each property as part of the One2Many part from chapter 7 of SF101. And with this chapter 7 ends. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 2 +- estate/models/estate_property.py | 1 + estate/models/estate_property_offer.py | 11 +++++++++++ estate/security/ir.model.access.csv | 1 + estate/views/estate_property_offer_view.xml | 14 ++++++++++++++ estate/views/estate_property_views.xml | 6 +++++- 7 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/views/estate_property_offer_view.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 57c8eb1cded..3984d417946 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,7 @@ 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_offer_view.xml', 'views/estate_property_tag_views.xml', 'views/estate_property_type_view.xml', 'views/estate_property_views.xml', diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 6c1ae061713..93a6bd86abd 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property, estate_property_type, estate_property_tag +from . import estate_property, estate_property_type, estate_property_tag, estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index e09a2663492..cafd840400c 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -37,3 +37,4 @@ class Property(models.Model): salesman = fields.Many2one("res.users", default=lambda self: self.env.user) buyer = fields.Many2one("res.partner", copy=False) tag_ids = fields.Many2many("estate.property.tag") + offer_ids = fields.One2many("estate.property.offer", "partner_id") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..674ed52b7bc --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class PropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = "Estate Property Offer" + + price = fields.Float() + status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) + partner_id = fields.Many2one('res.partner', required=True) + property_id = fields.Many2one('estate.property', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 57df98b8327..49bca99cac8 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -2,3 +2,4 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml new file mode 100644 index 00000000000..8b745ddbc83 --- /dev/null +++ b/estate/views/estate_property_offer_view.xml @@ -0,0 +1,14 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index ae459a4527e..6605fde8ece 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,13 +4,14 @@ estate.property.search estate.property - + + @@ -59,6 +60,9 @@ + + + From 8adef14cbbc661770fdcec64b50f28fce9862c21 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Thu, 23 Jul 2026 17:27:57 +0200 Subject: [PATCH 14/25] [IMP] estate: dependencies and inverse function Got to chapter 8 from sf101 and implemented the dependencies and inverse function parts under the computed fields section. Now we total area is computed using garden area and living area, best price of the offers is computed as well and we also compute the available date and the validity of an offer (they affect each other through an inverse function). --- estate/models/estate_property.py | 21 +++++++++++++++++++-- estate/models/estate_property_offer.py | 15 ++++++++++++++- estate/views/estate_property_offer_view.xml | 8 +++++--- estate/views/estate_property_views.xml | 2 ++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index cafd840400c..794db23cd90 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class Property(models.Model): @@ -37,4 +37,21 @@ class Property(models.Model): salesman = fields.Many2one("res.users", default=lambda self: self.env.user) buyer = fields.Many2one("res.partner", copy=False) tag_ids = fields.Many2many("estate.property.tag") - offer_ids = fields.One2many("estate.property.offer", "partner_id") + offer_ids = fields.One2many("estate.property.offer", "property_id") + + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Float(compute="_compute_best_offer") + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price") + def _compute_best_offer(self): + for record in self: + if record.offer_ids: + record.best_price = max(record.offer_ids.mapped("price")) + continue + + record.best_price = 0.0 diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 674ed52b7bc..676c6487a54 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class PropertyOffer(models.Model): @@ -9,3 +9,16 @@ class PropertyOffer(models.Model): status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) partner_id = fields.Many2one('res.partner', required=True) property_id = fields.Many2one('estate.property', required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline") + + @api.depends("create_date", "validity") + def _compute_date_deadline(self): + for record in self: + create_date = record.create_date if record.create_date else fields.Date.today() + record.date_deadline = fields.Date.add(create_date, days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + create_date = record.create_date if record.create_date else fields.Date.today() + record.validity = (record.date_deadline - fields.Date.to_date(create_date)).days diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml index 8b745ddbc83..5f75f8bc1ff 100644 --- a/estate/views/estate_property_offer_view.xml +++ b/estate/views/estate_property_offer_view.xml @@ -5,9 +5,11 @@ estate.property.offer - - - + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 6605fde8ece..a4dfc489061 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -44,6 +44,7 @@ + @@ -58,6 +59,7 @@ + From c7550baaf8451a28256ff404d894c8747fe70f38 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Fri, 24 Jul 2026 09:03:29 +0200 Subject: [PATCH 15/25] [IMP] estate: Onchanges Finished Chapter 8 of SF101. Implemented onchange so that whenever garden is ticked, then garden area and orientation are filled by default with some values. --- estate/models/estate_property.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 794db23cd90..cb918d5b286 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -55,3 +55,13 @@ def _compute_best_offer(self): continue record.best_price = 0.0 + + @api.onchange("garden") + def _onchange_property(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + return + + self.garden_area = None + self.garden_orientation = None From 767c4bba3bf129c9620dced030ff98d9eaca37da Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Fri, 24 Jul 2026 10:15:43 +0200 Subject: [PATCH 16/25] [IMP] estate: Chapter 9 Finished Chapter 9 of the SF101. Now we can mark a property as sold and cancelled and we have errors in place incase a user wants to cancel an already sold property or vice versa. WE can also accept and refuse offers. Once an offer has been accepted then the others are refused, the selling price is set as well as the buyer. --- estate/models/estate_property.py | 21 ++++++++++++++++++++- estate/models/estate_property_offer.py | 19 +++++++++++++++++++ estate/views/estate_property_offer_view.xml | 12 +++++++----- estate/views/estate_property_views.xml | 5 +++++ 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index cb918d5b286..60a9de56681 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class Property(models.Model): @@ -33,7 +34,7 @@ class Property(models.Model): copy=False, default='new' ) - property_type_id=fields.Many2one("estate.property.type", string="Property Type") + property_type_id = fields.Many2one("estate.property.type", string="Property Type") salesman = fields.Many2one("res.users", default=lambda self: self.env.user) buyer = fields.Many2one("res.partner", copy=False) tag_ids = fields.Many2many("estate.property.tag") @@ -65,3 +66,21 @@ def _onchange_property(self): self.garden_area = None self.garden_orientation = None + + def action_cancel_property(self): + for record in self: + if self.state == 'sold': + raise UserError(self.env._("A sold property cannot be cancelled.")) + + self.state = 'cancelled' + + return True + + def action_sold_property(self): + for record in self: + if self.state == 'cancelled': + raise UserError(self.env._("A cancelled property cannot be sold.")) + + self.state = 'sold' + + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 676c6487a54..7171e920479 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -22,3 +22,22 @@ def _inverse_date_deadline(self): for record in self: create_date = record.create_date if record.create_date else fields.Date.today() record.validity = (record.date_deadline - fields.Date.to_date(create_date)).days + + def action_accept_offer(self): + self.ensure_one() + + self.status = 'accepted' + + other_offers = self.property_id.offer_ids - self + other_offers.write({'status': 'refused'}) + + self.property_id.selling_price = self.price + self.property_id.buyer = self.partner_id + + return True + + def action_refuse_offer(self): + self.ensure_one() + self.status = 'refused' + + return True diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml index 5f75f8bc1ff..e209e0b8a23 100644 --- a/estate/views/estate_property_offer_view.xml +++ b/estate/views/estate_property_offer_view.xml @@ -5,11 +5,13 @@ estate.property.offer - - - - - + + + + + +

diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 6cb48274bdc..131c52a0c3f 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -9,11 +9,11 @@ - + - + @@ -27,11 +27,6 @@ estate.property -
-

@@ -62,13 +57,13 @@ - - + + - + @@ -86,7 +81,9 @@ estate.property.list estate.property - + @@ -94,7 +91,7 @@ - + @@ -103,5 +100,6 @@ Properties estate.property list,form + {'search_default_available': True} From 25fa1b655a17045a66baf06173624ed31e0bd23f Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Mon, 27 Jul 2026 13:30:59 +0200 Subject: [PATCH 21/25] [IMP] estate: started chapter 12 started chapter 12 and redid the method which sets the state as offer received after the creation of an offer also inclusing the guardrail that an offer shoudn't be created if it's lower than the max offer. also prevent deletion if the state of the property is new or cancelled. --- estate/models/estate_property.py | 6 ++++++ estate/models/estate_property_offer.py | 17 ++++++++++++----- estate/views/estate_property_views.xml | 5 +++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 0a9b3442a9f..f41fde24cd6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -79,6 +79,12 @@ def _onchange_property(self): self.garden_area = None self.garden_orientation = None + @api.ondelete(at_uninstall=False) + def _check_state(self): + for record in self: + if record.state == "new" or record.state == "cancelled": + raise UserError(self.env._("Can't delete a new or cancelled property.")) + @api.constrains('selling_price') def _check_selling_price(self): for record in self: diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 34859aab819..d583a8c3fed 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare class PropertyOffer(models.Model): @@ -21,13 +23,18 @@ class PropertyOffer(models.Model): @api.model_create_multi def create(self, vals_list): - offers = super().create(vals_list) + for vals in vals_list: + property = self.env["estate.property"].browse(vals["property_id"]) - for offer in offers: - if offer.property_id.state == 'new': - offer.property_id.state = 'offer received' + if property.offer_ids: + max_offer = max(property.mapped("offer_ids.price")) + if float_compare(vals["price"], max_offer, precision_digits=2) <= 0: + raise UserError(f"The offer must be strictly higher than {max_offer:.2f}") + + property.state = 'offer received' + + return super().create(vals_list) - return offers @api.depends("create_date", "validity") def _compute_date_deadline(self): diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 131c52a0c3f..db7e87bf034 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -27,6 +27,11 @@ estate.property +
+

From 7b2691ab3839ec4f6a0cac48957614f3a8036bcb Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Mon, 27 Jul 2026 16:10:09 +0200 Subject: [PATCH 22/25] [IMP] estate: finish chpater 12 chpater 12 from sf101 finished. implemented model inheritence by creating a EstatePropertyUsers class that inherits from res.users and view inheritence using xpath. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 8 +++++++- estate/models/estate_property_users.py | 8 ++++++++ estate/views/estate_property_users.xml | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 estate/models/estate_property_users.py create mode 100644 estate/views/estate_property_users.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 3984d417946..8aa47ef69db 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,7 @@ 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_users.xml', 'views/estate_property_offer_view.xml', 'views/estate_property_tag_views.xml', 'views/estate_property_type_view.xml', diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 93a6bd86abd..b0ce1b080e8 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,7 @@ -from . import estate_property, estate_property_type, estate_property_tag, estate_property_offer +from . import ( + estate_property, + estate_property_type, + estate_property_tag, + estate_property_offer, + estate_property_users, +) diff --git a/estate/models/estate_property_users.py b/estate/models/estate_property_users.py new file mode 100644 index 00000000000..5e5b30144ca --- /dev/null +++ b/estate/models/estate_property_users.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class EstatePropertyUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", "salesman", + domain=[('state', 'in', ('new','offer received'))]) diff --git a/estate/views/estate_property_users.xml b/estate/views/estate_property_users.xml new file mode 100644 index 00000000000..f6ad5db536c --- /dev/null +++ b/estate/views/estate_property_users.xml @@ -0,0 +1,14 @@ + + + res.users.view.form.inherit.estate.property + res.users + + + + + + + + + + From fdca1a8b35c10be10f2924cb17022b9ef5c7550e Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Mon, 27 Jul 2026 21:31:39 +0200 Subject: [PATCH 23/25] [IMP] estate: finished chapter 13 implemented the estate_account module which links the estate property modeule with the invoicing module. I override the action sold property method so that it also creates an invoice for a sold property. --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 4 ++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 26 ++++++++++++++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..9c4f37cac23 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,4 @@ +{ + 'name': "Estate Account", + 'depends': ['estate', 'account'] +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..283a0acfee8 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,26 @@ +from odoo import Command, models + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold_property(self): + self.env['account.move'].create( + { + 'partner_id': self.buyer.id, + 'move_type': 'out_invoice', + '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 + }) + ] + } + ) + return super().action_sold_property() From 19ef623ac580fd927edfc4c778a0459ebac6776c Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Mon, 27 Jul 2026 23:04:12 +0200 Subject: [PATCH 24/25] [IMP] estate: finish of chapter 14 kanban view of properties based on type. Drag and drop is not working. --- estate/views/estate_property_views.xml | 34 +++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index db7e87bf034..59ed8f5c646 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -101,10 +101,42 @@ + + estate.property.kanban + estate.property + + + + + +
+

+ +

+
+ Expected Price: + +
+
+ Best Offer: + +
+
+ Selling Price: + +
+ +
+
+
+
+
+
+ Properties estate.property - list,form + list,form,kanban {'search_default_available': True} From 1764aaa1d7c1c4619a9503fd37f0bd5b22607714 Mon Sep 17 00:00:00 2001 From: "Cezar Bulancea (cebul)" Date: Thu, 30 Jul 2026 16:34:31 +0200 Subject: [PATCH 25/25] [IMP] estate: implemented feedback changes on feedback; refactoring + logic adjustments --- estate/__manifest__.py | 4 +- estate/models/__init__.py | 12 ++-- estate/models/estate_property.py | 71 ++++++++++++--------- estate/models/estate_property_offer.py | 66 ++++++++++++------- estate/models/estate_property_tag.py | 4 +- estate/models/estate_property_type.py | 10 +-- estate/models/estate_property_users.py | 8 --- estate/models/res_users.py | 11 ++++ estate/views/estate_property_offer_view.xml | 4 +- estate/views/estate_property_views.xml | 22 +++---- estate_account/__manifest__.py | 4 +- estate_account/models/estate_property.py | 15 +++-- 12 files changed, 135 insertions(+), 96 deletions(-) delete mode 100644 estate/models/estate_property_users.py create mode 100644 estate/models/res_users.py diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8aa47ef69db..2f750eceeaa 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,5 +11,7 @@ 'views/estate_property_type_view.xml', 'views/estate_property_views.xml', 'views/estate_menus.xml', - ] + ], + 'author': 'Odoo S.A.', + 'license': 'LGPL-3', } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index b0ce1b080e8..fea9f441d6d 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,7 +1,5 @@ -from . import ( - estate_property, - estate_property_type, - estate_property_tag, - estate_property_offer, - estate_property_users, -) +from . import estate_property +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 f41fde24cd6..0caf2e3dc60 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -3,73 +3,85 @@ from odoo.tools.float_utils import float_compare, float_is_zero -class Property(models.Model): +class EstateProperty(models.Model): _name = 'estate.property' _description = "Estate Property" - _order = "id desc" + _order = 'id desc' name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() date_availability = fields.Date( - default=lambda self: fields.Date.add(fields.Date.today(), months=3), - copy=False + default=fields.Date.add(fields.Date.today(), months=3), + copy=False, ) expected_price = fields.Float(required=True) - selling_price = fields.Float(readonly=True) + selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) - living_area = fields.Integer() + living_area = fields.Integer(string="Living Area (sqm)") facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer() + garden_area = fields.Integer(string="Garden Area (sqm)") garden_orientation = fields.Selection( string="Garden Orientation", - selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')] + selection=[('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")], ) active = fields.Boolean(default=True) state = fields.Selection( - string="State", - selection=[('new', 'New'), ('offer received', 'Offer Received'), - ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], + selection=[ + ('new', "New"), + ('offer_received', "Offer Received"), + ('offer_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") + salesman_id = fields.Many2one( + 'res.users', + string="Salesman", + default=lambda self: self.env.user, + ) + buyer_id = fields.Many2one( + 'res.partner', + string="Buyer", + copy=False, ) - property_type_id = fields.Many2one("estate.property.type", string="Property Type") - salesman = fields.Many2one("res.users", default=lambda self: self.env.user) - buyer = fields.Many2one("res.partner", copy=False) - tag_ids = fields.Many2many("estate.property.tag") - offer_ids = fields.One2many("estate.property.offer", "property_id") + tag_ids = fields.Many2many('estate.property.tag') + offer_ids = fields.One2many('estate.property.offer', 'property_id') - total_area = fields.Integer(compute="_compute_total_area") - best_price = fields.Float(compute="_compute_best_offer") + total_area = fields.Integer(compute='_compute_total_area') + best_price = fields.Float(compute='_compute_best_offer') _check_expected_price = models.Constraint( 'CHECK(expected_price > 0)', - 'The expected price must be strictly positive.', + "The expected price must be strictly positive.", ) _check_selling_price = models.Constraint( 'CHECK(selling_price >= 0)', - 'The selling price must be positive.', + "The selling price must be positive.", ) - @api.depends("living_area", "garden_area") + @api.depends('living_area', 'garden_area') def _compute_total_area(self): for record in self: record.total_area = record.living_area + record.garden_area - @api.depends("offer_ids.price") + @api.depends('offer_ids.price') def _compute_best_offer(self): for record in self: if record.offer_ids: - record.best_price = max(record.offer_ids.mapped("price")) + record.best_price = max(record.offer_ids.mapped('price')) continue record.best_price = 0.0 - @api.onchange("garden") + @api.onchange('garden') def _onchange_property(self): if self.garden: self.garden_area = 10 @@ -82,8 +94,8 @@ def _onchange_property(self): @api.ondelete(at_uninstall=False) def _check_state(self): for record in self: - if record.state == "new" or record.state == "cancelled": - raise UserError(self.env._("Can't delete a new or cancelled property.")) + if record.state not in {'new', 'canceled'}: + raise UserError(self.env._("Can't delete a property if the state is not New or Cancelled.")) @api.constrains('selling_price') def _check_selling_price(self): @@ -93,8 +105,9 @@ def _check_selling_price(self): record.selling_price, 0.9 * record.expected_price, precision_digits=2) < 0: - raise ValidationError(self.env._( - "The selling price should be at least 90% the expected price")) + raise ValidationError( + self.env._("The selling price should be at least 90% the expected price") + ) def action_cancel_property(self): for record in self: diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index d583a8c3fed..127306d5d1a 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -6,64 +6,84 @@ class PropertyOffer(models.Model): _name = 'estate.property.offer' _description = "Estate Property Offer" - _order = "price desc" + _order = 'price desc' price = fields.Float() - status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False) + status = fields.Selection( + selection=[ + ('accepted', "Accepted"), + ('refused', "Refused") + ], + copy=False, + ) partner_id = fields.Many2one('res.partner', required=True) property_id = fields.Many2one('estate.property', required=True) validity = fields.Integer(default=7) - date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline") - property_type_id = fields.Many2one('estate.property.type', related="property_id.property_type_id", store=True) + date_deadline = fields.Date( + compute='_compute_date_deadline', + inverse='_inverse_date_deadline', + ) + property_type_id = fields.Many2one( + 'estate.property.type', + related='property_id.property_type_id', + store=True, + ) _check_price = models.Constraint( 'CHECK(price > 0)', - 'An offer price must be strictly positive', + "An offer price must be strictly positive", ) @api.model_create_multi def create(self, vals_list): for vals in vals_list: - property = self.env["estate.property"].browse(vals["property_id"]) + property = self.env['estate.property'].browse(vals['property_id']) if property.offer_ids: - max_offer = max(property.mapped("offer_ids.price")) - if float_compare(vals["price"], max_offer, precision_digits=2) <= 0: - raise UserError(f"The offer must be strictly higher than {max_offer:.2f}") + max_offer = property.offer_ids[0].price + if float_compare(vals['price'], max_offer, precision_digits=2) <= 0: + raise UserError(self.env._("The offer must be strictly higher than %.2f", max_offer)) - property.state = 'offer received' + property.state = 'offer_received' return super().create(vals_list) - - @api.depends("create_date", "validity") + @api.depends('create_date', 'validity') def _compute_date_deadline(self): for record in self: - create_date = record.create_date if record.create_date else fields.Date.today() + create_date = record.create_date or fields.Date.today() record.date_deadline = fields.Date.add(create_date, days=record.validity) def _inverse_date_deadline(self): for record in self: - create_date = record.create_date if record.create_date else fields.Date.today() + create_date = record.create_date or fields.Date.today() record.validity = (record.date_deadline - fields.Date.to_date(create_date)).days def action_accept_offer(self): - self.ensure_one() + for record in self: + if record.status: + raise UserError( + self.env._("You can't accept an offer that has already been accepted/refused") + ) - self.status = 'accepted' + record.status = 'accepted' - other_offers = self.property_id.offer_ids - self - other_offers.write({'status': 'refused'}) + other_offers = record.property_id.offer_ids - record + other_offers.write({'status': 'refused'}) - self.property_id.selling_price = self.price - self.property_id.buyer = self.partner_id + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id - self.property_id.state = 'offer accepted' + record.property_id.state = 'offer_accepted' return True def action_refuse_offer(self): - self.ensure_one() - self.status = 'refused' + for record in self: + if record.status: + raise UserError( + self.env._("You can't refuse an offer that has already been accepted/refused") + ) + record.status = 'refused' return True diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 0fadd359746..f260e62f820 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -4,12 +4,12 @@ class PropertyTag(models.Model): _name = 'estate.property.tag' _description = "Estate Property Tag" - _order = "name" + _order = 'name' name = fields.Char(required=True) color = fields.Integer(string="Color") _unique_tag_name = models.Constraint( 'UNIQUE(name)', - 'The tag name must be unique.', + "The tag name must be unique.", ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index bc5c0a17dca..a726b82099d 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -4,13 +4,13 @@ class PropertyType(models.Model): _name = 'estate.property.type' _description = "Estate Property Type" - _order = "sequence, name" + _order = 'sequence, name' name = fields.Char(required=True) - property_ids = fields.One2many("estate.property", "property_type_id") + property_ids = fields.One2many('estate.property', 'property_type_id') sequence = fields.Integer('Sequence') - offer_ids = fields.One2many("estate.property.offer", "property_type_id") - offer_count = fields.Integer(compute="_compute_offer_count") + offer_ids = fields.One2many('estate.property.offer', 'property_type_id') + offer_count = fields.Integer(compute='_compute_offer_count') @api.depends('offer_ids') def _compute_offer_count(self): @@ -19,5 +19,5 @@ def _compute_offer_count(self): _unique_type_name = models.Constraint( 'UNIQUE(name)', - 'A type name must be unique.', + "A type name must be unique.", ) diff --git a/estate/models/estate_property_users.py b/estate/models/estate_property_users.py deleted file mode 100644 index 5e5b30144ca..00000000000 --- a/estate/models/estate_property_users.py +++ /dev/null @@ -1,8 +0,0 @@ -from odoo import fields, models - - -class EstatePropertyUsers(models.Model): - _inherit = "res.users" - - property_ids = fields.One2many("estate.property", "salesman", - domain=[('state', 'in', ('new','offer received'))]) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..af1472b5b7c --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'estate.property', + 'salesman_id', + domain=[('state', 'in', {'new', 'offer_received'})], + ) diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml index 9d33d5a41f1..33fd7994e85 100644 --- a/estate/views/estate_property_offer_view.xml +++ b/estate/views/estate_property_offer_view.xml @@ -10,8 +10,8 @@ -