From 85beafe720b27713e2aed98309983c56304b0cb3 Mon Sep 17 00:00:00 2001 From: metu-odoo Date: Thu, 3 Jul 2025 12:18:14 +0530 Subject: [PATCH 1/9] [ADD] real_estate: initial module structure and database table Created the initial file architecture for the real_estate module, including the necessary directories and manifest file. Implemented the estate.property model using Odoo ORM to define the database table structure required for storing property-related data. This commit sets up the foundation for the module, making it ready for further development of features such as property management, views, and business logic. --- real_estate/__init__.py | 1 + real_estate/__manifest__.py | 12 ++++++++++++ real_estate/data/real_estate.xml | 0 real_estate/models/__init__.py | 1 + real_estate/models/real_estate.py | 27 +++++++++++++++++++++++++++ 5 files changed, 41 insertions(+) create mode 100644 real_estate/__init__.py create mode 100644 real_estate/__manifest__.py create mode 100644 real_estate/data/real_estate.xml create mode 100644 real_estate/models/__init__.py create mode 100644 real_estate/models/real_estate.py diff --git a/real_estate/__init__.py b/real_estate/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/real_estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/real_estate/__manifest__.py b/real_estate/__manifest__.py new file mode 100644 index 00000000000..d65042efc8a --- /dev/null +++ b/real_estate/__manifest__.py @@ -0,0 +1,12 @@ +{ + 'name': "Dream Homes", + 'version': '1.0', + 'depends': ['base'], + 'author': "Megha Tulsyani", +# # data files containing optionally loaded demonstration data +# 'demo': [ +# 'demo/demo_data.xml', +# ], + 'installable': True, + 'application': True, +} \ No newline at end of file diff --git a/real_estate/data/real_estate.xml b/real_estate/data/real_estate.xml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/real_estate/models/__init__.py b/real_estate/models/__init__.py new file mode 100644 index 00000000000..22c8ee9c371 --- /dev/null +++ b/real_estate/models/__init__.py @@ -0,0 +1 @@ +from . import real_estate \ No newline at end of file diff --git a/real_estate/models/real_estate.py b/real_estate/models/real_estate.py new file mode 100644 index 00000000000..ceb31541d87 --- /dev/null +++ b/real_estate/models/real_estate.py @@ -0,0 +1,27 @@ +from odoo import models, fields + +class RealEstate(models.Model): + _name = "real.estate" + _description = "This is a real estate module" + + + 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 91fd5ca2c82edd18c382086f3783cfbcf700b394 Mon Sep 17 00:00:00 2001 From: metu-odoo Date: Fri, 4 Jul 2025 11:57:31 +0530 Subject: [PATCH 2/9] [IMP] real_estate: add access rules and property views Purpose: Restrict model access and enhance user interaction. Approach: Defined access rights via `ir.model.access.csv`; created list, form and search views with key fields. Impact: Users see only permitted records and can easily view, search, and manage properties through a structured UI. --- real_estate/__init__.py | 2 +- real_estate/__manifest__.py | 16 ++-- real_estate/data/real_estate.xml | 0 real_estate/models/__init__.py | 2 +- real_estate/models/real_estate.py | 32 ++++++-- real_estate/security/ir.model.access.csv | 2 + real_estate/views/real_estate_menus.xml | 12 +++ real_estate/views/real_estate_views.xml | 95 ++++++++++++++++++++++++ 8 files changed, 146 insertions(+), 15 deletions(-) delete mode 100644 real_estate/data/real_estate.xml create mode 100644 real_estate/security/ir.model.access.csv create mode 100644 real_estate/views/real_estate_menus.xml create mode 100644 real_estate/views/real_estate_views.xml diff --git a/real_estate/__init__.py b/real_estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/real_estate/__init__.py +++ b/real_estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/real_estate/__manifest__.py b/real_estate/__manifest__.py index d65042efc8a..91ca9c3635f 100644 --- a/real_estate/__manifest__.py +++ b/real_estate/__manifest__.py @@ -3,10 +3,14 @@ 'version': '1.0', 'depends': ['base'], 'author': "Megha Tulsyani", -# # data files containing optionally loaded demonstration data -# 'demo': [ -# 'demo/demo_data.xml', -# ], + 'category': 'Ecommerce For Properties', + 'data' :[ + + 'security/ir.model.access.csv', + 'views/real_estate_views.xml', + 'views/real_estate_menus.xml', + ], + 'license': 'LGPL-3', 'installable': True, - 'application': True, -} \ No newline at end of file + 'application': True +} diff --git a/real_estate/data/real_estate.xml b/real_estate/data/real_estate.xml deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/real_estate/models/__init__.py b/real_estate/models/__init__.py index 22c8ee9c371..05018e037ce 100644 --- a/real_estate/models/__init__.py +++ b/real_estate/models/__init__.py @@ -1 +1 @@ -from . import real_estate \ No newline at end of file +from . import real_estate diff --git a/real_estate/models/real_estate.py b/real_estate/models/real_estate.py index ceb31541d87..50a82a15e25 100644 --- a/real_estate/models/real_estate.py +++ b/real_estate/models/real_estate.py @@ -1,22 +1,28 @@ from odoo import models, fields +from dateutil.relativedelta import relativedelta class RealEstate(models.Model): _name = "real.estate" _description = "This is a real estate module" - - + + name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date( + string="Availability From", + default=lambda self: fields.Date.today() + relativedelta(months=3), + copy=False,) + 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() + active = fields.Boolean(default=True) garden_orientation = fields.Selection( selection=[ ('north','North'), @@ -24,4 +30,16 @@ class RealEstate(models.Model): ('east','East'), ('west','West') ] - ) \ No newline at end of file + ) + state = fields.Selection( + selection=[ + ('new','New'), + ('offer received','Offer Received'), + ('offer accepted','Offer Accepted'), + ('sold','Sold'), + ('cancelled','Cancelled'), + ], + copy = False, + required= True, + default='new' + ) diff --git a/real_estate/security/ir.model.access.csv b/real_estate/security/ir.model.access.csv new file mode 100644 index 00000000000..85c0f3522da --- /dev/null +++ b/real_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 +real_estate.access_real_estate,access_real_estate,real_estate.model_real_estate,base.group_user,1,1,1,1 diff --git a/real_estate/views/real_estate_menus.xml b/real_estate/views/real_estate_menus.xml new file mode 100644 index 00000000000..f92d00f03f2 --- /dev/null +++ b/real_estate/views/real_estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/real_estate/views/real_estate_views.xml b/real_estate/views/real_estate_views.xml new file mode 100644 index 00000000000..fb364267a45 --- /dev/null +++ b/real_estate/views/real_estate_views.xml @@ -0,0 +1,95 @@ + + + + + Estate Property + real.estate + list,form + +

+ Define a new Property +

+

+ Create a Propety to sell/Buy. +

+
+
+ + + real.estate.list + real.estate + + + + + + + + + + + + + + real.estate.form + real.estate + +
+ + +

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + real.estate.search + real.estate + + + + + + + + + + + + + + + + + +
From 8542e335d8ee8764cb4246416c50c3b8c309cd4f Mon Sep 17 00:00:00 2001 From: metu-odoo Date: Tue, 8 Jul 2025 10:47:02 +0530 Subject: [PATCH 3/9] [IMP] real_estate: add model relations, computed fields, and action buttons Purpose: Improve data linkage, automate values, and streamline user actions. Approach: - Linked models via One2many, Many2one, and Many2many fields. - Added computed fields like best_offer and offer_deadline. - Used onchange for reactive UI updates. - Added buttons for actions: Sold/Cancel (property), Accept/Refuse (offer). Impact: Simplifies workflows, ensures data consistency, and improves overall usability. --- real_estate/__manifest__.py | 9 +- real_estate/models/__init__.py | 5 +- real_estate/models/estate_property.py | 100 ++++++++++++++++ real_estate/models/estate_property_offers.py | 52 ++++++++ real_estate/models/estate_property_tags.py | 8 ++ real_estate/models/estate_property_types.py | 9 ++ real_estate/models/real_estate.py | 45 ------- real_estate/security/ir.model.access.csv | 5 +- ...te_menus.xml => estate_property_menus.xml} | 7 +- .../views/estate_property_offer_views.xml | 34 ++++++ .../views/estate_property_tag_views.xml | 18 +++ .../views/estate_property_type_views.xml | 17 +++ real_estate/views/estate_property_views.xml | 112 ++++++++++++++++++ real_estate/views/real_estate_views.xml | 95 --------------- 14 files changed, 369 insertions(+), 147 deletions(-) create mode 100644 real_estate/models/estate_property.py create mode 100644 real_estate/models/estate_property_offers.py create mode 100644 real_estate/models/estate_property_tags.py create mode 100644 real_estate/models/estate_property_types.py delete mode 100644 real_estate/models/real_estate.py rename real_estate/views/{real_estate_menus.xml => estate_property_menus.xml} (50%) create mode 100644 real_estate/views/estate_property_offer_views.xml create mode 100644 real_estate/views/estate_property_tag_views.xml create mode 100644 real_estate/views/estate_property_type_views.xml create mode 100644 real_estate/views/estate_property_views.xml delete mode 100644 real_estate/views/real_estate_views.xml diff --git a/real_estate/__manifest__.py b/real_estate/__manifest__.py index 91ca9c3635f..96aa8a7954b 100644 --- a/real_estate/__manifest__.py +++ b/real_estate/__manifest__.py @@ -4,11 +4,14 @@ 'depends': ['base'], 'author': "Megha Tulsyani", 'category': 'Ecommerce For Properties', - 'data' :[ + 'data': [ 'security/ir.model.access.csv', - 'views/real_estate_views.xml', - 'views/real_estate_menus.xml', + 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + "views/estate_property_tag_views.xml", + "views/estate_property_offer_views.xml", + 'views/estate_property_menus.xml' ], 'license': 'LGPL-3', 'installable': True, diff --git a/real_estate/models/__init__.py b/real_estate/models/__init__.py index 05018e037ce..a5a5c081b05 100644 --- a/real_estate/models/__init__.py +++ b/real_estate/models/__init__.py @@ -1 +1,4 @@ -from . import real_estate +from . import estate_property +from . import estate_property_offers +from . import estate_property_tags +from . import estate_property_types diff --git a/real_estate/models/estate_property.py b/real_estate/models/estate_property.py new file mode 100644 index 00000000000..ceadb5c12d5 --- /dev/null +++ b/real_estate/models/estate_property.py @@ -0,0 +1,100 @@ +from odoo import api, fields, models +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "This is a real estate module" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + 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)") + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + active = fields.Boolean(default=True) + buyer_id = fields.Many2one(comodel_name="res.partner", string="Buyer", copy=False) + total_area = fields.Float(compute='_compute_total_area', string="Total Area" ) + best_offer = fields.Float(compute='_compute_best_price',string="Best Offer") + date_availability = fields.Date( + string="Availability From", + default=lambda self: fields.Date.today() + relativedelta(months=3), + copy=False, + ) + estate_property_offer_ids = fields.One2many( + comodel_name="estate.property.offers", + inverse_name="property_id", + string="Offers", + ) + seller_id = fields.Many2one( + comodel_name="res.users", + string="SalesPerson", + default=lambda self: self.env.user, + ) + estate_property_type_id = fields.Many2one( + comodel_name="estate.property.types", string="Property Type" + ) + estate_property_tag_ids = fields.Many2many( + comodel_name="estate.property.tags", + string="Tags" + ) + garden_orientation = fields.Selection( + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ] + ) + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + copy=False, + required=True, + default="new", + ) + + @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('estate_property_offer_ids') + def _compute_best_price(self): + for record in self: + prices = record.estate_property_offer_ids.mapped("price") + record.best_offer = max(prices) if prices else 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 + + + def action_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError("You cannot mark a cancelled property as sold.") + record.state = 'sold' + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise UserError("You cannot mark a sold property as cancelled.") + record.state = 'cancelled' diff --git a/real_estate/models/estate_property_offers.py b/real_estate/models/estate_property_offers.py new file mode 100644 index 00000000000..ec457f4c37a --- /dev/null +++ b/real_estate/models/estate_property_offers.py @@ -0,0 +1,52 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstatePropertyOffers(models.Model): + _name = "estate.property.offers" + _description = "Estate Property Offers" + + price = fields.Float() + partner_id = fields.Many2one(comodel_name="res.partner", required=True) + property_id = fields.Many2one(comodel_name="estate.property", required=True) + validity = fields.Integer(default='7') + offer_deadline = fields.Date( + compute='_compute_date_deadline', + inverse='_inverse_date_deadline' + ) + status = fields.Selection( + selection=[ + ("accepted", "Accepted"), + ("refused", "Refused") + ] + ) + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for record in self: + if record.create_date and record.validity: + record.offer_deadline = fields.Date.add(record.create_date, days=record.validity) + else: + record.offer_deadline = fields.Date.add(fields.Date.today(), days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + if record.create_date and record.offer_deadline: + record.validity = (record.offer_deadline - fields.Date.to_date(record.create_date)).days + + def action_accept_offer(self): + for offer in self: + accepted_offer = self.env['estate.property.offers'].search([ + ('property_id', '=', offer.property_id.id), + ('status', '=', 'accepted') + ]) + if accepted_offer: + raise UserError("Only one offer can be accepted per property.") + offer.status = 'accepted' + offer.property_id.selling_price = offer.price + offer.property_id.buyer_id = offer.partner_id + offer.property_id.state = 'offer_accepted' + + def action_refuse_offer(self): + for offer in self: + offer.status = 'refused' diff --git a/real_estate/models/estate_property_tags.py b/real_estate/models/estate_property_tags.py new file mode 100644 index 00000000000..5ec0b566c34 --- /dev/null +++ b/real_estate/models/estate_property_tags.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class EstatePropertyTags(models.Model): + _name = "estate.property.tags" + _description = "Estate Property Tags" + + name = fields.Char(required=True) diff --git a/real_estate/models/estate_property_types.py b/real_estate/models/estate_property_types.py new file mode 100644 index 00000000000..38f7b1e706b --- /dev/null +++ b/real_estate/models/estate_property_types.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class EstatePropertyTypes(models.Model): + _name = "estate.property.types" + _description = "Estate Property Types" + + name = fields.Char(required=True) + diff --git a/real_estate/models/real_estate.py b/real_estate/models/real_estate.py deleted file mode 100644 index 50a82a15e25..00000000000 --- a/real_estate/models/real_estate.py +++ /dev/null @@ -1,45 +0,0 @@ -from odoo import models, fields -from dateutil.relativedelta import relativedelta - -class RealEstate(models.Model): - _name = "real.estate" - _description = "This is a real estate module" - - - name = fields.Char(required=True) - description = fields.Text() - postcode = fields.Char() - date_availability = fields.Date( - string="Availability From", - default=lambda self: fields.Date.today() + relativedelta(months=3), - copy=False,) - - 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)") - facades = fields.Integer() - garage = fields.Boolean() - garden = fields.Boolean() - garden_area = fields.Integer() - active = fields.Boolean(default=True) - garden_orientation = fields.Selection( - selection=[ - ('north','North'), - ('south','South'), - ('east','East'), - ('west','West') - ] - ) - state = fields.Selection( - selection=[ - ('new','New'), - ('offer received','Offer Received'), - ('offer accepted','Offer Accepted'), - ('sold','Sold'), - ('cancelled','Cancelled'), - ], - copy = False, - required= True, - default='new' - ) diff --git a/real_estate/security/ir.model.access.csv b/real_estate/security/ir.model.access.csv index 85c0f3522da..c0e938254c6 100644 --- a/real_estate/security/ir.model.access.csv +++ b/real_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 -real_estate.access_real_estate,access_real_estate,real_estate.model_real_estate,base.group_user,1,1,1,1 +real_estate.access_estate_property,access_estate_property,real_estate.model_estate_property,base.group_user,1,1,1,1 +real_estate.access_estate_property_types,access_estate_property_types,real_estate.model_estate_property_types,base.group_user,1,1,1,1 +real_estate.access_estate_property_tags,access_estate_property_tags,real_estate.model_estate_property_tags,base.group_user,1,1,1,1 +real_estate.access_estate_property_offers,access_estate_property_offers,real_estate.model_estate_property_offers,base.group_user,1,1,1,1 diff --git a/real_estate/views/real_estate_menus.xml b/real_estate/views/estate_property_menus.xml similarity index 50% rename from real_estate/views/real_estate_menus.xml rename to real_estate/views/estate_property_menus.xml index f92d00f03f2..632e9133b83 100644 --- a/real_estate/views/real_estate_menus.xml +++ b/real_estate/views/estate_property_menus.xml @@ -1,12 +1,15 @@ - - + + + + + diff --git a/real_estate/views/estate_property_offer_views.xml b/real_estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..3aa45a7cd2d --- /dev/null +++ b/real_estate/views/estate_property_offer_views.xml @@ -0,0 +1,34 @@ + + + + estate.property.offer.list + estate.property.offers + + + + + + + + + + +

