From db2fc411d695d522279da8cbe4f86adbf532e693 Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Mon, 20 Jul 2026 15:00:12 +0200 Subject: [PATCH 01/13] [ADD] estate: add initial module structure --- estate/__init__.py | 0 estate/__manifest__.py | 10 ++++++++++ 2 files changed, 10 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..247c4776e88 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,10 @@ +{ + 'name': 'Real Estate', + 'version': '1.0', + 'category': 'Real Estate/Brokerage', + 'summary': 'An estate module for training', + 'depends': ['base'], + 'data': [], + 'installable': True, + 'application': True, +} \ No newline at end of file From 7440ded47e42e60c1a0033a7ebedcdfb39025bc9 Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Mon, 20 Jul 2026 15:43:44 +0200 Subject: [PATCH 02/13] [IMP] estate: add estate.property model --- estate/__init__.py | 1 + estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 27 +++++++++++++++++++++++++++ 4 files changed, 30 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/__manifest__.py b/estate/__manifest__.py index 247c4776e88..4862b7f3336 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,4 +7,5 @@ 'data': [], 'installable': True, 'application': True, + 'license': 'LGPL-3', } \ 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..075acff40e5 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,27 @@ +from odoo import fields, models + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], + ) \ No newline at end of file From e26b1c824aa970028723098c391ed51007fe427e Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Mon, 20 Jul 2026 16:17:54 +0200 Subject: [PATCH 03/13] [IMP] estate: add access rights --- estate/__manifest__.py | 5 ++++- estate/security/ir.model.access.csv | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 estate/security/ir.model.access.csv diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 4862b7f3336..c6ef59206f7 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,10 +1,13 @@ { 'name': 'Real Estate', 'version': '1.0', + 'author': 'tomic', 'category': 'Real Estate/Brokerage', 'summary': 'An estate module for training', 'depends': ['base'], - 'data': [], + 'data': [ + 'security/ir.model.access.csv', + ], 'installable': True, 'application': True, 'license': 'LGPL-3', diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..32389642d4f --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 From 883ed90d27ed5ff44c21442358a84220cd0e0c1e Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Wed, 22 Jul 2026 09:55:22 +0200 Subject: [PATCH 04/13] [IMP] estate: add menus and field attributes --- estate/__manifest__.py | 2 ++ estate/models/estate_property.py | 33 ++++++++++++++++++++------ estate/views/estate_menus.xml | 6 +++++ estate/views/estate_property_views.xml | 9 +++++++ 4 files changed, 43 insertions(+), 7 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 c6ef59206f7..3e24400e795 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,6 +7,8 @@ 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ], 'installable': True, 'application': True, diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 075acff40e5..90e61e8d3c6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,3 +1,4 @@ +from dateutil.relativedelta import relativedelta from odoo import fields, models @@ -5,23 +6,41 @@ class EstateProperty(models.Model): _name = "estate.property" _description = "Real Estate Property" - name = fields.Char(required=True) + name = fields.Char(string="Title", required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date( + string="Available From", + copy=False, + default=fields.Date.add(fields.Date.today(), months=3) + ) expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() - living_area = fields.Integer() + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer(string="Living Area (sqm)") facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer() + garden_area = fields.Integer(string="Garden Area (sqm)") garden_orientation = fields.Selection( selection=[ ('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West'), + ] + ) + state = fields.Selection( + string="Status", + selection=[ + ('new','New'), + ('offer_received','Offer Received'), + ('offer_accepted','Offer Accepted'), + ('sold','Sold'), + ('cancelled','Cancelled'), ], - ) \ No newline at end of file + required=True, + copy=False, + default='new' + ) + active = fields.Boolean(default=True) \ 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..406a5691612 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,6 @@ + + + + + + \ 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..da6f033beed --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,9 @@ + + + + + Properties + estate.property + list,form + + \ No newline at end of file From ca2c41fe908bac9209a9a27b63339b26bbb80f42 Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Wed, 22 Jul 2026 11:11:14 +0200 Subject: [PATCH 05/13] [IMP] estate: add basic views --- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 15 ++--- estate/views/estate_menus.xml | 2 +- estate/views/estate_property_views.xml | 88 +++++++++++++++++++++++++- 4 files changed, 96 insertions(+), 11 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 3e24400e795..48b7c7fcf51 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -13,4 +13,4 @@ 'installable': True, 'application': True, 'license': 'LGPL-3', -} \ No newline at end of file +} diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 90e61e8d3c6..14205cfca3d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,3 @@ -from dateutil.relativedelta import relativedelta from odoo import fields, models @@ -11,7 +10,7 @@ class EstateProperty(models.Model): postcode = fields.Char() date_availability = fields.Date( string="Available From", - copy=False, + copy=False, default=fields.Date.add(fields.Date.today(), months=3) ) expected_price = fields.Float(required=True) @@ -33,14 +32,14 @@ class EstateProperty(models.Model): state = fields.Selection( string="Status", selection=[ - ('new','New'), - ('offer_received','Offer Received'), - ('offer_accepted','Offer Accepted'), - ('sold','Sold'), - ('cancelled','Cancelled'), + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), ], required=True, copy=False, default='new' ) - active = fields.Boolean(default=True) \ No newline at end of file + active = fields.Boolean(default=True) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 406a5691612..169d189e7e0 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index da6f033beed..c91be2ab1b2 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -6,4 +6,90 @@ 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 866051977cebdc8f89cfe5bd3230fa3232e144b8 Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Wed, 22 Jul 2026 13:47:15 +0200 Subject: [PATCH 06/13] [IMP] estate: add property types, tags and offers relations --- estate/__init__.py | 2 +- estate/__manifest__.py | 2 ++ estate/models/__init__.py | 5 ++++- estate/models/estate_property.py | 10 +++++++++ estate/models/estate_property_offer.py | 18 +++++++++++++++ estate/models/estate_property_tag.py | 8 +++++++ estate/models/estate_property_type.py | 8 +++++++ estate/security/ir.model.access.csv | 3 +++ estate/views/estate_menus.xml | 25 ++++++++++++++++++--- estate/views/estate_property_tag_views.xml | 10 +++++++++ estate/views/estate_property_type_views.xml | 10 +++++++++ estate/views/estate_property_views.xml | 23 +++++++++++++++++++ 12 files changed, 119 insertions(+), 5 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_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 48b7c7fcf51..6cf82657d10 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,6 +8,8 @@ '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_menus.xml', ], 'installable': True, diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..2f1821a39c1 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,4 @@ -from . import estate_property \ No newline at end of file +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 14205cfca3d..cc112e7edea 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -43,3 +43,13 @@ class EstateProperty(models.Model): default='new' ) active = fields.Boolean(default=True) + + 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="Salesman", 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..bb47b0aaa6a --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,18 @@ +from odoo import fields, models + + +class PropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real Estate Property Offer" + + price = fields.Float(string="Price") + status = fields.Selection( + 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..202d7df4006 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real Estate Property Tags" + + name = fields.Char(required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..0e2f3e1ef95 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class PropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + + name = fields.Char(string="Name", required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 32389642d4f..89f97c50842 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 169d189e7e0..bb5dfc2fe5e 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,6 +1,25 @@ - - - + + + + + + + + + + + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..2607e205b82 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,10 @@ + + + + + Property Tags + estate.property.tag + list,form + + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..5a43ea62b0d --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,10 @@ + + + + + Property Types + estate.property.type + list,form + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index c91be2ab1b2..1a588490781 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -34,8 +34,10 @@

+ + @@ -52,7 +54,9 @@ + + @@ -65,6 +69,24 @@ + + + + + + + + + + + + + + + + + + @@ -92,4 +114,5 @@ + From 377c3488248500e6e8b6980bee0dc8a0e0978360 Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Wed, 22 Jul 2026 15:17:10 +0200 Subject: [PATCH 07/13] [IMP] estate: add computed fields and onchanges --- estate/models/estate_property.py | 28 +++++++++++++++++++++++++- estate/models/estate_property_offer.py | 23 ++++++++++++++++++++- estate/views/estate_property_views.xml | 4 ++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index cc112e7edea..3d3974494c5 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): @@ -53,3 +53,29 @@ class EstateProperty(models.Model): tag_ids = fields.Many2many("estate.property.tag", string="Tags") offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + + total_area = fields.Integer( + string="Total Area (sqm)", compute="_compute_total_area" + ) + best_price = fields.Float( + string="Best Offer", 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 or 0) + (record.garden_area or 0) + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped("price"), default=0.0) + + @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 bb47b0aaa6a..b89dc90dc1f 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from datetime import timedelta + +from odoo import api, fields, models class PropertyOffer(models.Model): @@ -16,3 +18,22 @@ class PropertyOffer(models.Model): ) 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_date_deadline", + inverse="_inverse_date_deadline", + ) + + @api.depends("validity") + def _compute_date_deadline(self): + for record in self: + date = record.create_date.date() if record.create_date else fields.Date.today() + record.date_deadline = date + timedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + date = record.create_date.date() if record.create_date else fields.Date.today() + if record.date_deadline: + record.validity = (record.date_deadline - date).days diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1a588490781..2db82726fb3 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -43,6 +43,7 @@ + @@ -67,6 +68,7 @@ + @@ -75,6 +77,8 @@ + + From 6dc496b227159d0541447ebf9895420a54e19121 Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Wed, 22 Jul 2026 17:22:26 +0200 Subject: [PATCH 08/13] [IMP] estate: add buttons, action methods and user error management --- estate/models/estate_property.py | 15 +++++++++++++++ estate/models/estate_property_offer.py | 25 +++++++++++++++++++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 3 files changed, 48 insertions(+) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3d3974494c5..288e8e3015e 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class EstateProperty(models.Model): @@ -79,3 +80,17 @@ def _onchange_garden(self): else: self.garden_area = 0 self.garden_orientation = False + + def action_cancel(self): + for record in self: + if record.state == "sold": + raise UserError("A sold property cannot be cancelled.") + record.state = "cancelled" + return True + + def action_sold(self): + for record in self: + if record.state == "cancelled": + raise UserError("A cancelled property cannot be sold.") + record.state = "sold" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index b89dc90dc1f..a59bc1f1ca8 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,7 @@ from datetime import timedelta from odoo import api, fields, models +from odoo.exceptions import UserError class PropertyOffer(models.Model): @@ -37,3 +38,27 @@ def _inverse_date_deadline(self): date = record.create_date.date() if record.create_date else fields.Date.today() if record.date_deadline: record.validity = (record.date_deadline - date).days + + @api.model_create_multi + def create(self, vals_list): + offers = super().create(vals_list) + for offer in offers: + offer.property_id.state = "offer_received" + return offers + + def action_accept(self): + for record in self: + if record.property_id.buyer_id: + raise UserError("An offer has already been accepted for this property.") + record.status = "accepted" + record.property_id.buyer_id = record.partner_id + record.property_id.selling_price = record.price + record.property_id.state = "offer_accepted" + return True + + def action_refuse(self): + for record in self: + if record.status == "accepted": + raise UserError("An accepted offer cannot be refused.") + record.status = "refused" + return True diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 2db82726fb3..d4d100f595f 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -30,6 +30,12 @@ estate.property
+
+
+

