From a138c78adb088365ed197832b1c74751843dd274 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Mon, 19 Jan 2026 15:24:33 +0100 Subject: [PATCH 01/17] [ADD] estate: create the estate module for Ch2 of the tutorial - created init and manifest files - added the name and dependencies --- estate/__init__.py | 0 estate/__manifest__.py | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..28014e457f1 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Real Estate', + 'depends': ['base'], + 'application': True, +} \ No newline at end of file From 96111b91921b2e187a8f93396ed13ab29953e9ff Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Mon, 19 Jan 2026 15:38:51 +0100 Subject: [PATCH 02/17] [IMP] estate: add estate_property model - create the estate property model - add fields and attributes --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 20 ++++++++++++++++++++ 3 files changed, 22 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..1e0d49523a4 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,20 @@ +from odoo import models, fields + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Estate Property Information' + + name = fields.Char(string='Property Name', required=True) + description = fields.Text(string='Description') + postcode = fields.Char(string='Postcode') + date_availability = fields.Date(string='Available Date') + expected_price = fields.Float(string='Expected Price', required=True) + selling_price = fields.Float(string='Selling Price') + bedrooms = fields.Integer(string='Number of Bedrooms') + living_area = fields.Integer(string='Living Area') + facades = fields.Integer(string='Number of Facades') + garage = fields.Boolean(string='Garage') + garden = fields.Boolean(string='Garden') + garden_area = fields.Integer(string='Garden Area') + garden_orientation = fields.Selection(('north', 'south', 'east', 'west'), string='Garden Orientation') + \ No newline at end of file From 64c4787dbc40dc752d35e14e1d156c9d0ac33370 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Mon, 19 Jan 2026 16:06:22 +0100 Subject: [PATCH 03/17] [IMP] estate: add access rights - created access.csv file - defined the read/write permissions to be able to access the model --- estate/__manifest__.py | 3 +++ estate/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 28014e457f1..cf3f6611d40 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -2,5 +2,8 @@ { 'name': 'Real Estate', 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + ], 'application': True, } \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..976b61e8cb3 --- /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 \ No newline at end of file From 7d946f02cf0ef2bbdbd379ea27fd5fc19421d315 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Mon, 19 Jan 2026 17:37:13 +0100 Subject: [PATCH 04/17] [IMP] estate: add UI interaction by creating the form and adding menus - created menus to see the fields of the model --- estate/__manifest__.py | 2 ++ estate/models/estate_property.py | 18 +++++++++++++----- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 4 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index cf3f6611d40..0f34bd14a8b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,6 +4,8 @@ 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'application': True, } \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 1e0d49523a4..3f582b7d177 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -7,14 +7,22 @@ class EstateProperty(models.Model): name = fields.Char(string='Property Name', required=True) description = fields.Text(string='Description') postcode = fields.Char(string='Postcode') - date_availability = fields.Date(string='Available Date') + date_availability = fields.Date(string='Available Date', 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') - bedrooms = fields.Integer(string='Number of Bedrooms') + selling_price = fields.Float(string='Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer(string='Number of Bedrooms', default=2) living_area = fields.Integer(string='Living Area') facades = fields.Integer(string='Number of Facades') garage = fields.Boolean(string='Garage') garden = fields.Boolean(string='Garden') garden_area = fields.Integer(string='Garden Area') - garden_orientation = fields.Selection(('north', 'south', 'east', 'west'), string='Garden Orientation') - \ No newline at end of file + garden_orientation = fields.Selection([('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], string='Garden Orientation') + active = fields.Boolean(string='Active', default=True) + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Canceled') + ], string='Status', default='new', required=True, copy=False + ) \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..8cfae369abd --- /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..12a237ed336 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Estate Property View + estate.property + list,form + + \ No newline at end of file From 662b639dc144b28e23bf9ed5af4a48c805f1f384 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Tue, 20 Jan 2026 14:39:20 +0100 Subject: [PATCH 05/17] [IMP] estate: add basic views to real estate module (Ch 6) - added the list view with several fields - edited the form view to have two columns and a "Description" tab - added a search view with search fields, filter for available properties, and group properties by postcode. - fixed style: added newline at the end of files and two lines before class definition --- estate/__manifest__.py | 3 +- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 9 +-- estate/security/ir.model.access.csv | 2 +- estate/views/estate_menus.xml | 2 +- estate/views/estate_property_views.xml | 78 +++++++++++++++++++++++++- 6 files changed, 85 insertions(+), 11 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 0f34bd14a8b..e42249af364 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- { 'name': 'Real Estate', 'depends': ['base'], @@ -8,4 +7,4 @@ 'views/estate_menus.xml', ], 'application': True, -} \ No newline at end of file +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3f582b7d177..14bbc0a47d2 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ from odoo import models, fields + class EstateProperty(models.Model): _name = 'estate.property' _description = 'Estate Property Information' @@ -7,11 +8,11 @@ class EstateProperty(models.Model): name = fields.Char(string='Property Name', required=True) description = fields.Text(string='Description') postcode = fields.Char(string='Postcode') - date_availability = fields.Date(string='Available Date', copy=False, default=(fields.Date.add(fields.Date.today(), months=3))) + date_availability = fields.Date(string='Available From', 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='Number of Bedrooms', default=2) - living_area = fields.Integer(string='Living Area') + bedrooms = fields.Integer(string='Bedrooms', default=2) + living_area = fields.Integer(string='Living Area (sqm)') facades = fields.Integer(string='Number of Facades') garage = fields.Boolean(string='Garage') garden = fields.Boolean(string='Garden') @@ -25,4 +26,4 @@ class EstateProperty(models.Model): ('sold', 'Sold'), ('canceled', 'Canceled') ], string='Status', default='new', required=True, copy=False - ) \ No newline at end of file + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 976b61e8cb3..d9d6ba57cc5 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 8cfae369abd..6f3ba4c3510 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 12a237ed336..9b046125091 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,8 +1,82 @@ - Estate Property View + Estate Properties estate.property list,form - \ No newline at end of file + + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + From 46850b4ea6743626f133168ea90b839550ead5b5 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Wed, 21 Jan 2026 10:01:27 +0100 Subject: [PATCH 06/17] [IMP] estate: add property types, tags, and offers Chapter 7 of the Server 101 tutorial on model relationships. - Added a model for property types and added buyers and salesmen to the estate property model to showcase many2one relationships - Added tags to showcase many2many relationships - Added a model for offers to showcase one2many relationships - Created list and form views for the models and added fields to the property model --- estate/__manifest__.py | 3 ++ estate/models/__init__.py | 3 ++ estate/models/estate_property.py | 5 ++++ estate/models/estate_property_offer.py | 11 ++++++++ estate/models/estate_property_tag.py | 9 ++++++ estate/models/estate_property_type.py | 8 ++++++ estate/security/ir.model.access.csv | 3 ++ estate/views/estate_menus.xml | 5 ++++ estate/views/estate_property_offer_views.xml | 29 ++++++++++++++++++++ estate/views/estate_property_tag_views.xml | 21 ++++++++++++++ estate/views/estate_property_type_views.xml | 21 ++++++++++++++ estate/views/estate_property_views.xml | 13 ++++++++- 12 files changed, 130 insertions(+), 1 deletion(-) 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 e42249af364..7cc9aa7eb6a 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,6 +4,9 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ], 'application': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 14bbc0a47d2..ff92c3cabff 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -27,3 +27,8 @@ class EstateProperty(models.Model): ('canceled', 'Canceled') ], string='Status', default='new', required=True, copy=False ) + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) + salesperson_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user) + tag_ids = fields.Many2many('estate.property.tag', string='Tags') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..329c889b910 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,11 @@ +from odoo import models, fields + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer Information' + + price = fields.Float(string='Price') + status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', 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..ebb439abba3 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,9 @@ +from odoo import models, fields + + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'Estate Property Tag Information' + + name = fields.Char(string='Tag Name', required=True) + \ No newline at end of file diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..d6b01da3fe1 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'Estate Property Type Information' + + name = fields.Char(string='Property Type', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index d9d6ba57cc5..49bca99cac8 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 6f3ba4c3510..acd4195c3ad 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -4,5 +4,10 @@ + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..bf48704e3e6 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,29 @@ + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + +
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..eed037d5582 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,21 @@ + + + Estate Property Tags + estate.property.tag + list,form + + + + estate.property.tag.form + estate.property.tag + +
+ +

+ +

+
+
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..20db6092826 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,21 @@ + + + Estate Property Types + estate.property.type + list,form + + + + estate.property.type.form + estate.property.type + +
+ +

+ +

+
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9b046125091..8bed7429013 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,4 +1,3 @@ - Estate Properties @@ -18,6 +17,7 @@ + @@ -31,8 +31,10 @@

+ + @@ -55,6 +57,15 @@ + + + + + + + + + From 95d3d2a5ba7f7e7926c186fe6a2108df3c832375 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Wed, 21 Jan 2026 14:15:36 +0100 Subject: [PATCH 07/17] [IMP] estate: add computed fields and onchanges - added computed fields to calculate total area and best offer price - added a compute function to calculate the deadline from the number of validity days - added an inverse function to determine the validity in days from the deadline - added onchange function to adjust the garden area and orientation based on whether the garden option is set --- estate/__init__.py | 2 +- estate/__manifest__.py | 2 ++ estate/models/estate_property.py | 24 +++++++++++++++++++- estate/models/estate_property_offer.py | 18 ++++++++++++++- estate/models/estate_property_tag.py | 1 - estate/views/estate_property_offer_views.xml | 6 +++++ estate/views/estate_property_views.xml | 2 ++ 7 files changed, 51 insertions(+), 4 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/__manifest__.py b/estate/__manifest__.py index 7cc9aa7eb6a..156980a9aa6 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -10,4 +10,6 @@ 'views/estate_menus.xml', ], 'application': True, + 'author': 'Dilya Anvarbekova', + 'license': 'LGPL-3', } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index ff92c3cabff..e340f67d59d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import models, fields, api class EstateProperty(models.Model): @@ -32,3 +32,25 @@ class EstateProperty(models.Model): salesperson_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user) tag_ids = fields.Many2many('estate.property.tag', string='Tags') offer_ids = fields.One2many('estate.property.offer', 'property_id', 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("offer_ids.price") + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.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 = False diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 329c889b910..16c08876705 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import models, fields +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models class EstatePropertyOffer(models.Model): @@ -9,3 +11,17 @@ class EstatePropertyOffer(models.Model): status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', copy=False) partner_id = fields.Many2one('res.partner', string='Partner', required=True) property_id = fields.Many2one('estate.property', string='Property', required=True) + + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date(string="Deadline", compute="_compute_deadline", inverse="_inverse_deadline") + + @api.depends('validity') + def _compute_deadline(self): + for record in self: + start_date = record.create_date.date() or fields.Date.today() + record.date_deadline = fields.Date.add(start_date, days=record.validity) + + def _inverse_deadline(self): + for record in self: + start_date = record.create_date.date() or fields.Date.today() + record.validity = (record.date_deadline - start_date).days diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index ebb439abba3..e2630019f9b 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -6,4 +6,3 @@ class EstatePropertyTag(models.Model): _description = 'Estate Property Tag Information' name = fields.Char(string='Tag Name', required=True) - \ 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 bf48704e3e6..23534bf52c0 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -7,6 +7,8 @@ + + @@ -22,6 +24,10 @@ + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 8bed7429013..b7939ac2ede 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -41,6 +41,7 @@ + @@ -54,6 +55,7 @@ + From a21ad84dc0a24e349dea2244fefa3831ad942388 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Wed, 21 Jan 2026 15:17:01 +0100 Subject: [PATCH 08/17] [IMP] estate: add buttons to accept and reject offers - added action buttons to the property form - added action buttons to the offer form, the price and buyer are updated automatically - validity checks to make sure multiple offers are not accepted, cancelled properties are not sold, and sold properties are not cancelled --- estate/models/estate_property.py | 22 ++++++++++++++++++-- estate/models/estate_property_offer.py | 22 ++++++++++++++++---- estate/views/estate_property_offer_views.xml | 10 +++++---- estate/views/estate_property_views.xml | 6 +++++- 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index e340f67d59d..a8f01dcc0d6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import models, fields, api +from odoo.exceptions import UserError class EstateProperty(models.Model): @@ -24,7 +25,7 @@ class EstateProperty(models.Model): ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), - ('canceled', 'Canceled') + ('cancelled', 'Cancelled') ], string='Status', default='new', required=True, copy=False ) property_type_id = fields.Many2one('estate.property.type', string='Property Type') @@ -44,7 +45,10 @@ def _compute_total_area(self): @api.depends("offer_ids.price") def _compute_best_price(self): for record in self: - record.best_price = max(record.offer_ids.mapped('price')) + if record.offer_ids: + record.best_price = max(record.offer_ids.mapped('price')) + else: + record.best_price = 0.0 @api.onchange('garden') def _onchange_garden(self): @@ -54,3 +58,17 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = False + + def action_cancel_property(self): + for record in self: + if record.state == 'sold': + raise UserError("Sold properties cannot be cancelled.") + else: + record.state = 'cancelled' + + def action_sell_property(self): + for record in self: + if record.state == 'cancelled': + raise UserError("Cancelled properties cannot be sold.") + else: + record.state = 'sold' diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 16c08876705..860f5386719 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,5 @@ -from dateutil.relativedelta import relativedelta - from odoo import api, fields, models +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): @@ -18,10 +17,25 @@ class EstatePropertyOffer(models.Model): @api.depends('validity') def _compute_deadline(self): for record in self: - start_date = record.create_date.date() or fields.Date.today() + start_date = record.create_date.date() if record.create_date else fields.Date.today() record.date_deadline = fields.Date.add(start_date, days=record.validity) def _inverse_deadline(self): for record in self: - start_date = record.create_date.date() or fields.Date.today() + start_date = record.create_date.date() if record.create_date else fields.Date.today() record.validity = (record.date_deadline - start_date).days + + def action_accept_offer(self): + for record in self: + if "accepted" in record.property_id.offer_ids.mapped('status'): + raise UserError("An offer has already been accepted for this property.") + record.status = 'accepted' + record.property_id.selling_price = record.price + record.property_id.state = 'offer_accepted' + record.property_id.buyer_id = record.partner_id + + def action_refuse_offer(self): + for record in self: + if record.status == 'accepted': + raise UserError("You cannot refuse an accepted offer.") + record.status = 'refused' diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 23534bf52c0..ae263520402 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -4,11 +4,13 @@ estate.property.offer - - + + + + + +

+ + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1e871df3b6c..9b1b2174a04 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,22 +1,19 @@ - - Estate Properties - estate.property - list,form - - estate.property.list estate.property - + - + @@ -28,18 +25,19 @@
-

- + - - + @@ -58,13 +56,13 @@ - - + + - + @@ -86,7 +84,7 @@ - + @@ -96,4 +94,11 @@ + + + Estate Properties + estate.property + list,form + {'search_default_available': True} + From a7a6b8479a62d53d443e2c952b4f418d7be92166 Mon Sep 17 00:00:00 2001 From: dianv-odoo Date: Thu, 22 Jan 2026 11:58:11 +0100 Subject: [PATCH 11/17] [FIX] applied suggested style fixes - added style fixes to pass the style check Co-authored-by: Arthur Nanson --- estate/models/estate_property.py | 19 ++++++++++++------- estate/models/estate_property_offer.py | 1 - estate/models/estate_property_type.py | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 8c18a5d44a4..e05521749ad 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -22,13 +22,18 @@ class EstateProperty(models.Model): garden_area = fields.Integer(string='Garden Area') garden_orientation = fields.Selection([('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], string='Garden Orientation') active = fields.Boolean(string='Active', default=True) - state = fields.Selection([ - ('new', 'New'), - ('offer_received', 'Offer Received'), - ('offer_accepted', 'Offer Accepted'), - ('sold', 'Sold'), - ('cancelled', 'Cancelled') - ], string='Status', default='new', required=True, copy=False + state = fields.Selection( + [ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], + string='Status', + default='new', + required=True, + copy=False ) property_type_id = fields.Many2one('estate.property.type', string='Property Type') buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 7bc267a9ccd..6c533de496f 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -12,7 +12,6 @@ class EstatePropertyOffer(models.Model): partner_id = fields.Many2one('res.partner', string='Partner', required=True) property_id = fields.Many2one('estate.property', string='Property', required=True) property_type_id = fields.Many2one('estate.property.type', related='property_id.property_type_id', string='Property Type', store=True) - validity = fields.Integer(string="Validity (days)", default=7) date_deadline = fields.Date(string="Deadline", compute="_compute_deadline", inverse="_inverse_deadline") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 9e17a0695a4..618d9fd427e 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models, _ +from odoo import api, fields, models class EstatePropertyType(models.Model): From 17569cc56003dc022fbf243ed599c5ae03b6813e Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Thu, 22 Jan 2026 14:40:26 +0100 Subject: [PATCH 12/17] [IMP] add business logic to property state and inheritance to users - added a function to check offer price before adding a new one and to change the property status to "offer received" - added a function to check the property status before deleting to only allow deleting new or cancelled properties - used inheritance to add a page to the users (salesperson) model to include the properties they sold --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 6 ++++++ estate/models/estate_property_offer.py | 10 ++++++++++ estate/models/res_users.py | 12 ++++++++++++ estate/views/res_users_views.xml | 14 ++++++++++++++ 6 files changed, 44 insertions(+) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index bf8ee94deab..494adf6c64b 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,6 +7,7 @@ 'views/estate_property_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', + 'views/res_users_views.xml', 'views/estate_menus.xml', ], 'application': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index e05521749ad..c72040c84b0 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -82,6 +82,12 @@ def _check_selling_price_expected_price(self): and float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=2) < 0: raise ValidationError("The selling price must be at least 90% of the expected price.") + @api.ondelete(at_uninstall=False) + def _check_state_on_delete(self): + for record in self: + if record.state not in ["new", "cancelled"]: + raise UserError("Cannot delete a property that is not new or cancelled.") + def action_cancel_property(self): for record in self: if record.state == 'sold': diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 6c533de496f..0994f8c31e5 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -20,6 +20,16 @@ class EstatePropertyOffer(models.Model): 'The offer price must be strictly positive.' ) + @api.model + def create(self, vals_list): + for vals in vals_list: + prop = self.env['estate.property'].browse(vals.get('property_id')) + if prop.offer_ids: + if vals.get('price') < min(prop.offer_ids.mapped('price')): + raise UserError("The new offer price cannot be lower than existing offers.") + prop.state = 'offer_received' + return super().create(vals_list) + @api.depends('validity') def _compute_deadline(self): for record in self: diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..2fb87fee461 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'estate.property', + 'salesperson_id', + string='Properties', + domain=[('state', 'in', ['new', 'offer_received'])] + ) diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..4db7a533946 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,14 @@ + + + res.users.form.inherit.estate + res.users + + + + + + + + + + From 8710e7116ca61d893bb5e9b03c69196c5c212c99 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Thu, 22 Jan 2026 16:51:35 +0100 Subject: [PATCH 13/17] [FIX] estate: fix style issues from last commit - fixed whitespace on empty line - changed label name for res.user properties --- estate/models/estate_property_offer.py | 2 +- estate/models/res_users.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 0994f8c31e5..ba28d15d169 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -29,7 +29,7 @@ def create(self, vals_list): raise UserError("The new offer price cannot be lower than existing offers.") prop.state = 'offer_received' return super().create(vals_list) - + @api.depends('validity') def _compute_deadline(self): for record in self: diff --git a/estate/models/res_users.py b/estate/models/res_users.py index 2fb87fee461..a5a2c4dcf77 100644 --- a/estate/models/res_users.py +++ b/estate/models/res_users.py @@ -7,6 +7,6 @@ class ResUsers(models.Model): property_ids = fields.One2many( 'estate.property', 'salesperson_id', - string='Properties', + string='Assigned Properties', domain=[('state', 'in', ['new', 'offer_received'])] ) From b5e704cabaf3b23586a670e1e5b72b04102cf25b Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Thu, 22 Jan 2026 17:32:06 +0100 Subject: [PATCH 14/17] [ADD] estate_account: add link module estate_account - crated a link module named estate_account - created a function to create an invoice when a property is sold --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 14 +++++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 26 +++++++++++++++++++++ estate_account/security/ir.model.access.csv | 2 ++ 5 files changed, 44 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 create mode 100644 estate_account/security/ir.model.access.csv 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..c908eeb22ba --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Real Estate Accounting', + 'depends': [ + 'base', + 'estate', + 'account', + ], + 'data': [ + 'security/ir.model.access.csv', + ], + 'application': True, + 'author': 'Dilya Anvarbekova', + 'license': 'LGPL-3', +} 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..3a400e163c6 --- /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_sell_property(self): + res = super().action_sell_property() + for record in self: + self.env['account.move'].create({ + 'partner_id': record.buyer_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': [ + Command.create({ + 'name': "Commission (6%)", + 'quantity': 1.0, + 'price_unit': record.selling_price * 0.06, + }), + Command.create({ + 'name': "Administrative Fees", + 'quantity': 1.0, + 'price_unit': 100.00, + }) + ] + }) + return res diff --git a/estate_account/security/ir.model.access.csv b/estate_account/security/ir.model.access.csv new file mode 100644 index 00000000000..d9d6ba57cc5 --- /dev/null +++ b/estate_account/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 5a7fec3a143e6cc0b0722be1311ea0f1b9203ee6 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Fri, 23 Jan 2026 09:25:33 +0100 Subject: [PATCH 15/17] [IMP] estate: added kanban view option - created kanban view - edited the card to conditionally show best price and selling price based on the status o fthe property - created groups to sort the properties automatically by type of property - made the groups and properties not draggable --- estate/__manifest__.py | 2 +- estate/views/estate_property_views.xml | 33 +++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 494adf6c64b..be25d5673e9 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,11 +4,11 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_offer_views.xml', - 'views/estate_property_views.xml', 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', 'views/res_users_views.xml', 'views/estate_menus.xml', + 'views/estate_property_views.xml', ], 'application': True, 'author': 'Dilya Anvarbekova', diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 9b1b2174a04..1e818de003a 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,4 +1,35 @@ + + estate.property.kanban + estate.property + + + + + +
+ + + +
+ Expected Price: +
+
+ Best Offer: +
+
+ Selling Price: +
+
+ +
+
+
+
+
+
+
+ estate.property.list estate.property @@ -98,7 +129,7 @@ Estate Properties estate.property - list,form + kanban,list,form {'search_default_available': True}
From 5cead400b82eda7b9db9fbdc7f4db7e2d3fed9f5 Mon Sep 17 00:00:00 2001 From: Dilya Anvarbekova Date: Fri, 23 Jan 2026 09:47:07 +0100 Subject: [PATCH 16/17] [FIX] fix order of files in the manifest to load in correct order - menus were loading before the views, moved the views before the menus to remove errors --- estate/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index be25d5673e9..4a65ffd401c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,8 +7,8 @@ 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', 'views/res_users_views.xml', - 'views/estate_menus.xml', 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'application': True, 'author': 'Dilya Anvarbekova', From 32eb7a1f34302f0ea1f78aa843e1aca965e4c095 Mon Sep 17 00:00:00 2001 From: dianv-odoo Date: Fri, 23 Jan 2026 11:42:18 +0100 Subject: [PATCH 17/17] [FIX] estate: change the offer price requirement to be higher than all previous offers Co-authored-by: Arthur Nanson --- estate/models/estate_property_offer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index ba28d15d169..07a2db11e62 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -25,7 +25,7 @@ def create(self, vals_list): for vals in vals_list: prop = self.env['estate.property'].browse(vals.get('property_id')) if prop.offer_ids: - if vals.get('price') < min(prop.offer_ids.mapped('price')): + if vals.get('price') <= max(prop.offer_ids.mapped('price')): raise UserError("The new offer price cannot be lower than existing offers.") prop.state = 'offer_received' return super().create(vals_list)