From 137d16bac4411fbc795f2b829059be7d297847e3 Mon Sep 17 00:00:00 2001 From: macau Date: Tue, 15 Sep 2026 11:04:40 +0200 Subject: [PATCH 01/14] [IMP] estate: End of chapter 2 --- estate/__init__.py | 0 estate/__manifest__.py | 8 ++++++++ 2 files changed, 8 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..1293f186deb --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,8 @@ +{ + 'name': 'Real Estate', + 'depends': [ + 'base' + ], + "installable": True, + "application": True +} \ No newline at end of file From 23a4825fd0580f62458c1c004cb7f01b0468ac11 Mon Sep 17 00:00:00 2001 From: macau Date: Tue, 15 Sep 2026 11:58:01 +0200 Subject: [PATCH 02/14] [IMP] estate: End of chapter 3 --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 19 +++++++++++++++++++ 3 files changed, 21 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..65397116d9c --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,19 @@ +from odoo import fields, models + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Estate Property' + + name = fields.Char(string='Name', required=True) + description = fields.Text('Description') + postcode = fields.Char('Postcode') + date_availability = fields.Date('Date availability') + expected_price = fields.Float(string='Expected price', required=True) + selling_price = fields.Float('Selling price') + bedrooms = fields.Integer('Bedrooms') + living_area = fields.Integer('Living area') + facades = fields.Integer('Facades') + garage = fields.Boolean('Garage') + garden = fields.Boolean('Garden') + garden_area = fields.Integer('Garden area') + garden_orientation = fields.Selection(string='Garden orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) From 1c14732b2be6a6d36c9b7ab167fa097e004e751e Mon Sep 17 00:00:00 2001 From: macau Date: Tue, 15 Sep 2026 13:47:19 +0200 Subject: [PATCH 03/14] [IMP] estate: Adding python and JS linters --- estate/models/estate_property.py | 1 + 1 file changed, 1 insertion(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 65397116d9c..62ae7e45705 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ from odoo import fields, models + class EstateProperty(models.Model): _name = 'estate.property' _description = 'Estate Property' From 03143cdd12682ff2679fa8edd3bf2bd8dc45c448 Mon Sep 17 00:00:00 2001 From: macau Date: Tue, 15 Sep 2026 14:08:22 +0200 Subject: [PATCH 04/14] [IMP] estate: End of chapter 4 --- estate/__init__.py | 2 +- estate/__manifest__.py | 9 ++++++--- estate/models/__init__.py | 2 +- estate/security/ir.model.access.csv | 2 ++ 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 1293f186deb..31c19718a63 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,8 +1,11 @@ { 'name': 'Real Estate', + "installable": True, + "application": True, 'depends': [ - 'base' + 'base', + ], + "data": [ + "security/ir.model.access.csv", ], - "installable": True, - "application": True } \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..ab63520e22b --- /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 +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file From 4bfd2b29711fb87a93ef77e2764f9eb18f6bd2a0 Mon Sep 17 00:00:00 2001 From: macau Date: Tue, 15 Sep 2026 15:10:30 +0200 Subject: [PATCH 05/14] [IMP] estate: End of chapter 5 --- estate/__manifest__.py | 3 ++ estate/models/estate_property.py | 44 ++++++++++++++++++++++---- estate/views/estate_menus.xml | 8 +++++ estate/views/estate_property_views.xml | 8 +++++ 4 files changed, 57 insertions(+), 6 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 31c19718a63..343601c4579 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,5 +7,8 @@ ], "data": [ "security/ir.model.access.csv", + + "views/estate_property_views.xml", + "views/estate_menus.xml", ], } \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 62ae7e45705..e3597b9bd23 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,16 +5,48 @@ class EstateProperty(models.Model): _name = 'estate.property' _description = 'Estate Property' - name = fields.Char(string='Name', required=True) + name = fields.Char( + string='Name', + required=True, + default="Unknown", + ) description = fields.Text('Description') postcode = fields.Char('Postcode') - date_availability = fields.Date('Date availability') - expected_price = fields.Float(string='Expected price', required=True) - selling_price = fields.Float('Selling price') - bedrooms = fields.Integer('Bedrooms') + date_availability = fields.Date( + string='Date availability', + copy=False, + default=fields.Date.add(fields.Date.today(), 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='Bedrooms', + default=2, + ) living_area = fields.Integer('Living area') facades = fields.Integer('Facades') garage = fields.Boolean('Garage') garden = fields.Boolean('Garden') garden_area = fields.Integer('Garden area') - garden_orientation = fields.Selection(string='Garden orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) + garden_orientation = fields.Selection( + string='Garden orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + ) + active = fields.Boolean( + string='Active', + 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=True, + default='new', + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..48466b0c1ff --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..825ae24d7d4 --- /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 e57335a993e17ab12cfabf2640a03d39152bfc26 Mon Sep 17 00:00:00 2001 From: macau Date: Tue, 15 Sep 2026 16:27:11 +0200 Subject: [PATCH 06/14] [IMP] estate: End of chapter 6 Update of the list and form views and of the search field. --- estate/__manifest__.py | 4 +- estate/views/estate_property_views.xml | 75 +++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 343601c4579..89a42ae4c6c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,5 +1,7 @@ { 'name': 'Real Estate', + 'author': 'macau', + 'license': 'LGPL-3', "installable": True, "application": True, 'depends': [ @@ -11,4 +13,4 @@ "views/estate_property_views.xml", "views/estate_menus.xml", ], -} \ No newline at end of file +} diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 825ae24d7d4..8e2038ea80b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,8 +1,81 @@ - Properties + estate.property.action estate_property list,form + + + estate.property.list + estate_property + + + + + + + + + + + + + + + estate.property.form + estate_property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate_property + + + + + + + + + + + + +
\ No newline at end of file From 35fd32d35660077d33c636bf7bb35b5455f421b9 Mon Sep 17 00:00:00 2001 From: macau Date: Tue, 15 Sep 2026 17:31:01 +0200 Subject: [PATCH 07/14] [IMP] estate: End of chapter 7 Adding relations between models. --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 7 ++++- estate/models/estate_property.py | 5 +++ estate/models/estate_property_offer.py | 15 +++++++++ estate/models/estate_property_tag.py | 12 ++++++++ estate/models/estate_property_type.py | 12 ++++++++ estate/security/ir.model.access.csv | 5 ++- estate/views/estate_menus.xml | 4 +++ estate/views/estate_property_offer_views.xml | 32 ++++++++++++++++++++ estate/views/estate_property_tag_views.xml | 24 +++++++++++++++ estate/views/estate_property_type_views.xml | 24 +++++++++++++++ estate/views/estate_property_views.xml | 21 ++++++++++++- 12 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 89a42ae4c6c..ea612364a06 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,6 +11,9 @@ "security/ir.model.access.csv", "views/estate_property_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_type_views.xml", "views/estate_menus.xml", ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..3683ff97b61 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 e3597b9bd23..e5f2744ef6b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -50,3 +50,8 @@ class EstateProperty(models.Model): copy=True, default='new', ) + property_type = fields.Many2one('estate_property_type', string='Property type') + buyer = fields.Many2one('res.partner', string='Buyer', copy=False) + salesperson = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user) + tags = fields.Many2many('estate_property_tag', string='Tags') + offers = 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 00000000000..b8ada6cef77 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = 'estate_property_offer' + _description = 'Estate Property Offer' + + price = fields.Float('Price') + status = fields.Selection( + string='Status', + selection=[('accepted', 'Accepted'), ('refused', 'Refused')], + copy=False, + ) + partner_id = fields.Many2one('res.partner', string='Partner', 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 00000000000..dedd1d8eef9 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = 'estate_property_tag' + _description = 'Estate Property Tag' + + name = fields.Char( + string='Name', + required=True, + default="Unknown", + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..74407d4b69b --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate_property_type' + _description = 'Estate Property Type' + + name = fields.Char( + string='Name', + required=True, + default="Unknown", + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index ab63520e22b..c706f12ad03 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 -estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_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_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_menus.xml b/estate/views/estate_menus.xml index 48466b0c1ff..d9faa85af45 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,9 @@ + + + + \ 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 00000000000..c73976de3f5 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,32 @@ + + + + estate.property.offer.list + estate_property_offer + + + + + + + + + + + + estate.property.offer.form + estate_property_offer + +
+ + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..c86fe01dbbb --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,24 @@ + + + + Property Tags + estate_property_tag + list,form + + + + estate.property.tag.form + estate_property_tag + +
+ +
+

+ +

+
+
+
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..9ec35115691 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,24 @@ + + + + Property Types + estate_property_type + list,form + + + + estate.property.type.form + estate_property_type + +
+ +
+

+ +

+
+
+
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 8e2038ea80b..11284d4cb43 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,7 +1,7 @@ - estate.property.action + Properties estate_property list,form @@ -12,12 +12,14 @@ + + @@ -33,8 +35,10 @@ + + @@ -56,6 +60,15 @@ + + + + + + + + + @@ -68,13 +81,19 @@ + + + + + + From 888003d5371d79b06d03c1b697a30a56327a491d Mon Sep 17 00:00:00 2001 From: macau Date: Wed, 16 Sep 2026 10:20:21 +0200 Subject: [PATCH 08/14] [IMP] estate: End of chapter 8 Adding computed fields and onchange observators. --- estate/models/estate_property.py | 83 +++++++++++++++++--- estate/models/estate_property_offer.py | 41 +++++++++- estate/views/estate_property_offer_views.xml | 4 + estate/views/estate_property_views.xml | 2 + 4 files changed, 113 insertions(+), 17 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index e5f2744ef6b..d9f1dfb6f3d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import _, api, fields, models class EstateProperty(models.Model): @@ -10,8 +10,12 @@ class EstateProperty(models.Model): required=True, default="Unknown", ) - description = fields.Text('Description') - postcode = fields.Char('Postcode') + description = fields.Text( + string='Description', + ) + postcode = fields.Char( + string='Postcode', + ) date_availability = fields.Date( string='Date availability', copy=False, @@ -30,11 +34,21 @@ class EstateProperty(models.Model): string='Bedrooms', default=2, ) - living_area = fields.Integer('Living area') - facades = fields.Integer('Facades') - garage = fields.Boolean('Garage') - garden = fields.Boolean('Garden') - garden_area = fields.Integer('Garden area') + living_area = fields.Integer( + string='Living area', + ) + facades = fields.Integer( + string='Facades', + ) + garage = fields.Boolean( + string='Garage', + ) + garden = fields.Boolean( + string='Garden', + ) + garden_area = fields.Integer( + string='Garden area', + ) garden_orientation = fields.Selection( string='Garden orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], @@ -50,8 +64,51 @@ class EstateProperty(models.Model): copy=True, default='new', ) - property_type = fields.Many2one('estate_property_type', string='Property type') - buyer = fields.Many2one('res.partner', string='Buyer', copy=False) - salesperson = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user) - tags = fields.Many2many('estate_property_tag', string='Tags') - offers = fields.One2many('estate_property_offer', 'property_id', string='Offers') + property_type = fields.Many2one( + comodel_name='estate_property_type', + string='Property type', + ) + buyer = fields.Many2one( + comodel_name='res.partner', + string='Buyer', + copy=False, + ) + salesperson = fields.Many2one( + comodel_name='res.users', + string='Salesperson', + default=lambda self: self.env.user, + ) + tags = fields.Many2many( + comodel_name='estate_property_tag', + string='Tags', + ) + offers = fields.One2many( + comodel_name='estate_property_offer', + inverse_name='property_id', + string='Offers', + ) + total_area = fields.Integer( + compute='_compute_total_area', + ) + best_price = fields.Float( + compute='_compute_best_price', + ) + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends('offers.price') + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offers.mapped('price')) + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = None diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index b8ada6cef77..24b118b4865 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,15 +1,48 @@ -from odoo import fields, models +from datetime import datetime, timedelta + +from odoo import api, fields, models class EstatePropertyOffer(models.Model): _name = 'estate_property_offer' _description = 'Estate Property Offer' - price = fields.Float('Price') + price = fields.Float( + string='Price', + ) status = fields.Selection( string='Status', selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False, ) - partner_id = fields.Many2one('res.partner', string='Partner', required=True) - property_id = fields.Many2one('estate_property', string='Property', required=True) + partner_id = fields.Many2one( + comodel_name='res.partner', + string='Partner', + required=True, + ) + property_id = fields.Many2one( + comodel_name='estate_property', + string='Property', + required=True, + ) + validity = fields.Integer( + string='Validity', + default=7, + ) + date_deadline = fields.Date( + string='Deadline', + compute='_compute_date_deadline', + inverse='_inverse_date_deadline', + ) + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = record.create_date + timedelta(days=record.validity) + else: + record.date_deadline = datetime.now() + timedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index c73976de3f5..97e80200ed0 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + + @@ -24,6 +26,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 11284d4cb43..c4774a0eb55 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -44,6 +44,7 @@ + @@ -58,6 +59,7 @@ + From 5c8803057e979cb0bf50c9831330cfd1d74aaea3 Mon Sep 17 00:00:00 2001 From: macau Date: Wed, 16 Sep 2026 12:25:42 +0200 Subject: [PATCH 09/14] [IMP] estate: End of chapter 9 Adding some actions. In addition, the corrections related to the PR review for some previous commits are added with this one for simplicity. The first commits are modified directly through git rebase so I known how to do it. Next time, I will modify commit by commit. --- estate/__manifest__.py | 2 +- estate/models/__init__.py | 10 +- estate/models/estate_property.py | 122 +++++++------------ estate/models/estate_property_offer.py | 60 +++++---- estate/models/estate_property_tag.py | 8 +- estate/models/estate_property_type.py | 8 +- estate/security/ir.model.access.csv | 8 +- estate/views/estate_menus.xml | 12 +- estate/views/estate_property_offer_views.xml | 21 +++- estate/views/estate_property_tag_views.xml | 10 +- estate/views/estate_property_type_views.xml | 10 +- estate/views/estate_property_views.xml | 29 +++-- 12 files changed, 148 insertions(+), 152 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index ea612364a06..110b290c683 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Real Estate', - 'author': 'macau', + 'author': 'Odoo S.A.', 'license': 'LGPL-3', "installable": True, "application": True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 3683ff97b61..8f2187ee09e 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,6 +1,4 @@ -from . import ( - estate_property, - estate_property_offer, - estate_property_tag, - estate_property_type, -) +from . import estate_property +from . import estate_property_offer +from . import estate_property_tag +from . import estate_property_type diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d9f1dfb6f3d..5116ae574ca 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,62 +1,37 @@ from odoo import _, api, fields, models +from odoo.exceptions import UserError class EstateProperty(models.Model): _name = 'estate.property' _description = 'Estate Property' - name = fields.Char( - string='Name', - required=True, - default="Unknown", - ) - description = fields.Text( - string='Description', - ) - postcode = fields.Char( - string='Postcode', - ) + active = fields.Boolean(string='Active', default=True) + bedrooms = fields.Integer(string='Bedrooms', default=2) + best_price = fields.Float(compute='_compute_best_price') + buyer = fields.Many2one(comodel_name='res.partner', readonly=True, string='Buyer', copy=False) date_availability = fields.Date( string='Date availability', copy=False, - default=fields.Date.add(fields.Date.today(), 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='Bedrooms', - default=2, - ) - living_area = fields.Integer( - string='Living area', - ) - facades = fields.Integer( - string='Facades', - ) - garage = fields.Boolean( - string='Garage', - ) - garden = fields.Boolean( - string='Garden', - ) - garden_area = fields.Integer( - string='Garden area', - ) + default=lambda self: fields.Date.add(fields.Date.today(), months=3), + ) + description = fields.Text(string='Description') + expected_price = fields.Float(string='Expected price', required=True) + facades = fields.Integer(string='Facades') + garage = fields.Boolean(string='Garage') + garden = fields.Boolean(string='Garden') + garden_area = fields.Integer(string='Garden area') garden_orientation = fields.Selection( string='Garden orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], ) - active = fields.Boolean( - string='Active', - default=True, - ) + living_area = fields.Integer(string='Living area') + name = fields.Char(string='Name', required=True, default="Unknown") + offers = fields.One2many(comodel_name='estate.property.offer', inverse_name='property_id', string='Offers') + postcode = fields.Char(string='Postcode') + property_type = fields.Many2one(comodel_name='estate.property.type', string='Property type') + salesperson = fields.Many2one(comodel_name='res.users', string='Salesperson', default=lambda self: self.env.user) + selling_price = fields.Float(string='Selling price', readonly=True, copy=False) state = fields.Selection( string='State', selection=[('new', 'New'), ('offer_received', 'Offer received'), ('offer_accepted', 'Offer accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], @@ -64,35 +39,8 @@ class EstateProperty(models.Model): copy=True, default='new', ) - property_type = fields.Many2one( - comodel_name='estate_property_type', - string='Property type', - ) - buyer = fields.Many2one( - comodel_name='res.partner', - string='Buyer', - copy=False, - ) - salesperson = fields.Many2one( - comodel_name='res.users', - string='Salesperson', - default=lambda self: self.env.user, - ) - tags = fields.Many2many( - comodel_name='estate_property_tag', - string='Tags', - ) - offers = fields.One2many( - comodel_name='estate_property_offer', - inverse_name='property_id', - string='Offers', - ) - total_area = fields.Integer( - compute='_compute_total_area', - ) - best_price = fields.Float( - compute='_compute_best_price', - ) + tags = fields.Many2many(comodel_name='estate.property.tag', string='Tags') + total_area = fields.Integer(compute='_compute_total_area') @api.depends('living_area', 'garden_area') def _compute_total_area(self): @@ -102,7 +50,7 @@ def _compute_total_area(self): @api.depends('offers.price') def _compute_best_price(self): for record in self: - record.best_price = max(record.offers.mapped('price')) + record.best_price = max(record.offers.mapped('price')) if record.offers else 0 @api.onchange('garden') def _onchange_garden(self): @@ -112,3 +60,27 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = None + + def sold_property(self): + if any(record.state == 'cancelled' for record in self): + raise UserError(_('A cancelled property cannot be sold.')) + for record in self: + record.state = 'sold' + return True + + def cancel_property(self): + if any(record.state == 'sold' for record in self): + raise UserError(_('A sold property cannot be cancelled.')) + for record in self: + record.state = 'cancelled' + return True + + def action_view_offers(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'res_model': 'estate.property.offer', + 'name': _('Offers'), + 'views': [[False, 'list'], [False, 'form']], + 'domain': [('property_id', '=', self.id)], + } diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 24b118b4865..66cb6e786df 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,39 +1,24 @@ from datetime import datetime, timedelta -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): - _name = 'estate_property_offer' + _name = 'estate.property.offer' _description = 'Estate Property Offer' - price = fields.Float( - string='Price', - ) + date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline') + partner_id = fields.Many2one(comodel_name='res.partner', string='Partner', required=True) + price = fields.Float(string='Price') + property_id = fields.Many2one(comodel_name='estate.property', string='Property', required=True) status = fields.Selection( string='Status', + readonly=True, selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False, ) - partner_id = fields.Many2one( - comodel_name='res.partner', - string='Partner', - required=True, - ) - property_id = fields.Many2one( - comodel_name='estate_property', - string='Property', - required=True, - ) - validity = fields.Integer( - string='Validity', - default=7, - ) - date_deadline = fields.Date( - string='Deadline', - compute='_compute_date_deadline', - inverse='_inverse_date_deadline', - ) + validity = fields.Integer(string='Validity', default=7) @api.depends('create_date', 'validity') def _compute_date_deadline(self): @@ -46,3 +31,30 @@ def _compute_date_deadline(self): def _inverse_date_deadline(self): for record in self: record.validity = (record.date_deadline - record.create_date.date()).days + + def accept_offer(self): + if any(any(offer.status == 'accepted' for offer in record.property_id.offers) for record in self): + raise UserError(_('Only one offer can be accepted by property.')) + for record in self: + record.status = 'accepted' + record.property_id.buyer = record.partner_id + record.property_id.selling_price = record.price + return True + + def refuse_offer(self): + for record in self: + if record.status == 'accepted': + record.property_id.buyer = None + record.property_id.selling_price = None + record.status = 'refused' + return True + + def action_view_property(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'res_model': 'estate.property', + 'name': _('Properties'), + 'views': [[False, 'list'], [False, 'form']], + 'domain': [('id', '=', self.property_id.id)], + } diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index dedd1d8eef9..7a865a4d22b 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -2,11 +2,7 @@ class EstatePropertyTag(models.Model): - _name = 'estate_property_tag' + _name = 'estate.property.tag' _description = 'Estate Property Tag' - name = fields.Char( - string='Name', - required=True, - default="Unknown", - ) + name = fields.Char(string='Name', required=True, default="Unknown") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 74407d4b69b..6fa6fa2b941 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -2,11 +2,7 @@ class EstatePropertyType(models.Model): - _name = 'estate_property_type' + _name = 'estate.property.type' _description = 'Estate Property Type' - name = fields.Char( - string='Name', - required=True, - default="Unknown", - ) + name = fields.Char(string='Name', required=True, default="Unknown") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index c706f12ad03..4a6c6023026 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 -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_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 \ No newline at end of file +estate.access.estate.property,access.estate.property,model_estate_property,base.group_user,1,1,1,1 +estate.access.estate.property.offer,access.estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1 +estate.access.estate.property.tag,access.estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1 +estate.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 d9faa85af45..fb16d3442c3 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,12 +1,12 @@ - - + + - - - + + + - \ No newline at end of file + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 97e80200ed0..d75c7f74053 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,8 +1,8 @@ - + estate.property.offer.list - estate_property_offer + estate.property.offer @@ -11,16 +11,27 @@ + + @@ -33,4 +44,4 @@ - \ No newline at end of file + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index c86fe01dbbb..7e2c4fc027f 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -1,14 +1,14 @@ - + Property Tags - estate_property_tag + estate.property.tag list,form - + estate.property.tag.form - estate_property_tag + estate.property.tag
@@ -21,4 +21,4 @@
-
\ No newline at end of file + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 9ec35115691..0652b056fc6 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -1,14 +1,14 @@ - + Property Types - estate_property_type + estate.property.type list,form - + estate.property.type.form - estate_property_type + estate.property.type
@@ -21,4 +21,4 @@
-
\ No newline at end of file + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index c4774a0eb55..e3a2f6fa6f5 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,14 +1,14 @@ - + Properties - estate_property + estate.property list,form - + estate.property.list - estate_property + estate.property @@ -24,20 +24,31 @@ - + estate.property.form - estate_property + estate.property
+
+
+
+ +