@@ -79,6 +85,8 @@ + + + +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 144ba869959..be7f02b4e5e 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -5,6 +5,7 @@ Properties estate.property list,form + {'search_default_available': True} @@ -12,14 +13,15 @@ estate.property.list estate.property - + - + @@ -31,8 +33,8 @@
-
@@ -40,10 +42,10 @@

- + - + @@ -54,9 +56,6 @@ - - - @@ -72,15 +71,16 @@ - - + + - - + + @@ -115,9 +115,10 @@ - + + From a8fde859bf356a2e731fca47b8616c5bf878e307 Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Thu, 23 Jul 2026 16:12:17 +0200 Subject: [PATCH 11/13] [IMP] estate: add CRUD overrides and inheritance --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 32 ++++++++++++++++++++++---- estate/models/estate_property_offer.py | 17 ++++++-------- estate/models/res_users.py | 12 ++++++++++ estate/views/res_users_views.xml | 17 ++++++++++++++ 6 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 estate/models/res_users.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index be028cab60f..4d3afba0c51 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,6 +12,7 @@ 'views/estate_property_type_views.xml', 'views/estate_property_tag_views.xml', 'views/estate_menus.xml', + 'views/res_users_views.xml', ], 'installable': True, '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 3885dca3847..cda6874f34a 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ from odoo import api, fields, models -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import UserError from odoo.tools import float_compare, float_is_zero @@ -103,19 +103,43 @@ def _check_selling_price_ratio(self): for record in self: if not float_is_zero(record.selling_price, precision_rounding=0.01): if float_compare(record.selling_price, record.expected_price * 0.9, precision_rounding=0.01) < 0: - raise ValidationError("The selling price cannot be lower than 90% of the expected price!") + raise UserError(self.env._("The selling price cannot be lower than 90% of the expected price!")) + + # Helper Methods + def _check_new_offer_price(self, price): + self.ensure_one() + for offer in self.offer_ids: + if price <= offer.price: + raise UserError(self.env._("The offer amount must be strictly higher than existing offers.")) + + def _accept_offer(self, buyer, price): + self.ensure_one() + if self.buyer_id: + raise UserError(self.env._("An offer has already been accepted for this property.")) + self.write({ + "buyer_id": buyer.id, + "selling_price": price, + "state": "offer_accepted", + }) + + # CRUD Methods + @api.ondelete(at_uninstall=False) + def _unlink_except_new_or_cancelled(self): + for record in self: + if record.state not in ("new", "cancelled"): + raise UserError(self.env._("Only new and cancelled properties can be deleted.")) # Action Methods def action_cancel(self): for record in self: if record.state == "sold": - raise UserError("A sold property cannot be cancelled.") + raise UserError(self.env._("A sold property cannot be cancelled.")) record.state = "cancelled" return True def action_sold(self): for record in self: if record.state == "cancelled": - raise UserError("A cancelled property cannot be sold.") + raise UserError(self.env._("A cancelled property cannot be sold.")) record.state = "sold" return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 6fb5ce16ef9..70119e274b2 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -61,25 +61,22 @@ def _inverse_date_deadline(self): # CRUD Methods @api.model_create_multi def create(self, vals_list): - offers = super().create(vals_list) - for offer in offers: - offer.property_id.state = "offer_received" - return offers + for vals in vals_list: + property_id = self.env["estate.property"].browse(vals["property_id"]) + property_id._check_new_offer_price(vals["price"]) + property_id.state = "offer_received" + return super().create(vals_list) # Action Methods def action_accept(self): for record in self: - if record.property_id.buyer_id: - raise UserError("An offer has already been accepted for this property.") + record.property_id._accept_offer(record.partner_id, record.price) record.status = "accepted" - record.property_id.buyer_id = record.partner_id - record.property_id.selling_price = record.price - record.property_id.state = "offer_accepted" return True def action_refuse(self): for record in self: if record.status == "accepted": - raise UserError("An accepted offer cannot be refused.") + raise UserError(self.env._("An accepted offer cannot be refused.")) record.status = "refused" return True diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..5f18526d060 --- /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="Real Estate 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..b90b1642457 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,17 @@ + + + + + res.users.form.inherit.estate + res.users + + + + + + + + + + + From fa0eab7b9556a8ec436f111b943f2805735dd29b Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Thu, 23 Jul 2026 17:12:48 +0200 Subject: [PATCH 12/13] [IMP] estate: add link module to generate invoices --- estate/models/estate_property.py | 26 ++++++++-------- estate/models/estate_property_offer.py | 21 ++++++++----- estate/models/estate_property_type.py | 1 + estate/views/estate_property_offer_views.xml | 1 - estate/views/estate_property_views.xml | 5 ---- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 10 +++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 31 ++++++++++++++++++++ 9 files changed, 70 insertions(+), 27 deletions(-) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index cda6874f34a..63966f0aeb6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -46,6 +46,7 @@ class EstateProperty(models.Model): required=True, copy=False, default="new", + index=True, ) active = fields.Boolean(default=True) @@ -63,7 +64,7 @@ class EstateProperty(models.Model): string="Total Area (sqm)", compute="_compute_total_area" ) best_price = fields.Float( - string="Best Offer", compute="_compute_best_price" + string="Best Offer", compute="_compute_best_price", store=True ) # SQL Constraints @@ -85,7 +86,7 @@ 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"), default=0.0) + record.best_price = record.offer_ids[0].price if record.offer_ids else 0.0 # Onchange Methods @api.onchange("garden") @@ -108,9 +109,8 @@ def _check_selling_price_ratio(self): # Helper Methods def _check_new_offer_price(self, price): self.ensure_one() - for offer in self.offer_ids: - if price <= offer.price: - raise UserError(self.env._("The offer amount must be strictly higher than existing offers.")) + if self.offer_ids and price <= self.offer_ids[0].price: + raise UserError(self.env._("The offer amount must be strictly higher than existing offers.")) def _accept_offer(self, buyer, price): self.ensure_one() @@ -131,15 +131,15 @@ def _unlink_except_new_or_cancelled(self): # Action Methods def action_cancel(self): - for record in self: - if record.state == "sold": - raise UserError(self.env._("A sold property cannot be cancelled.")) - record.state = "cancelled" + if self.filtered(lambda record: record.state == "sold"): + raise UserError(self.env._("A sold property cannot be cancelled.")) + self.write({"state": "cancelled"}) return True def action_sold(self): - for record in self: - if record.state == "cancelled": - raise UserError(self.env._("A cancelled property cannot be sold.")) - record.state = "sold" + if self.filtered(lambda record: record.state == "cancelled"): + raise UserError(self.env._("A cancelled property cannot be sold.")) + if self.filtered(lambda prop: prop.state != "offer_accepted"): + raise UserError(self.env._("A property can only be sold if an offer has been accepted.")) + self.write({"state": "sold"}) return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 70119e274b2..9f3571a11c4 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -61,22 +61,27 @@ def _inverse_date_deadline(self): # CRUD Methods @api.model_create_multi def create(self, vals_list): + property_ids = list({vals["property_id"] for vals in vals_list}) + properties = self.env["estate.property"].browse(property_ids) for vals in vals_list: - property_id = self.env["estate.property"].browse(vals["property_id"]) - property_id._check_new_offer_price(vals["price"]) - property_id.state = "offer_received" + property_rec = properties.filtered(lambda property: property.id == vals["property_id"]) + property_rec._check_new_offer_price(vals["price"]) + properties.write({"state": "offer_received"}) return super().create(vals_list) # Action Methods def action_accept(self): + if self.filtered(lambda offer: offer.status in ('accepted', 'refused')): + raise UserError(self.env._("You can only accept pending offers.")) for record in self: record.property_id._accept_offer(record.partner_id, record.price) - record.status = "accepted" + other_offers = record.property_id.offer_ids - record + other_offers.write({"status": "refused"}) + self.write({"status": "accepted"}) return True def action_refuse(self): - for record in self: - if record.status == "accepted": - raise UserError(self.env._("An accepted offer cannot be refused.")) - record.status = "refused" + if self.filtered(lambda offer: offer.status == "accepted"): + raise UserError(self.env._("An accepted offer cannot be refused.")) + self.write({"status": "refused"}) return True diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 90f6988da61..031d66614d5 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -25,6 +25,7 @@ class PropertyType(models.Model): ) # Compute Methods + # TODO: FIND A WAY TO DO A SINGLE COUNT QUERY !!!!!!!!!!!!!! @api.depends("offer_ids") def _compute_offer_count(self): for record in self: diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 301846f86e5..9f6817de8d1 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -6,7 +6,6 @@ Property Offers estate.property.offer list,form - [('property_type_id', '=', active_id)] diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index be7f02b4e5e..3e264d7fc29 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -55,11 +55,6 @@ - - - - - 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..53f0f2a8575 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,10 @@ +{ + "name": "Estate Account", + "version": "1.0", + 'author': 'tomic', + "depends": ["estate", "account"], + "data": [], + "installable": True, + "application": False, + '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..768cee23352 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,31 @@ +from odoo import Command, models + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold(self): + res = super().action_sold() + invoices_vals = [] + + for record in self: + invoices_vals.append({ + "partner_id": record.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create({ + "name": f"Commission (6%) - {record.name}", + "quantity": 1, + "price_unit": 0.06 * record.selling_price, + }), + Command.create({ + "name": "Administrative fees", + "quantity": 1, + "price_unit": 100.00, + }), + ], + }) + + if invoices_vals: + self.env["account.move"].with_context(default_move_type="out_invoice").create(invoices_vals) + return res From 19b4dc8f29874fbaf05c06fd1ff240f7f9f8a441 Mon Sep 17 00:00:00 2001 From: "Tony Michel (tomic)" Date: Fri, 24 Jul 2026 13:38:35 +0200 Subject: [PATCH 13/13] [IMP] estate: add kanban view --- estate/__manifest__.py | 2 +- estate/models/estate_property.py | 96 ++++++++++---------- estate/models/estate_property_offer.py | 42 ++++----- estate/models/estate_property_tag.py | 6 +- estate/models/estate_property_type.py | 17 ++-- estate/views/estate_menus.xml | 9 +- estate/views/estate_property_offer_views.xml | 6 +- estate/views/estate_property_tag_views.xml | 2 - estate/views/estate_property_type_views.xml | 9 +- estate/views/estate_property_views.xml | 47 +++++++--- estate/views/res_users_views.xml | 2 - estate_account/__manifest__.py | 13 ++- estate_account/models/estate_property.py | 22 ++--- 13 files changed, 144 insertions(+), 129 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 4d3afba0c51..92119192477 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Real Estate', 'version': '1.0', - 'author': 'tomic', + 'author': 'Odoo S.A.', 'category': 'Real Estate/Brokerage', 'summary': 'An estate module for training', 'depends': ['base'], diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 63966f0aeb6..d307b22bbed 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -5,9 +5,9 @@ class EstateProperty(models.Model): # Attributes - _name = "estate.property" + _name = 'estate.property' _description = "Real Estate Property" - _order = "id desc" + _order = 'id desc' # Fields name = fields.Char(string="Title", required=True) @@ -21,85 +21,82 @@ class EstateProperty(models.Model): expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) - living_area = fields.Integer(string="Living Area (sqm)") + living_area = fields.Integer(string="Living Area (sqm)", default=0) facades = fields.Integer() garage = fields.Boolean() garden = fields.Boolean() - garden_area = fields.Integer(string="Garden Area (sqm)") + garden_area = fields.Integer(string="Garden Area (sqm)", default=0) garden_orientation = fields.Selection( selection=[ - ("north", "North"), - ("south", "South"), - ("east", "East"), - ("west", "West"), - ] + ('north', "North"), + ('south', "South"), + ('east', "East"), + ('west', "West"), + ], ) state = fields.Selection( string="Status", selection=[ - ("new", "New"), - ("offer_received", "Offer Received"), - ("offer_accepted", "Offer Accepted"), - ("sold", "Sold"), - ("cancelled", "Cancelled"), + ('new', "New"), + ('offer_received', "Offer Received"), + ('offer_accepted', "Offer Accepted"), + ('sold', "Sold"), + ('cancelled', "Cancelled"), ], required=True, copy=False, - default="new", + default='new', index=True, ) active = fields.Boolean(default=True) # Relational Fields - property_type_id = fields.Many2one("estate.property.type", string="Property Type") - buyer_id = fields.Many2one("res.partner", string="Buyer", 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="Salesman", default=lambda self: self.env.user + 'res.users', string="Salesman", 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") + tag_ids = fields.Many2many('estate.property.tag', string="Tags") + offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") # Computed Fields - total_area = fields.Integer( - string="Total Area (sqm)", compute="_compute_total_area" - ) - best_price = fields.Float( - string="Best Offer", compute="_compute_best_price", store=True - ) + total_area = fields.Integer(string="Total Area (sqm)", compute='_compute_total_area') + best_price = fields.Float(string="Best Offer", compute='_compute_best_price', store=True) # SQL Constraints _check_expected_price = models.Constraint( - "CHECK(expected_price > 0)", + 'CHECK(expected_price > 0)', "The expected price must be strictly positive.", ) _check_selling_price = models.Constraint( - "CHECK(selling_price >= 0)", + 'CHECK(selling_price >= 0)', "The selling price must be positive.", ) # Compute / Inverse Methods - @api.depends("living_area", "garden_area") + @api.depends('living_area', 'garden_area') def _compute_total_area(self): for record in self: - record.total_area = (record.living_area or 0) + (record.garden_area or 0) + record.total_area = record.living_area + record.garden_area - @api.depends("offer_ids.price") + @api.depends('offer_ids.price') def _compute_best_price(self): for record in self: + # offer_ids is ordered by 'price desc', so index 0 gives the highest current offer record.best_price = record.offer_ids[0].price if record.offer_ids else 0.0 # Onchange Methods - @api.onchange("garden") + @api.onchange('garden') def _onchange_garden(self): if self.garden: self.garden_area = 10 - self.garden_orientation = "north" + self.garden_orientation = 'north' else: self.garden_area = 0 self.garden_orientation = False # Constrains Methods - @api.constrains("selling_price", "expected_price") + @api.constrains('selling_price', 'expected_price') def _check_selling_price_ratio(self): for record in self: if not float_is_zero(record.selling_price, precision_rounding=0.01): @@ -109,6 +106,7 @@ def _check_selling_price_ratio(self): # Helper Methods def _check_new_offer_price(self, price): self.ensure_one() + # offer_ids is ordered by 'price desc', so index 0 gives the highest current offer if self.offer_ids and price <= self.offer_ids[0].price: raise UserError(self.env._("The offer amount must be strictly higher than existing offers.")) @@ -117,29 +115,35 @@ def _accept_offer(self, buyer, price): if self.buyer_id: raise UserError(self.env._("An offer has already been accepted for this property.")) self.write({ - "buyer_id": buyer.id, - "selling_price": price, - "state": "offer_accepted", + 'buyer_id': buyer.id, + 'selling_price': price, + 'state': 'offer_accepted', }) # CRUD Methods @api.ondelete(at_uninstall=False) def _unlink_except_new_or_cancelled(self): for record in self: - if record.state not in ("new", "cancelled"): + if record.state not in {'new', 'cancelled'}: raise UserError(self.env._("Only new and cancelled properties can be deleted.")) # Action Methods def action_cancel(self): - if self.filtered(lambda record: record.state == "sold"): - raise UserError(self.env._("A sold property cannot be cancelled.")) - self.write({"state": "cancelled"}) + sold_properties = self.filtered(lambda record: record.state == 'sold') + if sold_properties: + names = ", ".join(sold_properties.mapped('name')) + raise UserError(self.env._("The following sold properties cannot be cancelled: %s.", names)) + self.write({'state': 'cancelled'}) return True def action_sold(self): - if self.filtered(lambda record: record.state == "cancelled"): - raise UserError(self.env._("A cancelled property cannot be sold.")) - if self.filtered(lambda prop: prop.state != "offer_accepted"): - raise UserError(self.env._("A property can only be sold if an offer has been accepted.")) - self.write({"state": "sold"}) + cancelled_properties = self.filtered(lambda record: record.state == 'cancelled') + if cancelled_properties: + names = ", ".join(cancelled_properties.mapped('name')) + raise UserError(self.env._("The following cancelled properties cannot be sold: %s", names)) + not_accepted_properties = self.filtered(lambda prop: prop.state != 'offer_accepted') + if not_accepted_properties: + names = ", ".join(not_accepted_properties.mapped('name')) + raise UserError(self.env._("The following properties must have an accepted offer before being sold: %s", names)) + self.write({'state': 'sold'}) return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 9f3571a11c4..824d5acc386 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -6,28 +6,28 @@ class PropertyOffer(models.Model): # Attributes - _name = "estate.property.offer" + _name = 'estate.property.offer' _description = "Real Estate Property Offer" - _order = "price desc" + _order = 'price desc' # Fields price = fields.Float(string="Price") status = fields.Selection( selection=[ - ("accepted", "Accepted"), - ("refused", "Refused"), + ('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) + 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) # Relational Fields property_type_id = fields.Many2one( - "estate.property.type", - related="property_id.property_type_id", + 'estate.property.type', + related='property_id.property_type_id', string="Property Type", store=True, ) @@ -35,18 +35,18 @@ class PropertyOffer(models.Model): # Computed Fields date_deadline = fields.Date( string="Deadline", - compute="_compute_date_deadline", - inverse="_inverse_date_deadline", + compute='_compute_date_deadline', + inverse='_inverse_date_deadline', ) # SQL Constraints _check_price = models.Constraint( - "CHECK(price > 0)", + 'CHECK(price > 0)', "The offer price must be strictly positive.", ) # Compute / Inverse Methods - @api.depends("validity") + @api.depends('validity') def _compute_date_deadline(self): for record in self: date = record.create_date.date() if record.create_date else fields.Date.today() @@ -61,27 +61,25 @@ def _inverse_date_deadline(self): # CRUD Methods @api.model_create_multi def create(self, vals_list): - property_ids = list({vals["property_id"] for vals in vals_list}) - properties = self.env["estate.property"].browse(property_ids) for vals in vals_list: - property_rec = properties.filtered(lambda property: property.id == vals["property_id"]) - property_rec._check_new_offer_price(vals["price"]) - properties.write({"state": "offer_received"}) + record = self.env['estate.property'].browse(vals['property_id']) + record._check_new_offer_price(vals['price']) + record.state = 'offer_received' return super().create(vals_list) # Action Methods def action_accept(self): - if self.filtered(lambda offer: offer.status in ('accepted', 'refused')): + if self.filtered(lambda offer: offer.status in {'accepted', 'refused'}): raise UserError(self.env._("You can only accept pending offers.")) for record in self: record.property_id._accept_offer(record.partner_id, record.price) other_offers = record.property_id.offer_ids - record - other_offers.write({"status": "refused"}) - self.write({"status": "accepted"}) + other_offers.write({'status': 'refused'}) + self.write({'status': 'accepted'}) return True def action_refuse(self): - if self.filtered(lambda offer: offer.status == "accepted"): + if self.filtered(lambda offer: offer.status == 'accepted'): raise UserError(self.env._("An accepted offer cannot be refused.")) - self.write({"status": "refused"}) + self.write({'status': 'refused'}) return True diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index fb72dd51344..f38a17ff63c 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -3,9 +3,9 @@ class PropertyTag(models.Model): # Attributes - _name = "estate.property.tag" + _name = 'estate.property.tag' _description = "Real Estate Property Tag" - _order = "name" + _order = 'name' # Fields name = fields.Char(required=True) @@ -13,6 +13,6 @@ class PropertyTag(models.Model): # SQL Constraints _unique_name = models.Constraint( - "UNIQUE(name)", + 'UNIQUE(name)', "The property tag name must be unique.", ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 031d66614d5..41d4caef6b7 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -3,30 +3,29 @@ class PropertyType(models.Model): # Attributes - _name = "estate.property.type" + _name = 'estate.property.type' _description = "Real Estate Property Type" - _order = "sequence, name" + _order = 'sequence, name' # Fields name = fields.Char(string="Name", required=True) - sequence = fields.Integer(string="Sequence", default=10) + sequence = fields.Integer(string="Sequence") # Relational Fields - property_ids = fields.One2many("estate.property", "property_type_id", string="Properties") - offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers") + property_ids = fields.One2many('estate.property', 'property_type_id', string="Properties") + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers") # Computed Fields - offer_count = fields.Integer(string="Offers Count", compute="_compute_offer_count") + offer_count = fields.Integer(string="Offers Count", compute='_compute_offer_count') # SQL Constraints _unique_name = models.Constraint( - "UNIQUE(name)", + 'UNIQUE(name)', "The property type name must be unique.", ) # Compute Methods - # TODO: FIND A WAY TO DO A SINGLE COUNT QUERY !!!!!!!!!!!!!! - @api.depends("offer_ids") + @api.depends('offer_ids') def _compute_offer_count(self): for record in self: record.offer_count = len(record.offer_ids) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index 4433882e992..b0fbdac688f 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,26 +1,23 @@ - + - + - + - - - diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 9f6817de8d1..f7d1e3012d8 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -1,14 +1,13 @@ - - + Property Offers estate.property.offer list,form - + estate.property.offer.list estate.property.offer @@ -24,5 +23,4 @@ - diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml index ab9358b7fb7..bc527ecb7fc 100644 --- a/estate/views/estate_property_tag_views.xml +++ b/estate/views/estate_property_tag_views.xml @@ -1,6 +1,5 @@ - Property Tags estate.property.tag @@ -16,5 +15,4 @@ - diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml index 30a5fc79333..b9a8610d1d0 100644 --- a/estate/views/estate_property_type_views.xml +++ b/estate/views/estate_property_type_views.xml @@ -1,13 +1,13 @@ - + Property Types estate.property.type list,form - + estate.property.type.list estate.property.type @@ -19,7 +19,7 @@ - + estate.property.type.form estate.property.type @@ -30,7 +30,8 @@ diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 3e264d7fc29..1176448632c 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,14 +1,14 @@ - + Properties estate.property - list,form + kanban,list,form {'search_default_available': True} - + estate.property.list estate.property @@ -26,7 +26,7 @@ - + estate.property.form estate.property @@ -57,7 +57,6 @@ - @@ -71,7 +70,6 @@ - - -
- + estate.property.search estate.property @@ -111,11 +107,8 @@ - - - @@ -123,4 +116,34 @@ + + estate.property.kanban + estate.property + + + + + +
+
+ +
+
+ Expected Price: +
+
+ Best Offer: +
+
+ Selling Price: +
+
+ +
+
+
+
+
+
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml index b90b1642457..a000d4c4b83 100644 --- a/estate/views/res_users_views.xml +++ b/estate/views/res_users_views.xml @@ -1,6 +1,5 @@ - res.users.form.inherit.estate res.users @@ -13,5 +12,4 @@
- diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index 53f0f2a8575..a729f215d9a 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -1,10 +1,9 @@ { - "name": "Estate Account", - "version": "1.0", - 'author': 'tomic', - "depends": ["estate", "account"], - "data": [], - "installable": True, - "application": False, + 'name': 'Estate Account', + 'version': '1.0', + 'author': 'Odoo S.A.', + 'depends': ['estate', 'account'], + 'installable': True, + 'application': False, 'license': 'LGPL-3', } diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 768cee23352..3fabec87258 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -2,7 +2,7 @@ class EstateProperty(models.Model): - _inherit = "estate.property" + _inherit = 'estate.property' def action_sold(self): res = super().action_sold() @@ -10,22 +10,22 @@ def action_sold(self): for record in self: invoices_vals.append({ - "partner_id": record.buyer_id.id, - "move_type": "out_invoice", - "invoice_line_ids": [ + 'partner_id': record.buyer_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': [ Command.create({ - "name": f"Commission (6%) - {record.name}", - "quantity": 1, - "price_unit": 0.06 * record.selling_price, + 'name': self.env._("Commission (6%%) - %s", record.name), + 'quantity': 1, + 'price_unit': 0.06 * record.selling_price, }), Command.create({ - "name": "Administrative fees", - "quantity": 1, - "price_unit": 100.00, + 'name': self.env._("Administrative fees"), + 'quantity': 1, + 'price_unit': 100.00, }), ], }) if invoices_vals: - self.env["account.move"].with_context(default_move_type="out_invoice").create(invoices_vals) + self.env['account.move'].create(invoices_vals) return res