From 1db8870ec4dee8905fcf253184f33015f7d8845a Mon Sep 17 00:00:00 2001 From: rvar-odoo Date: Wed, 2 Jul 2025 19:02:40 +0530 Subject: [PATCH 1/8] [ADD] estate: initial module for real estate properties Created new 'estate' module. Added base model 'estate.property' with fields mentioned in exercise. Set up module structure. Set 'name' and 'expected_price' as required fields. --- estate/__init__.py | 1 + estate/__manifest__.py | 9 +++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py 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 new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ 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 new file mode 100644 index 00000000000..42644eb784a --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,9 @@ +{ + 'name': 'Real Estate', + 'category': 'Sales/CRM', + 'summary': 'Demo app for estate', + 'description': "This is the demo app ", + 'installable': True, + 'application': True, + 'auto_install': False +} \ 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..4e9feb4402f --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,30 @@ + +from odoo import fields, models + + +class RecurringPlan(models.Model): + _name = "estate.property" + _description = "estate property revenue plans" + + name = fields.Char(required=True) + description = fields.Text(string="Description") + postcode = fields.Char(string="Postcode") + date_availability = fields.Date(string="Available From") + expected_price = fields.Float(string="Expected Price", required=True) + selling_price = fields.Float(string="Selling Price") + bedrooms = fields.Integer(string="Bedrooms") + living_area = fields.Integer(string="Living Area (sqm)") + facades = fields.Integer(string="Number of Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area (sqm)") + garden_orientation = fields.Selection( + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ], + string='Garden Orientation', + help="Orientation of the garden relative to the property" + ) From 447c426b738f57238502ebd4825800f2b2968d8f Mon Sep 17 00:00:00 2001 From: rvar-odoo Date: Fri, 4 Jul 2025 10:07:12 +0530 Subject: [PATCH 2/8] [ADD] estate: initial module with security and basic views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces the initial version of the module as part of the Odoo 18 developer tutorial. The module includes basic models, access rights, security rules, and simple form and tree views for managing real estate properties. The purpose of this change is to set up a foundational structure for the module. It follows the official tutorial steps to demonstrate Odoo’s ORM, security mechanisms, and view definitions. Adding proper access control ensures that only authorized users can interact with the module. The initial UI provides the groundwork for extending functionality later. This is part of a learning exercise to understand Odoo’s server framework and how to implement a feature-rich module following best practices. --- estate/__manifest__.py | 10 ++- estate/models/estate_property.py | 22 +++++-- estate/security/ir.model.access.csv | 2 + estate/views/estate_menus.xml | 14 ++++ estate/views/estate_property_views.xml | 90 ++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 estate/security/ir.model.access.csv 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 42644eb784a..44b70f45831 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,9 +1,15 @@ { 'name': 'Real Estate', - 'category': 'Sales/CRM', + 'category': 'All', 'summary': 'Demo app for estate', 'description': "This is the demo app ", 'installable': True, + 'depends': ['base'], 'application': True, - 'auto_install': False + 'auto_install': False, + 'data' : [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml' + ] } \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 4e9feb4402f..151a2b72fae 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ - +from dateutil.relativedelta import relativedelta from odoo import fields, models @@ -9,10 +9,10 @@ class RecurringPlan(models.Model): name = fields.Char(required=True) description = fields.Text(string="Description") postcode = fields.Char(string="Postcode") - date_availability = fields.Date(string="Available From") + date_availability = fields.Date(string="Available From", copy=False, default=lambda self: fields.Date.today() + relativedelta(months=3)) expected_price = fields.Float(string="Expected Price", required=True) - selling_price = fields.Float(string="Selling Price") - bedrooms = fields.Integer(string="Bedrooms") + selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) + bedrooms = fields.Integer(string="Bedrooms", default=2) living_area = fields.Integer(string="Living Area (sqm)") facades = fields.Integer(string="Number of Facades") garage = fields.Boolean(string="Garage") @@ -28,3 +28,17 @@ class RecurringPlan(models.Model): string='Garden Orientation', help="Orientation of the garden relative to the property" ) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ('new', 'NEW'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], + required=True, + default='new', + copy=False + + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..e9e8f757eca --- /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_model,estate.property,model_estate_property,,1,1,1,0 \ 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..f9164cb151d --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..068652f59b5 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,90 @@ + + + Properties + estate.property + list,form + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + +
From cdb6fa82958fa4340e7ffd5e131ad99259b21be7 Mon Sep 17 00:00:00 2001 From: rvar-odoo Date: Fri, 4 Jul 2025 17:54:11 +0530 Subject: [PATCH 3/8] [IMP] estate: add relations, computed fields, and server actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit enhances the module by introducing relational fields (many2one, one2many, and many2many), computed fields with dependencies, onchange methods for dynamic form updates, and server actions to extend business logic. The motivation for these changes is to provide a richer data model and dynamic behavior in the estate management workflow. Relations connect properties to users and offers, improving data integrity. Computed and stored fields enable automatic updates (e.g., total area or best offer). Onchange handlers improve UX by pre-filling or validating data at the form level. Server actions and automated behaviors lay the groundwork for more complex business processes. These improvements align with Odoo’s design philosophy of reactive and modular business logic while maintaining clear separation between models and views. --- estate/models/estate_property.py | 38 ++++++++++++++++++++++++- estate/models/estate_property_offer.py | 39 ++++++++++++++++++++++++++ estate/views/estate_property_views.xml | 22 +++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 estate/models/estate_property_offer.py diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 151a2b72fae..c57227665b4 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,5 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import fields, models, api class RecurringPlan(models.Model): @@ -42,3 +42,39 @@ class RecurringPlan(models.Model): copy=False ) + + total_area = fields.Integer(string="Total Area", compute="_compute_total_area") + best_price = fields.Float(string="Best Offer", compute="_compute_best_price") + + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + user_id = fields.Many2one("res.users", string="Salesman", copy=False, default=lambda self: self.env.user) + + tag_id = fields.Many2many("estate.property.tag", string="Tags",copy=False) + + offer_id = fields.One2many("estate.property.offer","property_id", string="Offer") + + + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for area in self: + area.total_area = area.living_area + area.garden_area + + + @api.depends('offer_id.price') + def _compute_best_price(self): + for record in self: + # Get all prices from offer_ids + offer_prices = record.offer_id.mapped('price') + record.best_price = max(offer_prices) if offer_prices else 0.0 + + + @api.onchange("garden") + def _onchange_garden(self): + if(self.garden): + self.garden_orientation = 'north' + self.garden_area = 10 + else: + self.garden_orientation = False + self.garden_area = 0 \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..3c941d50dd7 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,39 @@ +from odoo import fields, models, api +from dateutil.relativedelta import relativedelta + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Offers of Estate property " + + price = fields.Float(string="Price") + status = fields.Selection( + selection = [ + ('accepted','Accepted'), + ('refused','Refused') + ], copy=False,string='Status' + ) + + validity = fields.Integer(string="Validity", default=7) + date_deadline = fields.Date(string="Deadline", compute="_compute_deadline_date", inverse="_inverse_deadline_date") + + partner_id = fields.Many2one("res.partner", string="Partner",copy=False,required=True) + property_id = fields.Many2one("estate.property",copy=False,required=True) + + + + @api.depends('create_date','validity') + def _compute_deadline_date(self): + for offer in self: + create_dt = offer.create_date or fields.Date.today() + offer.date_deadline = create_dt + relativedelta(days=offer.validity) + + + def _inverse_deadline_date(self): + for offer in self: + if offer.date_deadline: + create_dt = (offer.create_date or fields.Datetime.now()).date() + delta = (offer.date_deadline - create_dt).days + offer.validity = delta + else: + offer.validity = 0 diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 068652f59b5..a670c0ba5d7 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -44,6 +44,9 @@ + + + @@ -57,9 +60,28 @@ + + + + + + + + + + + + + + + + + + + From 45baaf411ccea432eb8c1b87ee39177690da5006 Mon Sep 17 00:00:00 2001 From: rvar-odoo Date: Mon, 7 Jul 2025 11:24:27 +0530 Subject: [PATCH 4/8] [IMP] estate: add relations, computed fields, and server actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit enhances the estate module by introducing relational fields (many2one, one2many, and many2many), computed fields with dependencies, onchange methods for dynamic form updates, and server actions to extend business logic. The motivation for these changes is to provide a richer data model and dynamic behavior in the estate management workflow. Relations connect properties to users and offers, improving data integrity. Computed and stored fields enable automatic updates (e.g., total area or best offer). Onchange handlers improve UX by pre-filling or validating data at the form level. Server actions and automated behaviors lay the groundwork for more complex business processes. These improvements align with Odoo’s design philosophy of reactive and modular business logic while maintaining clear separation between models and views. --- estate/models/estate_property.py | 21 +++++++++++++++++++-- estate/models/estate_property_offer.py | 25 ++++++++++++++++++++++++- estate/views/estate_property_views.xml | 23 +++++++++++++++++------ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index c57227665b4..b85f847051b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,7 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models, api +from odoo import fields, models, api, _ +from odoo.exceptions import UserError + class RecurringPlan(models.Model): @@ -37,6 +39,7 @@ class RecurringPlan(models.Model): ('sold', 'Sold'), ('cancelled', 'Cancelled') ], + string="Status", required=True, default='new', copy=False @@ -77,4 +80,18 @@ def _onchange_garden(self): self.garden_area = 10 else: self.garden_orientation = False - self.garden_area = 0 \ No newline at end of file + self.garden_area = 0 + + def action_property_sold(self): + for prop in self: + if prop.state == 'cancelled': + raise UserError(_("Cancelled properties cannot be sold.")) + prop.state = 'sold' + return True + + def action_property_cancel(self): + for prop in self: + if prop.state == 'sold': + raise UserError(_("Sold properties cannot be cancelled.")) + prop.state = 'cancelled' + return True \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 3c941d50dd7..7e07e5bf9b6 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,5 +1,6 @@ -from odoo import fields, models, api +from odoo import fields, models, api, _ from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError class EstatePropertyOffer(models.Model): @@ -37,3 +38,25 @@ def _inverse_deadline_date(self): offer.validity = delta else: offer.validity = 0 + + def action_accept(self): + for offer in self: + # Allow only if property has no accepted offer + if offer.property_id.buyer_id: + raise UserError(_("An offer has already been accepted for this property.")) + # Set buyer and selling price + offer.property_id.buyer_id = offer.partner_id + offer.property_id.selling_price = offer.price + offer.status = 'accepted' + # Setting remaining Offer as refused + other_offers = offer.property_id.offer_id - offer + # for other in other_offers: # -----> Normal for loop logic + # other.status = 'refused' + other_offers.write({'status': 'refused'}) # -----> Odoo ORM Method + + return True + + def action_refused(self): + for offer in self: + offer.status = 'refused' + return True diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index a670c0ba5d7..882701cbfea 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -27,6 +27,10 @@ estate.property
+
+

@@ -34,7 +38,12 @@

- + + + + + + @@ -73,11 +82,13 @@ - - - - - + + + + + + + +
+

+ +

+
+ + + + + + + + + + + +
+
+
+ + + + + From 82359486c79da142c48cdaf370a72d79283e853b Mon Sep 17 00:00:00 2001 From: rvar-odoo Date: Wed, 9 Jul 2025 15:23:44 +0530 Subject: [PATCH 6/8] [IMP] estate: add inheritance and module interaction features - Added model and view inheritance for extending existing functionality. - Implemented interaction with external modules using dependencies. - Updated estate module to demonstrate cross-module field access and method calls. --- estate/__manifest__.py | 5 ++++- estate/models/__init__.py | 6 +++++- estate/models/estate_property.py | 8 +++++++- estate/models/estate_property_offer.py | 27 +++++++++++++++++++++++++- estate/models/inherited_model.py | 7 +++++++ estate/security/ir.model.access.csv | 5 ++++- estate/views/res_users_views.xml | 15 ++++++++++++++ 7 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 estate/models/inherited_model.py create mode 100644 estate/views/res_users_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 58298929883..9a3f50c5081 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -11,6 +11,9 @@ 'security/ir.model.access.csv', 'views/estate_property_offer_views.xml', 'views/estate_property_views.xml', - 'views/estate_menus.xml' + 'views/estate_menus.xml', + 'views/estate_settings_views.xml', + 'views/estate_property_tags_views.xml', + 'views/res_users_views.xml' ] } \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..0be15897435 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,5 @@ -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 +from . import inherited_model \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 90dcaff1a43..35a2f384f81 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -116,4 +116,10 @@ def _check_selling_price(self): raise ValidationError( "Selling price cannot be lower than 90% of the expected price. " f"(Minimum allowed: {min_allowed_price:.2f})" - ) \ No newline at end of file + ) + + @api.ondelete(at_uninstall=False) + def _unlink_except_state_not_new(self): + for rec in self: + if rec.state not in ['new', 'cancelled']: + raise UserError(_("You cannot delete a property unless its state is 'New' or 'Cancelled'.")) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 743a97e968d..d6590164cc5 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import fields, models, api, _ +from odoo import fields, models, api, exceptions, _ from dateutil.relativedelta import relativedelta from odoo.exceptions import UserError @@ -66,3 +66,28 @@ def action_refused(self): for offer in self: offer.status = 'refused' return True + + @api.model_create_multi + def create(self, vals_list): + new_records = [] + print(f"vals_list---------> {vals_list}") + for vals in vals_list: + property_id = vals.get('property_id') + + property_rec = self.env['estate.property'].browse(property_id) + # Business logic: set property state + if property_rec.state == 'new': + property_rec.state = 'offer_received' + print("DEBUG: Property state set to 'offer_received'") + + # Business logic: validate price + if 'price' in vals and vals['price'] < property_rec.best_price: + raise exceptions.ValidationError("Offer price must be higher than existing offers.") + + new_records.append(vals) + + return super().create(new_records) + + + + # vals_list---------> [{'partner_id': 23, 'price': 100, 'validity': 7, 'date_deadline': '2025-07-16', 'property_id': 19}] diff --git a/estate/models/inherited_model.py b/estate/models/inherited_model.py new file mode 100644 index 00000000000..649eb42f9a8 --- /dev/null +++ b/estate/models/inherited_model.py @@ -0,0 +1,7 @@ +from odoo import models, fields + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many(comodel_name='estate.property', inverse_name='user_id', string='Properties', domain=[('state', 'in', ['new', 'offer_received'])] +) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index e9e8f757eca..c5dcdaee55e 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_model,estate.property,model_estate_property,,1,1,1,0 \ No newline at end of file +access_estate_model,estate.property,model_estate_property,,1,1,1,1 +access_estate_model_property,estate.property.type,model_estate_property_type,,1,1,1,1 +access_estate_model_tag,estate.property.tag,model_estate_property_tag,,1,1,1,1 +access_estate_model_offer,estate.property.offer,model_estate_property_offer,,1,1,1,1 \ No newline at end of file diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..de6fcc194b7 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + res.users.form.inherit.estate + res.users + + + + + + + + + + + \ No newline at end of file From f96eeb391bfe9e25631a062a8c0473b4702544db Mon Sep 17 00:00:00 2001 From: rvar-odoo Date: Thu, 10 Jul 2025 10:59:22 +0530 Subject: [PATCH 7/8] [IMP] estate: interact with external modules and code cleanup - Implemented cross-module field access and method calls to demonstrate interaction with other modules. - Cleaned codebase to fix linter errors and improve readability. --- estate/__init__.py | 2 +- estate/__manifest__.py | 37 ++++---- estate/models/__init__.py | 2 +- estate/models/estate_property.py | 106 +++++++++++++---------- estate/models/estate_property_offer.py | 69 ++++++++------- estate/models/estate_property_tag.py | 15 ++-- estate/models/estate_property_type.py | 31 ++++--- estate/models/inherited_model.py | 11 ++- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 7 ++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 48 ++++++++++ 12 files changed, 216 insertions(+), 114 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/__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 9a3f50c5081..d59e60af2e6 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,19 +1,20 @@ { - 'name': 'Real Estate', - 'category': 'All', - 'summary': 'Demo app for estate', - 'description': "This is the demo app ", - 'installable': True, - 'depends': ['base'], - 'application': True, - 'auto_install': False, - 'data' : [ - 'security/ir.model.access.csv', - 'views/estate_property_offer_views.xml', - 'views/estate_property_views.xml', - 'views/estate_menus.xml', - 'views/estate_settings_views.xml', - 'views/estate_property_tags_views.xml', - 'views/res_users_views.xml' - ] -} \ No newline at end of file + "name": "Real Estate", + "category": "All", + "summary": "Demo app for estate", + "description": "This is the demo app ", + "installable": True, + "depends": ["base"], + "application": True, + "auto_install": False, + "license": "AGPL-3", + "data": [ + "security/ir.model.access.csv", + "views/estate_property_offer_views.xml", + "views/estate_property_views.xml", + "views/estate_settings_views.xml", + "views/estate_property_tags_views.xml", + "views/res_users_views.xml", + "views/estate_menus.xml", + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 0be15897435..c0917a3d550 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,4 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer -from . import inherited_model \ No newline at end of file +from . import inherited_model diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 35a2f384f81..eb492b51d13 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,20 +1,22 @@ from dateutil.relativedelta import relativedelta from odoo import fields, models, api, _ from odoo.exceptions import UserError, ValidationError -from odoo.tools.float_utils import float_compare, float_is_zero, float_round - - +from odoo.tools.float_utils import float_compare, float_is_zero class RecurringPlan(models.Model): _name = "estate.property" _description = "estate property revenue plans" - _order = "id desc" # For ordering in ascending opr descending order one more way to do so is from view like: + _order = "id desc" # For ordering in ascending opr descending order one more way to do so is from view like: name = fields.Char(required=True) description = fields.Text(string="Description") postcode = fields.Char(string="Postcode") - date_availability = fields.Date(string="Available From", copy=False, default=lambda self: fields.Date.today() + relativedelta(months=3)) + date_availability = fields.Date( + string="Available From", + copy=False, + default=lambda self: fields.Date.today() + relativedelta(months=3), + ) expected_price = fields.Float(string="Expected Price", required=True) selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) bedrooms = fields.Integer(string="Bedrooms", default=2) @@ -25,29 +27,28 @@ class RecurringPlan(models.Model): garden_area = fields.Integer(string="Garden Area (sqm)") garden_orientation = fields.Selection( selection=[ - ('north', 'North'), - ('south', 'South'), - ('east', 'East'), - ('west', 'West') + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), ], - string='Garden Orientation', - help="Orientation of the garden relative to the property" + string="Garden Orientation", + help="Orientation of the garden relative to the property", ) active = fields.Boolean(default=True) state = fields.Selection( 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"), ], string="Status", - required=True, - default='new', + required=True, + default="new", copy=False, # readonly=True - ) total_area = fields.Integer(string="Total Area", compute="_compute_total_area") @@ -55,64 +56,75 @@ class RecurringPlan(models.Model): property_type_id = fields.Many2one("estate.property.type", string="Property Type") buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) - user_id = fields.Many2one("res.users", string="Salesman", copy=False, default=lambda self: self.env.user) - - tag_id = fields.Many2many("estate.property.tag", string="Tags",copy=False) + user_id = fields.Many2one( + "res.users", string="Salesman", copy=False, default=lambda self: self.env.user + ) - offer_id = fields.One2many("estate.property.offer","property_id", string="Offer") + tag_id = fields.Many2many("estate.property.tag", string="Tags", copy=False) + offer_id = fields.One2many("estate.property.offer", "property_id", string="Offer") _sql_constraints = [ - ('check_expected_price', 'CHECK(expected_price > 0)','The Expected price of a property should be strictly positive.'), - ('check_selling_price', 'CHECK(selling_price IS NULL OR selling_price >= 0)', 'Selling price must be positive when set.') + ( + "check_expected_price", + "CHECK(expected_price > 0)", + "The Expected price of a property should be strictly positive.", + ), + ( + "check_selling_price", + "CHECK(selling_price IS NULL OR selling_price >= 0)", + "Selling price must be positive when set.", + ), ] - - @api.depends('living_area', 'garden_area') + @api.depends("living_area", "garden_area") def _compute_total_area(self): for area in self: area.total_area = area.living_area + area.garden_area - - @api.depends('offer_id.price') + @api.depends("offer_id.price") def _compute_best_price(self): for record in self: # Get all prices from offer_ids - offer_prices = record.offer_id.mapped('price') + offer_prices = record.offer_id.mapped("price") record.best_price = max(offer_prices) if offer_prices else 0.0 - @api.onchange("garden") def _onchange_garden(self): - if(self.garden): - self.garden_orientation = 'north' + if self.garden: + self.garden_orientation = "north" self.garden_area = 10 else: self.garden_orientation = False self.garden_area = 0 - - def action_property_sold(self): - for prop in self: - if prop.state == 'cancelled': + + def action_property_sold(self): + for prop in self: + if prop.state == "cancelled": raise UserError(_("Cancelled properties cannot be sold.")) - prop.state = 'sold' - return True + prop.state = "sold" + return True def action_property_cancel(self): for prop in self: - if prop.state == 'sold': + if prop.state == "sold": raise UserError(_("Sold properties cannot be cancelled.")) - prop.state = 'cancelled' + prop.state = "cancelled" return True - @api.constrains('selling_price','expected_price') + @api.constrains("selling_price", "expected_price") def _check_selling_price(self): for record in self: if float_is_zero(record.selling_price, precision_digits=2): continue min_allowed_price = record.expected_price * 0.9 - if float_compare(record.selling_price, min_allowed_price, precision_digits=2) < 0: + if ( + float_compare( + record.selling_price, min_allowed_price, precision_digits=2 + ) + < 0 + ): raise ValidationError( "Selling price cannot be lower than 90% of the expected price. " f"(Minimum allowed: {min_allowed_price:.2f})" @@ -121,5 +133,9 @@ def _check_selling_price(self): @api.ondelete(at_uninstall=False) def _unlink_except_state_not_new(self): for rec in self: - if rec.state not in ['new', 'cancelled']: - raise UserError(_("You cannot delete a property unless its state is 'New' or 'Cancelled'.")) + if rec.state not in ["new", "cancelled"]: + raise UserError( + _( + "You cannot delete a property unless its state is 'New' or 'Cancelled'." + ) + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index d6590164cc5..e1f6bd9af69 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -10,31 +10,42 @@ class EstatePropertyOffer(models.Model): price = fields.Float(string="Price") status = fields.Selection( - selection = [ - ('accepted','Accepted'), - ('refused','Refused') - ], copy=False,string='Status' + selection=[("accepted", "Accepted"), ("refused", "Refused")], + copy=False, + string="Status", ) validity = fields.Integer(string="Validity", default=7) - date_deadline = fields.Date(string="Deadline", compute="_compute_deadline_date", inverse="_inverse_deadline_date") + date_deadline = fields.Date( + string="Deadline", + compute="_compute_deadline_date", + inverse="_inverse_deadline_date", + ) - partner_id = fields.Many2one("res.partner", string="Partner",copy=False,required=True) - property_id = fields.Many2one("estate.property",copy=False,required=True) - property_type_id = fields.Many2one("estate.property.type", related="property_id.property_type_id",store=True,readonly=False) - _sql_constraints =[ - ('check_offer_price', 'CHECK(price > 0)', 'The Offer price must be strictly positive') + partner_id = fields.Many2one( + "res.partner", string="Partner", copy=False, required=True + ) + property_id = fields.Many2one("estate.property", copy=False, required=True) + property_type_id = fields.Many2one( + "estate.property.type", + related="property_id.property_type_id", + store=True, + readonly=False, + ) + _sql_constraints = [ + ( + "check_offer_price", + "CHECK(price > 0)", + "The Offer price must be strictly positive", + ) ] - - - @api.depends('create_date','validity') + @api.depends("create_date", "validity") def _compute_deadline_date(self): for offer in self: create_dt = offer.create_date or fields.Date.today() offer.date_deadline = create_dt + relativedelta(days=offer.validity) - def _inverse_deadline_date(self): for offer in self: if offer.date_deadline: @@ -48,46 +59,46 @@ def action_accept(self): for offer in self: # Allow only if property has no accepted offer if offer.property_id.buyer_id: - raise UserError(_("An offer has already been accepted for this property.")) + raise UserError( + _("An offer has already been accepted for this property.") + ) # Set buyer and selling price offer.property_id.buyer_id = offer.partner_id offer.property_id.selling_price = offer.price - offer.status = 'accepted' - offer.property_id.state = 'offer_accepted' + offer.status = "accepted" + offer.property_id.state = "offer_accepted" # Setting remaining Offer as refused other_offers = offer.property_id.offer_id - offer # for other in other_offers: # -----> Normal for loop logic # other.status = 'refused' - other_offers.write({'status': 'refused'}) # -----> Odoo ORM Method + other_offers.write({"status": "refused"}) # -----> Odoo ORM Method return True def action_refused(self): for offer in self: - offer.status = 'refused' + offer.status = "refused" return True @api.model_create_multi def create(self, vals_list): new_records = [] - print(f"vals_list---------> {vals_list}") for vals in vals_list: - property_id = vals.get('property_id') + property_id = vals.get("property_id") - property_rec = self.env['estate.property'].browse(property_id) + property_rec = self.env["estate.property"].browse(property_id) # Business logic: set property state - if property_rec.state == 'new': - property_rec.state = 'offer_received' - print("DEBUG: Property state set to 'offer_received'") + if property_rec.state == "new": + property_rec.state = "offer_received" # Business logic: validate price - if 'price' in vals and vals['price'] < property_rec.best_price: - raise exceptions.ValidationError("Offer price must be higher than existing offers.") + if "price" in vals and vals["price"] < property_rec.best_price: + raise exceptions.ValidationError( + "Offer price must be higher than existing offers." + ) new_records.append(vals) return super().create(new_records) - - # vals_list---------> [{'partner_id': 23, 'price': 100, 'validity': 7, 'date_deadline': '2025-07-16', 'property_id': 19}] diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index b8999687a52..09db9eec7f7 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -1,15 +1,18 @@ from odoo import fields, models + class EstatePropertyTag(models.Model): _name = "estate.property.tag" _description = "Tags of Estate property " _order = "name asc" - name = fields.Char(required=True, string='Name') + name = fields.Char(required=True, string="Name") color = fields.Integer(default=3) - - - _sql_constraints=[ - ('unique_property_tag_name', 'UNIQUE(name)', 'A property tag name must be unique.'), - ] \ No newline at end of file + _sql_constraints = [ + ( + "unique_property_tag_name", + "UNIQUE(name)", + "A property tag name must be unique.", + ), + ] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 19b406d13c3..b1a0d6e4e24 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,24 +1,33 @@ from odoo import fields, models, api + class EstatePropertyType(models.Model): _name = "estate.property.type" _description = "Types of Estate property " _order = "sequence, name asc" # Order by sequence first - name = fields.Char(required=True) - sequence = fields.Integer('Sequence', default=10, help="Used to order property types.") + sequence = fields.Integer( + "Sequence", default=10, help="Used to order property types." + ) - property_ids = fields.One2many("estate.property","property_type_id") - offer_ids = fields.One2many("estate.property.offer","property_type_id", string="Offers") - offer_count = fields.Integer(string="Offer Count", compute="_compute_offer_count", store=True) + property_ids = fields.One2many("estate.property", "property_type_id") + offer_ids = fields.One2many( + "estate.property.offer", "property_type_id", string="Offers" + ) + offer_count = fields.Integer( + string="Offer Count", compute="_compute_offer_count", store=True + ) - - _sql_constraints=[ - ('unique_property_type_name', 'UNIQUE(name)', 'A property type name must be unique.'), + _sql_constraints = [ + ( + "unique_property_type_name", + "UNIQUE(name)", + "A property type name must be unique.", + ), ] - - @api.depends('offer_ids') + + @api.depends("offer_ids") def _compute_offer_count(self): for rec in self: - rec.offer_count = len(rec.offer_ids) \ No newline at end of file + rec.offer_count = len(rec.offer_ids) diff --git a/estate/models/inherited_model.py b/estate/models/inherited_model.py index 649eb42f9a8..e7460290d2c 100644 --- a/estate/models/inherited_model.py +++ b/estate/models/inherited_model.py @@ -1,7 +1,12 @@ from odoo import models, fields + class ResUsers(models.Model): - _inherit = 'res.users' + _inherit = "res.users" - property_ids = fields.One2many(comodel_name='estate.property', inverse_name='user_id', string='Properties', domain=[('state', 'in', ['new', 'offer_received'])] -) + property_ids = fields.One2many( + comodel_name="estate.property", + inverse_name="user_id", + string="Properties", + domain=[("state", "in", ["new", "offer_received"])], + ) 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..0e97fb85c9a --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,7 @@ +{ + "name": "Estate Account", + "depends": ["base", "estate", "account"], + "category": "Tutorials", + "installable": True, + "license": "AGPL-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..cdd924412a6 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,48 @@ +from odoo import models, Command +from odoo.exceptions import UserError + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_property_sold(self): + res = super().action_property_sold() + for property in self: + if not property.buyer_id: + continue + + journal = self.env["account.journal"].search( + [("type", "=", "sale")], limit=1 + ) + + if not journal: + raise UserError("No sales journal found!") + + commission = property.selling_price * 0.06 + + admin_fee = 100.00 + + self.env["account.move"].create( + { + "partner_id": property.buyer_id.id, + "move_type": "out_invoice", + "journal_id": journal.id, + "invoice_line_ids": [ + Command.create( + { + "name": "Commission (6%)", + "quantity": 1, + "price_unit": commission, + } + ), + Command.create( + { + "name": "Administrative fees", + "quantity": 1, + "price_unit": admin_fee, + } + ), + ], + } + ) + return res From b6e2c1ed04a0d0c2953837bd4af511fdf6c6d0d0 Mon Sep 17 00:00:00 2001 From: rvar-odoo Date: Thu, 10 Jul 2025 11:58:21 +0530 Subject: [PATCH 8/8] [IMP] estate: interact with external modules and code cleanup - Implemented cross-module field access and method calls to demonstrate interaction with other modules. - Cleaned codebase to fix linter errors and improve readability. --- estate/security/ir.model.access.csv | 8 +++---- estate/views/estate_menus.xml | 15 +++++++++++++ estate/views/estate_property_tags_views.xml | 24 +++++++++++++++++++++ estate/views/estate_property_views.xml | 4 ---- 4 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 estate/views/estate_property_tags_views.xml diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index c5dcdaee55e..392f26fecfe 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_estate_model,estate.property,model_estate_property,,1,1,1,1 -access_estate_model_property,estate.property.type,model_estate_property_type,,1,1,1,1 -access_estate_model_tag,estate.property.tag,model_estate_property_tag,,1,1,1,1 -access_estate_model_offer,estate.property.offer,model_estate_property_offer,,1,1,1,1 \ No newline at end of file +access_estate_model,estate.property,model_estate_property,base.group_user,1,1,1,1 +access_estate_model_property,estate.property.type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_model_tag,estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_model_offer,estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml index f9164cb151d..720bfe10b1f 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -9,6 +9,21 @@ action="estate_property_action"/> + + + + + + + + + + + + diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml new file mode 100644 index 00000000000..da6830b2fc2 --- /dev/null +++ b/estate/views/estate_property_tags_views.xml @@ -0,0 +1,24 @@ + + + Property Tag + estate.property.tag + list,form + + + + estate.property.tag.form + estate.property.tag + +
+ +
+

+ +

+
+
+
+
+
+ +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 1ebab62339d..8ff87d110d4 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -49,9 +49,6 @@ - @@ -97,7 +94,6 @@ decoration-danger="status == 'refused'" decoration-success="status == 'accepted'"> -