+ + @@ -77,9 +88,9 @@ - + estate.property.search - estate_property + estate.property @@ -99,4 +110,4 @@ - \ No newline at end of file + From 1d4ff807b34146cfb50f275a2a5dfcf6f51c3556 Mon Sep 17 00:00:00 2001 From: macau Date: Thu, 17 Sep 2026 09:49:22 +0200 Subject: [PATCH 10/14] [IMP] estate: End of chapter 10 Implementation of constraints on the models' fields. --- estate/models/estate_property.py | 26 +++++++++++++++++++++----- estate/models/estate_property_offer.py | 24 ++++++++++++++++++++---- estate/models/estate_property_tag.py | 5 +++++ estate/models/estate_property_type.py | 5 +++++ 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 5116ae574ca..4274ca51414 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ -from odoo import _, api, fields, models -from odoo.exceptions import UserError +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero class EstateProperty(models.Model): @@ -42,6 +43,15 @@ class EstateProperty(models.Model): tags = fields.Many2many(comodel_name='estate.property.tag', string='Tags') total_area = fields.Integer(compute='_compute_total_area') + _strictly_positive_expected_price = models.Constraint( + 'CHECK(expected_price > 0)', + 'The expected price of a property should be strictly positive.', + ) + _positive_selling_price = models.Constraint( + 'CHECK(selling_price >= 0)', + 'The selling price of a property should be positive.', + ) + @api.depends('living_area', 'garden_area') def _compute_total_area(self): for record in self: @@ -63,14 +73,14 @@ def _onchange_garden(self): def sold_property(self): if any(record.state == 'cancelled' for record in self): - raise UserError(_('A cancelled property cannot be sold.')) + raise UserError(self.env._('A cancelled property cannot be sold.')) for record in self: record.state = 'sold' return True def cancel_property(self): if any(record.state == 'sold' for record in self): - raise UserError(_('A sold property cannot be cancelled.')) + raise UserError(self.env._('A sold property cannot be cancelled.')) for record in self: record.state = 'cancelled' return True @@ -80,7 +90,13 @@ def action_view_offers(self): return { 'type': 'ir.actions.act_window', 'res_model': 'estate.property.offer', - 'name': _('Offers'), + 'name': self.env._('Offers'), 'views': [[False, 'list'], [False, 'form']], 'domain': [('property_id', '=', self.id)], } + + @api.constrains('expected_price', 'selling_price') + def _check_selling_price_relation_to_expected_price(self): + for record in self: + if float_compare(record.expected_price * 0.9, record.selling_price, 2) == 1 and not float_is_zero(record.selling_price, 2): + raise ValidationError(self.env._(r'The selling price 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 66cb6e786df..a3e34bd252f 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,7 +1,8 @@ from datetime import datetime, timedelta -from odoo import _, api, fields, models -from odoo.exceptions import UserError +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare class EstatePropertyOffer(models.Model): @@ -20,6 +21,15 @@ class EstatePropertyOffer(models.Model): ) validity = fields.Integer(string='Validity', default=7) + _strictly_positive_price = models.Constraint( + 'CHECK(price > 0)', + 'The price of an offer should be strictly positive', + ) + + def _compute_display_name(self): + for record in self: + record.display_name = self.env._("Estate Property Offer %s", self.id) + @api.depends('create_date', 'validity') def _compute_date_deadline(self): for record in self: @@ -34,7 +44,7 @@ def _inverse_date_deadline(self): def accept_offer(self): if any(any(offer.status == 'accepted' for offer in record.property_id.offers) for record in self): - raise UserError(_('Only one offer can be accepted by property.')) + raise UserError(self.env._('Only one offer can be accepted by property.')) for record in self: record.status = 'accepted' record.property_id.buyer = record.partner_id @@ -54,7 +64,13 @@ def action_view_property(self): return { 'type': 'ir.actions.act_window', 'res_model': 'estate.property', - 'name': _('Properties'), + 'name': self.env._('Properties'), 'views': [[False, 'list'], [False, 'form']], 'domain': [('id', '=', self.property_id.id)], } + + @api.constrains('price') + def _check_price(self): + for record in self: + if float_compare(record.property_id.expected_price * 0.9, record.price, 2) == 1: + raise ValidationError(self.env._(r'The price of an offer cannot be lower than 90% of the expected price of the property.')) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 7a865a4d22b..4cf347fad91 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -6,3 +6,8 @@ class EstatePropertyTag(models.Model): _description = 'Estate Property Tag' name = fields.Char(string='Name', required=True, default="Unknown") + + _unique_tag_name = models.Constraint( + 'unique (name)', + 'The name of a property tag should be unique.', + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 6fa6fa2b941..4b36f476b1f 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -6,3 +6,8 @@ class EstatePropertyType(models.Model): _description = 'Estate Property Type' name = fields.Char(string='Name', required=True, default="Unknown") + + _unique_type_name = models.Constraint( + 'unique (name)', + 'The name of a property type should be unique.', + ) From 356052ad3f5218fa0217c8dc22b261df1fc33ea3 Mon Sep 17 00:00:00 2001 From: macau Date: Thu, 17 Sep 2026 13:11:03 +0200 Subject: [PATCH 11/14] [IMP] estate: End of chapter 11 --- estate/models/estate_property.py | 1 + estate/models/estate_property_offer.py | 4 +++ estate/models/estate_property_tag.py | 2 ++ estate/models/estate_property_type.py | 12 ++++++++- estate/views/estate_property_offer_views.xml | 14 +++++++--- estate/views/estate_property_tag_views.xml | 14 ++++++++++ estate/views/estate_property_type_views.xml | 27 +++++++++++++++++++ estate/views/estate_property_views.xml | 28 ++++++++++++-------- 8 files changed, 86 insertions(+), 16 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4274ca51414..301930f8b85 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -6,6 +6,7 @@ class EstateProperty(models.Model): _name = 'estate.property' _description = 'Estate Property' + _order = 'id desc' active = fields.Boolean(string='Active', default=True) bedrooms = fields.Integer(string='Bedrooms', default=2) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index a3e34bd252f..53e58acc037 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -8,11 +8,13 @@ class EstatePropertyOffer(models.Model): _name = 'estate.property.offer' _description = 'Estate Property Offer' + _order = 'price desc' date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline') partner_id = fields.Many2one(comodel_name='res.partner', string='Partner', required=True) price = fields.Float(string='Price') property_id = fields.Many2one(comodel_name='estate.property', string='Property', required=True) + property_type = fields.Many2one(related='property_id.property_type', string='Property type') status = fields.Selection( string='Status', readonly=True, @@ -49,6 +51,7 @@ def accept_offer(self): record.status = 'accepted' record.property_id.buyer = record.partner_id record.property_id.selling_price = record.price + record.property_id.state = 'offer_accepted' return True def refuse_offer(self): @@ -56,6 +59,7 @@ def refuse_offer(self): if record.status == 'accepted': record.property_id.buyer = None record.property_id.selling_price = None + record.property_id.state = 'offer_received' record.status = 'refused' return True diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 4cf347fad91..ffca340f37d 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -4,8 +4,10 @@ class EstatePropertyTag(models.Model): _name = 'estate.property.tag' _description = 'Estate Property Tag' + _order = 'name' name = fields.Char(string='Name', required=True, default="Unknown") + color = fields.Integer(string='Color') _unique_tag_name = models.Constraint( 'unique (name)', diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 4b36f476b1f..8e96424e5ab 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,13 +1,23 @@ -from odoo import fields, models +from odoo import api, fields, models class EstatePropertyType(models.Model): _name = 'estate.property.type' _description = 'Estate Property Type' + _order = 'sequence, name' name = fields.Char(string='Name', required=True, default="Unknown") + offers = fields.One2many(comodel_name='estate.property.offer', inverse_name='property_type', string="Offers") + offers_count = fields.Integer(string='Offers count', compute='_compute_offers_count') + properties = fields.One2many(comodel_name='estate.property', inverse_name='property_type', string='Properties') + sequence = fields.Integer(string='Sequence', default=1, help='Used to order stages. Lower is better.') _unique_type_name = models.Constraint( 'unique (name)', 'The name of a property type should be unique.', ) + + @api.depends('offers') + def _compute_offers_count(self): + for record in self: + record.offers_count = len(record.offers) diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index d75c7f74053..663f64870ca 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,18 +1,24 @@ + + Property Offers + estate.property.offer + list,form + [('property_type', '=', active_id)] + + estate.property.offer.list estate.property.offer - + - - +

+ + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e3a2f6fa6f5..4af94f3f9c7 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -4,13 +4,19 @@ Properties estate.property list,form + {'search_default_available': True}
estate.property.list estate.property - + @@ -18,7 +24,7 @@ - + @@ -30,8 +36,9 @@
-
@@ -44,12 +51,11 @@
- + - - + @@ -68,13 +74,13 @@ - - + + - + @@ -98,7 +104,7 @@ - + From f2df72557aaf6642e643bc53aa3e615369dc59ce Mon Sep 17 00:00:00 2001 From: macau Date: Thu, 17 Sep 2026 15:48:52 +0200 Subject: [PATCH 12/14] [IMP] estate: End of chapter 12 --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_inherited_users.py | 12 ++++++++++++ estate/models/estate_property.py | 13 +++++++++---- estate/models/estate_property_offer.py | 14 ++++++++++++-- estate/views/estate_inherited_users_views.xml | 15 +++++++++++++++ 6 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 estate/models/estate_inherited_users.py create mode 100644 estate/views/estate_inherited_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 110b290c683..9486f78213b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -15,5 +15,6 @@ "views/estate_property_tag_views.xml", "views/estate_property_type_views.xml", "views/estate_menus.xml", + "views/estate_inherited_users_views.xml", ], } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8f2187ee09e..b94f7a1ae29 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,3 +1,4 @@ +from . import estate_inherited_users from . import estate_property from . import estate_property_offer from . import estate_property_tag diff --git a/estate/models/estate_inherited_users.py b/estate/models/estate_inherited_users.py new file mode 100644 index 00000000000..cac471b3532 --- /dev/null +++ b/estate/models/estate_inherited_users.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstateInheritedUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + comodel_name='estate.property', + inverse_name='salesperson', + string='Estate Properties', + domain=['|', ('state', '=', 'new'), ('state', '=', 'offer_received')], + ) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 301930f8b85..d44438599d9 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -73,16 +73,16 @@ def _onchange_garden(self): self.garden_orientation = None def sold_property(self): - if any(record.state == 'cancelled' for record in self): - raise UserError(self.env._('A cancelled property cannot be sold.')) for record in self: + if record.state == 'cancelled': + raise UserError(self.env._('A cancelled property cannot be sold.')) record.state = 'sold' return True def cancel_property(self): - if any(record.state == 'sold' for record in self): - raise UserError(self.env._('A sold property cannot be cancelled.')) for record in self: + if record.state == 'sold': + raise UserError(self.env._('A sold property cannot be cancelled.')) record.state = 'cancelled' return True @@ -101,3 +101,8 @@ def _check_selling_price_relation_to_expected_price(self): for record in self: if float_compare(record.expected_price * 0.9, record.selling_price, 2) == 1 and not float_is_zero(record.selling_price, 2): raise ValidationError(self.env._(r'The selling price cannot be lower than 90% of the expected price.')) + + @api.ondelete(at_uninstall=False) + def _unlink_if_status_is_new_or_cancelled(self): + if any(record.state not in ['new', 'cancelled'] for record in self): + raise UserError(self.env._('Can\'t delete a property that is not new or cancelled.')) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 53e58acc037..69114645fc1 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -45,9 +45,9 @@ def _inverse_date_deadline(self): record.validity = (record.date_deadline - record.create_date.date()).days def accept_offer(self): - if any(any(offer.status == 'accepted' for offer in record.property_id.offers) for record in self): - raise UserError(self.env._('Only one offer can be accepted by property.')) for record in self: + if any(offer.status == 'accepted' for offer in record.property_id.offers): + raise UserError(self.env._('Only one offer can be accepted by property.')) record.status = 'accepted' record.property_id.buyer = record.partner_id record.property_id.selling_price = record.price @@ -78,3 +78,13 @@ def _check_price(self): for record in self: if float_compare(record.property_id.expected_price * 0.9, record.price, 2) == 1: raise ValidationError(self.env._(r'The price of an offer cannot be lower than 90% of the expected price of the property.')) + + @api.model + def create(self, vals_list): + for vals in vals_list: + property = self.env['estate.property'].browse(vals['property_id']).with_prefetch(self.ids) + if any(offer.price > vals['price'] for offer in property.offers): + raise UserError(self.env._('An offer cannot have a lower price than an existing offer.')) + if property.state == 'new': + property.state = 'offer_received' + return super().create(vals_list) diff --git a/estate/views/estate_inherited_users_views.xml b/estate/views/estate_inherited_users_views.xml new file mode 100644 index 00000000000..369aa70af4a --- /dev/null +++ b/estate/views/estate_inherited_users_views.xml @@ -0,0 +1,15 @@ + + + + estate.inherited.users.form + res.users + + + + + + + + + + From e085d2f80a5b3f7d52848cf0da6663044db2cb73 Mon Sep 17 00:00:00 2001 From: macau Date: Thu, 17 Sep 2026 16:37:29 +0200 Subject: [PATCH 13/14] [IMP] estate: End of chapter 13 --- estate/models/estate_property_offer.py | 2 +- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 9 +++++++ .../models/EstateInheritedProperty.py | 27 +++++++++++++++++++ estate_account/models/__init__.py | 1 + 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/EstateInheritedProperty.py create mode 100644 estate_account/models/__init__.py diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 69114645fc1..4406ce6f38f 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -30,7 +30,7 @@ class EstatePropertyOffer(models.Model): def _compute_display_name(self): for record in self: - record.display_name = self.env._("Estate Property Offer %s", self.id) + record.display_name = self.env._("Estate Property Offer %s", record.id) @api.depends('create_date', 'validity') def _compute_date_deadline(self): 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..d1f1f81f4dc --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,9 @@ +{ + 'name': 'Real Estate Account', + 'author': 'Odoo S.A.', + 'license': 'LGPL-3', + 'depends': [ + 'estate', + 'account', + ], +} diff --git a/estate_account/models/EstateInheritedProperty.py b/estate_account/models/EstateInheritedProperty.py new file mode 100644 index 00000000000..bb40d1069d8 --- /dev/null +++ b/estate_account/models/EstateInheritedProperty.py @@ -0,0 +1,27 @@ +from odoo import Command, models + + +class EstateInheritedProperty(models.Model): + _inherit = 'estate.property' + + def sold_property(self): + journal_id = self.env['account.journal'].search([('code', '=', 'INV')], limit=1).id + for record in self: + self.env['account.move'].create({ + 'partner_id': record.buyer.id, + 'move_type': 'out_invoice', + 'journal_id': journal_id, + 'invoice_line_ids': [ + Command.create({ + 'name': self.env._('Purchase of %s', record.id), + 'quantity': 1, + 'price_unit': record.selling_price * 0.06, + }), + Command.create({ + 'name': self.env._('Administrative fees'), + 'quantity': 1, + 'price_unit': 100, + }), + ], + }) + return super().sold_property() diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..7c7fdcc432c --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import EstateInheritedProperty From a786d99a7312a2528a16964bd7a49f7950660f34 Mon Sep 17 00:00:00 2001 From: macau Date: Thu, 17 Sep 2026 22:42:46 +0200 Subject: [PATCH 14/14] [IMP] estate: End of chapter 14 --- estate/views/estate_property_views.xml | 31 +++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 4af94f3f9c7..7984ce6c47c 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,10 +3,39 @@ Properties estate.property - list,form + list,form,kanban {'search_default_available': True} + + estate.property.kanban + estate.property + + + + + +
+ +
+
+ Expected price: +
+
+ Best offfer: +
+
+ Selling price: +
+
+ +
+
+
+
+
+
+ estate.property.list estate.property