+ +

+
+ + + + + + + + + + + + + +
+
+ + + estate.property.type.list + estate.property.types + + + + + + + +
diff --git a/real_estate/views/estate_property_views.xml b/real_estate/views/estate_property_views.xml index ccf80111b1d..0b73506a386 100644 --- a/real_estate/views/estate_property_views.xml +++ b/real_estate/views/estate_property_views.xml @@ -5,6 +5,7 @@ Estate Property estate.property list,form + {'search_default_available': True}

Define a new Property @@ -19,14 +20,18 @@ real.estate.list estate.property - + - + @@ -36,20 +41,33 @@

-

- +
- + @@ -68,13 +86,15 @@ - - + + - + @@ -97,7 +117,9 @@ - + From 5e763835491edbc6e68b71107db576f67fc0efe6 Mon Sep 17 00:00:00 2001 From: metu-odoo Date: Wed, 9 Jul 2025 19:06:05 +0530 Subject: [PATCH 5/9] [ADD] estate_account: modular invoicing extension for real_estate Purpose: Introduce a separate module to handle property invoicing and commissions. Approach: Created `estate_account` module with billing logic, including fixed admin fees and percentage-based commissions. Impact: Enables optional, reusable invoicing integration for real_estate without polluting core logic. --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 11 +++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 48 ++++++++++++++++++++ real_estate/__manifest__.py | 1 + real_estate/models/__init__.py | 1 + real_estate/models/estate_property.py | 11 +++++ real_estate/models/estate_property_offers.py | 15 ++++++ real_estate/models/res_users.py | 11 +++++ real_estate/views/estate_property_menus.xml | 15 +++--- real_estate/views/res_users_views.xml | 16 +++++++ 11 files changed, 125 insertions(+), 6 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 create mode 100644 real_estate/models/res_users.py create mode 100644 real_estate/views/res_users_views.xml 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..8ba12ae1b60 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,11 @@ +{ + "name": "Estate Account", + "version": "1.0", + "depends": ["real_estate", "account"], + "author": "", + "category": "Accounts for Estate", + "data": [], + "license": "LGPL-3", + "installable": True, + "application": True, +} 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..322abde00c7 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,48 @@ +from odoo import Command, models + + +class Property(models.Model): + _inherit = "estate.property" + + def action_sold(self): + for property in self: + journal = self.env["account.journal"].search( + [("type", "=", "sale")], limit=1 + ) + + commission = property.selling_price * 0.06 + discount = property.selling_price * 0.05 + invoice_lines = [ + Command.create( + { + "name": "6% Commission", + "quantity": 1, + "price_unit": commission, + } + ), + Command.create( + { + "name": "Administrative Fees", + "quantity": 1, + "price_unit": 100.00, + } + ), + Command.create( + { + "name": "Discount", + "quantity": 1, + "price_unit": -discount, + } + ), + ] + + invoice_vals = { + "partner_id": property.buyer_id.id, + "move_type": "out_invoice", + "journal_id": journal.id, + "invoice_line_ids": invoice_lines, + } + + self.env["account.move"].create(invoice_vals) + + return super().action_sold() diff --git a/real_estate/__manifest__.py b/real_estate/__manifest__.py index e0b8accc372..fbe59bdf45d 100644 --- a/real_estate/__manifest__.py +++ b/real_estate/__manifest__.py @@ -11,6 +11,7 @@ "views/estate_property_type_views.xml", "views/estate_property_tag_views.xml", "views/estate_property_menus.xml", + "views/res_users_views.xml", ], "license": "LGPL-3", "installable": True, diff --git a/real_estate/models/__init__.py b/real_estate/models/__init__.py index a5a5c081b05..7a502ee8822 100644 --- a/real_estate/models/__init__.py +++ b/real_estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_offers from . import estate_property_tags from . import estate_property_types +from . import res_users diff --git a/real_estate/models/estate_property.py b/real_estate/models/estate_property.py index 9dd84093867..885f2cf00cb 100644 --- a/real_estate/models/estate_property.py +++ b/real_estate/models/estate_property.py @@ -88,6 +88,9 @@ def _onchange_garden(self): def action_sold(self): for record in self: + if not record.state == "offer_accepted": + raise UserError("Accept an offer first") + if record.state == "cancelled": raise UserError("You cannot mark a cancelled property as sold.") record.state = "sold" @@ -117,3 +120,11 @@ def _check_selling_price(self): raise ValidationError( "Selling price must be at least 90% of the expected price." ) + + @api.ondelete(at_uninstall=False) + def _check_deletion_state(self): + for rec in self: + if rec.state not in ["new", "cancelled"]: + raise UserError( + "Only properties in 'New' or 'Cancelled' state can be deleted." + ) diff --git a/real_estate/models/estate_property_offers.py b/real_estate/models/estate_property_offers.py index 196aba0dea7..4a83c16d56c 100644 --- a/real_estate/models/estate_property_offers.py +++ b/real_estate/models/estate_property_offers.py @@ -67,3 +67,18 @@ def action_refuse_offer(self): "Offer price must be strictly positive.", ) ] + + @api.model_create_multi + def create(self, vals): + for val in vals: + property_id = self.env["estate.property"].browse(val["property_id"]) + + if property_id.state == "new": + property_id.state = "offer_received" + + if val["price"] <= property_id.best_offer: + raise UserError( + f"The offer must be higher than {property_id.best_offer}" + ) + + return super().create(vals) diff --git a/real_estate/models/res_users.py b/real_estate/models/res_users.py new file mode 100644 index 00000000000..5867cfb8663 --- /dev/null +++ b/real_estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + comodel_name="estate.property", + inverse_name="seller_id", + domain="[('state', 'in', ('new','offer_received'))]", + ) diff --git a/real_estate/views/estate_property_menus.xml b/real_estate/views/estate_property_menus.xml index 632e9133b83..f730c268209 100644 --- a/real_estate/views/estate_property_menus.xml +++ b/real_estate/views/estate_property_menus.xml @@ -1,15 +1,18 @@ - + - - + + - - - + + + diff --git a/real_estate/views/res_users_views.xml b/real_estate/views/res_users_views.xml new file mode 100644 index 00000000000..11df075473f --- /dev/null +++ b/real_estate/views/res_users_views.xml @@ -0,0 +1,16 @@ + + + + + res.users.view.form.inherit.property + res.users + + + + + + + + + + From 06b1d0ec6736c54d8d16e817d2123acb3b0df0ca Mon Sep 17 00:00:00 2001 From: metu-odoo Date: Thu, 10 Jul 2025 15:23:55 +0530 Subject: [PATCH 6/9] [FIX] real_estate: apply coding rules, guidelines and improve security Purpose: Make the code cleaner and more secure. Approach: - Renamed methods and variables to follow Odoo naming rules. - Avoided unsafe public methods and direct field access. Impact: Code is easier to read, safer to use, and follows Odoo guidelines. --- estate_account/__init__.py | 2 + estate_account/__manifest__.py | 4 +- estate_account/models/__init__.py | 2 + estate_account/models/estate_property.py | 2 + real_estate/__init__.py | 2 + real_estate/__manifest__.py | 5 ++- real_estate/models/__init__.py | 2 + real_estate/models/estate_property.py | 45 ++++++++++--------- real_estate/models/estate_property_offers.py | 2 + real_estate/models/estate_property_tags.py | 2 + real_estate/models/estate_property_types.py | 15 +++---- real_estate/models/res_users.py | 2 + real_estate/views/estate_property_menus.xml | 17 +++---- .../views/estate_property_offer_views.xml | 8 ++-- .../views/estate_property_tag_views.xml | 14 ++---- .../views/estate_property_type_views.xml | 17 +++---- real_estate/views/estate_property_views.xml | 17 +++---- real_estate/views/res_users_views.xml | 1 - 18 files changed, 76 insertions(+), 83 deletions(-) diff --git a/estate_account/__init__.py b/estate_account/__init__.py index 0650744f6bc..d6210b1285d 100644 --- a/estate_account/__init__.py +++ b/estate_account/__init__.py @@ -1 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py index 8ba12ae1b60..61146f31997 100644 --- a/estate_account/__manifest__.py +++ b/estate_account/__manifest__.py @@ -1,10 +1,10 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + { "name": "Estate Account", "version": "1.0", "depends": ["real_estate", "account"], - "author": "", "category": "Accounts for Estate", - "data": [], "license": "LGPL-3", "installable": True, "application": True, diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py index 5e1963c9d2f..09b94f90f8d 100644 --- a/estate_account/models/__init__.py +++ b/estate_account/models/__init__.py @@ -1 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py index 322abde00c7..fe50e7a98d1 100644 --- a/estate_account/models/estate_property.py +++ b/estate_account/models/estate_property.py @@ -1,3 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo import Command, models diff --git a/real_estate/__init__.py b/real_estate/__init__.py index 0650744f6bc..d6210b1285d 100644 --- a/real_estate/__init__.py +++ b/real_estate/__init__.py @@ -1 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from . import models diff --git a/real_estate/__manifest__.py b/real_estate/__manifest__.py index fbe59bdf45d..38a11990492 100644 --- a/real_estate/__manifest__.py +++ b/real_estate/__manifest__.py @@ -1,9 +1,10 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + { "name": "Dream Homes", "version": "1.0", "depends": ["base"], - "author": "Megha Tulsyani", - "category": "Ecommerce For Properties", + "category": "Real Estate/Brokerage", "data": [ "security/ir.model.access.csv", "views/estate_property_views.xml", diff --git a/real_estate/models/__init__.py b/real_estate/models/__init__.py index 7a502ee8822..7d1b1c46ec3 100644 --- a/real_estate/models/__init__.py +++ b/real_estate/models/__init__.py @@ -1,3 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from . import estate_property from . import estate_property_offers from . import estate_property_tags diff --git a/real_estate/models/estate_property.py b/real_estate/models/estate_property.py index 885f2cf00cb..898ae96c71b 100644 --- a/real_estate/models/estate_property.py +++ b/real_estate/models/estate_property.py @@ -1,3 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo import api, fields, models from dateutil.relativedelta import relativedelta from odoo.exceptions import UserError, ValidationError @@ -9,18 +11,18 @@ class EstateProperty(models.Model): _description = "This is a real estate module" _order = "id desc" - name = fields.Char(required=True) - description = fields.Text() - postcode = fields.Char() - expected_price = fields.Float(required=True) - selling_price = fields.Float(readonly=True, copy=False) - bedrooms = fields.Integer(default=2) + name = fields.Char(string="Property Name", required=True) + description = fields.Text(string="Description") + postcode = fields.Char(string="Postcode") + expected_price = fields.Float(string="Expected Price", required=True) + selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) + bedrooms = fields.Integer(string="Total Bedrooms", default=2) living_area = fields.Integer(string="Living Area (sqm)") - facades = fields.Integer() - garage = fields.Boolean() - garden = fields.Boolean() - garden_area = fields.Integer() - active = fields.Boolean(default=True) + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area (sqm)") + active = fields.Boolean(string="Is Active", default=True) buyer_id = fields.Many2one(comodel_name="res.partner", string="Buyer", copy=False) total_area = fields.Float(compute="_compute_total_area", string="Total Area") best_offer = fields.Float(compute="_compute_best_price", string="Best Offer") @@ -51,7 +53,8 @@ class EstateProperty(models.Model): ("south", "South"), ("east", "East"), ("west", "West"), - ] + ], + string="Garden Orientation", ) state = fields.Selection( selection=[ @@ -64,7 +67,15 @@ class EstateProperty(models.Model): copy=False, required=True, default="new", + string="State", ) + _sql_constraints = [ + ( + "check_expected_price_positive", + "CHECK(expected_price > 0)", + "Expected price must be strictly positive.", + ), + ] @api.depends("living_area", "garden_area") def _compute_total_area(self): @@ -90,7 +101,6 @@ def action_sold(self): for record in self: if not record.state == "offer_accepted": raise UserError("Accept an offer first") - if record.state == "cancelled": raise UserError("You cannot mark a cancelled property as sold.") record.state = "sold" @@ -101,20 +111,11 @@ def action_cancel(self): raise UserError("You cannot mark a sold property as cancelled.") record.state = "cancelled" - _sql_constraints = [ - ( - "check_expected_price_positive", - "CHECK(expected_price > 0)", - "Expected price must be strictly positive.", - ), - ] - @api.constrains("expected_price", "selling_price") def _check_selling_price(self): for record in self: if float_is_zero(record.selling_price, precision_digits=2): continue - min_price = record.expected_price * 0.9 if float_compare(record.selling_price, min_price, precision_digits=2) < 0: raise ValidationError( diff --git a/real_estate/models/estate_property_offers.py b/real_estate/models/estate_property_offers.py index 4a83c16d56c..54e60a01225 100644 --- a/real_estate/models/estate_property_offers.py +++ b/real_estate/models/estate_property_offers.py @@ -1,3 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo import api, fields, models from odoo.exceptions import UserError diff --git a/real_estate/models/estate_property_tags.py b/real_estate/models/estate_property_tags.py index 4164b40ff21..487e77a2651 100644 --- a/real_estate/models/estate_property_tags.py +++ b/real_estate/models/estate_property_tags.py @@ -1,3 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo import models, fields diff --git a/real_estate/models/estate_property_types.py b/real_estate/models/estate_property_types.py index 2c161f1791a..a1cb85c483b 100644 --- a/real_estate/models/estate_property_types.py +++ b/real_estate/models/estate_property_types.py @@ -1,3 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo import api, fields, models @@ -8,15 +10,10 @@ class EstatePropertyTypes(models.Model): name = fields.Char(required=True) sequence = fields.Integer(default=1) - offer_ids = fields.One2many( - comodel_name="estate.property.offers", inverse_name="property_type_id" - ) - offer_count = fields.Integer( - string="Number of Offers", compute="_compute_offer_count" - ) - property_ids = fields.One2many( - "estate.property", "estate_property_type_id", string="Properties" - ) + offer_ids = fields.One2many(comodel_name="estate.property.offers", inverse_name="property_type_id") + offer_count = fields.Integer(string="Number of Offers", compute="_compute_offer_count") + property_ids = fields.One2many("estate.property", "estate_property_type_id", string="Properties") + _sql_constraints = [ ( "unique_type_name", diff --git a/real_estate/models/res_users.py b/real_estate/models/res_users.py index 5867cfb8663..cade946f04b 100644 --- a/real_estate/models/res_users.py +++ b/real_estate/models/res_users.py @@ -1,3 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo import fields, models diff --git a/real_estate/views/estate_property_menus.xml b/real_estate/views/estate_property_menus.xml index f730c268209..b14b84d095d 100644 --- a/real_estate/views/estate_property_menus.xml +++ b/real_estate/views/estate_property_menus.xml @@ -1,18 +1,15 @@ - - + - - - + + - + action="estate_property_main_view_action" /> + + action="estate_property_type_view_action" /> - + action="estate_property_tag_view_action" /> diff --git a/real_estate/views/estate_property_offer_views.xml b/real_estate/views/estate_property_offer_views.xml index 49a7de5307d..636ddaadd4a 100644 --- a/real_estate/views/estate_property_offer_views.xml +++ b/real_estate/views/estate_property_offer_views.xml @@ -1,19 +1,17 @@ - + estate.property.offer.action estate.property.offers list,form [('property_type_id', '=', active_id)] - + estate.property.offer.list estate.property.offers - + diff --git a/real_estate/views/estate_property_tag_views.xml b/real_estate/views/estate_property_tag_views.xml index d809a0d54b6..2e6934238d6 100644 --- a/real_estate/views/estate_property_tag_views.xml +++ b/real_estate/views/estate_property_tag_views.xml @@ -1,21 +1,16 @@ - - + Property Tags estate.property.tags list,form -

- Define a new Property Tag -

-

- Create a Propety tag and link it to as many properties as needed. -

+

Define a new Property Tag

+

Create a Propety tag and link it to as many properties as needed.

- + estate.property.tag.list estate.property.tags @@ -24,5 +19,4 @@
-
diff --git a/real_estate/views/estate_property_type_views.xml b/real_estate/views/estate_property_type_views.xml index 3a45e0e7886..4fab7193a10 100644 --- a/real_estate/views/estate_property_type_views.xml +++ b/real_estate/views/estate_property_type_views.xml @@ -1,27 +1,23 @@ - + Property Type estate.property.types list,form -

- Define a new Property Type -

-

- Create a Propety type and link it to as many properties as needed. -

+

Define a new Property Type

+

Create a Propety type and link it to as many properties as needed.

- + estate.property.type.form estate.property.types
-