From 7aaf8396627fab4554054a99903510f38f5db3e2 Mon Sep 17 00:00:00 2001 From: gasa-odoo Date: Thu, 3 Jul 2025 12:44:39 +0530 Subject: [PATCH 1/8] [ADD] real_estate: Create Our First App From this Tutorial We learn How to create our application in odoo. In this Tutorial we create manifest.py file where stored app name and we define version, and also create init.py file where import will be stored. When we create our app then it will show in apps module in odoo localhost server. from apps we can download the app which we create. Active developer mode is must be required. --- estate-gasa/__init__.py | 1 + estate-gasa/__manifest__.py | 10 ++++++++++ estate-gasa/models/__init__.py | 1 + estate-gasa/models/estate.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 estate-gasa/__init__.py create mode 100644 estate-gasa/__manifest__.py create mode 100644 estate-gasa/models/__init__.py create mode 100644 estate-gasa/models/estate.py diff --git a/estate-gasa/__init__.py b/estate-gasa/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate-gasa/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate-gasa/__manifest__.py b/estate-gasa/__manifest__.py new file mode 100644 index 00000000000..e382b995390 --- /dev/null +++ b/estate-gasa/__manifest__.py @@ -0,0 +1,10 @@ +{ + 'name': "estate", + 'version': '1.0', + 'depends': ['base'], + 'author': "Author Name", + 'category': 'Category', + "license":"LGPL-3", + "application":True, + "sequence":1, +} diff --git a/estate-gasa/models/__init__.py b/estate-gasa/models/__init__.py new file mode 100644 index 00000000000..e4f59229d23 --- /dev/null +++ b/estate-gasa/models/__init__.py @@ -0,0 +1 @@ +from . import estate diff --git a/estate-gasa/models/estate.py b/estate-gasa/models/estate.py new file mode 100644 index 00000000000..57509382b66 --- /dev/null +++ b/estate-gasa/models/estate.py @@ -0,0 +1,29 @@ +from odoo import fields, models + + +class Estate(models.Model): + _name = "estate.property" + _description = "Estate Property" + + nname = fields.Char(string="Title", required=True, default="New Property") + description = fields.Text(string="Description") + postcode = fields.Char(string="Postcode") + date_availability = fields.Date(string="Available From", default=fields.Date.today) + expected_price = fields.Float(string="Expected Price", required=True) + selling_price = fields.Float(string="Selling Price", readonly=True) + bedrooms = fields.Integer(string="Number of Bedrooms", default=2) + living_area = fields.Integer(string="Living Area (sqm)") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Has Garage") + garden = fields.Boolean(string="Has 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="Direction the garden faces" + ) From 8b76f10d3aff9d31ade63c80652eca3e67105d25 Mon Sep 17 00:00:00 2001 From: gasa-odoo Date: Fri, 4 Jul 2025 10:58:48 +0530 Subject: [PATCH 2/8] [ADD] real_estate: give some basic UI to New Applicataion In this chapter,first we add security access rules for the basic model to manage user permissions. next we learn to give UI to our Application which we create in our first tutorial. we add some basic views and and form in our application, also we add custmize search in model. --- .vscode/extensions.json | 5 ++ estate-gasa/__manifest__.py | 5 ++ estate-gasa/models/estate.py | 54 +++++++++---- estate-gasa/security/ir.model.access.csv | 2 + estate-gasa/views/estate_menus.xml | 7 ++ estate-gasa/views/estate_property_views.xml | 85 +++++++++++++++++++++ 6 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 estate-gasa/security/ir.model.access.csv create mode 100644 estate-gasa/views/estate_menus.xml create mode 100644 estate-gasa/views/estate_property_views.xml diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000000..0f148de5ef1 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode" + ] +} \ No newline at end of file diff --git a/estate-gasa/__manifest__.py b/estate-gasa/__manifest__.py index e382b995390..dd55f85418a 100644 --- a/estate-gasa/__manifest__.py +++ b/estate-gasa/__manifest__.py @@ -7,4 +7,9 @@ "license":"LGPL-3", "application":True, "sequence":1, + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml' + ], } diff --git a/estate-gasa/models/estate.py b/estate-gasa/models/estate.py index 57509382b66..2232bea24a9 100644 --- a/estate-gasa/models/estate.py +++ b/estate-gasa/models/estate.py @@ -1,29 +1,55 @@ from odoo import fields, models - +from datetime import date, timedelta class Estate(models.Model): _name = "estate.property" _description = "Estate Property" - nname = fields.Char(string="Title", required=True, default="New Property") + name = fields.Char(required=True,default="Unknown") description = fields.Text(string="Description") postcode = fields.Char(string="Postcode") - date_availability = fields.Date(string="Available From", default=fields.Date.today) - expected_price = fields.Float(string="Expected Price", required=True) - selling_price = fields.Float(string="Selling Price", readonly=True) - bedrooms = fields.Integer(string="Number of Bedrooms", default=2) - living_area = fields.Integer(string="Living Area (sqm)") + expected_price = fields.Float() + selling_price = fields.Float(copy=False,readonly=True) + bedrooms = fields.Integer(default=2) + last_seen = fields.Datetime("Last Seen", default=fields.Date.today) + date_availability = fields.Date( + default=lambda self: date.today() + timedelta(days=90), + copy=False + ) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ],default='new' + ) + living_area = fields.Integer(string="Living Area") facades = fields.Integer(string="Facades") - garage = fields.Boolean(string="Has Garage") - garden = fields.Boolean(string="Has Garden") - garden_area = fields.Integer(string="Garden Area (sqm)") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area") garden_orientation = fields.Selection( - selection=[ + [ ('north', 'North'), ('south', 'South'), ('east', 'East'), - ('west', 'West'), + ('west', 'West') ], - string="Garden Orientation", - help="Direction the garden faces" + string="Garden Orientation" ) + active = fields.Boolean(default=False) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], + default='new', + required=True, + copy=False +) diff --git a/estate-gasa/security/ir.model.access.csv b/estate-gasa/security/ir.model.access.csv new file mode 100644 index 00000000000..976b61e8cb3 --- /dev/null +++ b/estate-gasa/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate-gasa/views/estate_menus.xml b/estate-gasa/views/estate_menus.xml new file mode 100644 index 00000000000..b99c341032c --- /dev/null +++ b/estate-gasa/views/estate_menus.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/estate-gasa/views/estate_property_views.xml b/estate-gasa/views/estate_property_views.xml new file mode 100644 index 00000000000..91011597962 --- /dev/null +++ b/estate-gasa/views/estate_property_views.xml @@ -0,0 +1,85 @@ + + + + estate.property.form + estate.property + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.tree + estate.property + + + + + + + + + + + + + + estate.property.tree + estate.property + + + + + + + + + + + + + + + Real Estate + estate.property + list,form + +
\ No newline at end of file From 26fc1ca41ffb9af7b894a1d8c8e8ab8cf3d0eea2 Mon Sep 17 00:00:00 2001 From: gasa-odoo Date: Mon, 7 Jul 2025 10:09:00 +0530 Subject: [PATCH 3/8] [ADD] real_estate: add four model in our application In this tutorial we add four new models and create four-five pages like properties, tags, property type and so on.. also we learn how to manage UI for buttons and how to implement conditions using if-else statement. At the end we learn : How to implement business logic with condition checks and method decorators. How to create custom models and link them via relations (Many2one, One2many, Many2many). --- estate-gasa/__manifest__.py | 5 +- estate-gasa/models/__init__.py | 3 + estate-gasa/models/estate.py | 65 ++++++++++++++++++- estate-gasa/models/estate_property_offer.py | 54 +++++++++++++++ estate-gasa/models/estate_property_tag.py | 8 +++ estate-gasa/models/estate_property_type.py | 7 ++ estate-gasa/security/ir.model.access.csv | 5 +- estate-gasa/views/estate_menus.xml | 11 +++- .../views/estate_property_offer_views.xml | 39 +++++++++++ .../views/estate_property_type_views.xml | 7 ++ estate-gasa/views/estate_property_views.xml | 55 +++++++++++++++- estate-gasa/views/estate_tag_views.xml | 8 +++ 12 files changed, 262 insertions(+), 5 deletions(-) create mode 100644 estate-gasa/models/estate_property_offer.py create mode 100644 estate-gasa/models/estate_property_tag.py create mode 100644 estate-gasa/models/estate_property_type.py create mode 100644 estate-gasa/views/estate_property_offer_views.xml create mode 100644 estate-gasa/views/estate_property_type_views.xml create mode 100644 estate-gasa/views/estate_tag_views.xml diff --git a/estate-gasa/__manifest__.py b/estate-gasa/__manifest__.py index dd55f85418a..643cbf86cf7 100644 --- a/estate-gasa/__manifest__.py +++ b/estate-gasa/__manifest__.py @@ -10,6 +10,9 @@ 'data': [ 'security/ir.model.access.csv', 'views/estate_property_views.xml', - 'views/estate_menus.xml' + 'views/estate_property_type_views.xml', + 'views/estate_tag_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_menus.xml', ], } diff --git a/estate-gasa/models/__init__.py b/estate-gasa/models/__init__.py index e4f59229d23..b2de0366835 100644 --- a/estate-gasa/models/__init__.py +++ b/estate-gasa/models/__init__.py @@ -1 +1,4 @@ from . import estate +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer \ No newline at end of file diff --git a/estate-gasa/models/estate.py b/estate-gasa/models/estate.py index 2232bea24a9..80e3e545ae9 100644 --- a/estate-gasa/models/estate.py +++ b/estate-gasa/models/estate.py @@ -1,5 +1,6 @@ -from odoo import fields, models +from odoo import api, fields, models from datetime import date, timedelta +from odoo.exceptions import UserError class Estate(models.Model): _name = "estate.property" @@ -52,4 +53,66 @@ class Estate(models.Model): default='new', required=True, copy=False +) + property_type = fields.Many2one("estate.property.type", string="Property Type") + + buyer = fields.Many2one( + "res.partner", + string="Buyer", + copy=False + ) + + seller = fields.Many2one( + "res.users", + string="Salesperson", + default=lambda self: self.env.user + ) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many( + "estate.property.offer", "property_id", string="Offers" ) + total_area = fields.Integer( + string="Total Area", + compute="_compute_total_area", + store=True + ) + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + best_price = fields.Float( + string="Best Offer", + compute="_compute_best_price" + ) + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + prices = record.offer_ids.mapped("price") + record.best_price = 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 + + # Button methods + def action_mark_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError("Canceled properties cannot be sold.") + record.state = 'sold' + + def action_mark_cancelled(self): + for record in self: + if record.state == 'sold': + raise UserError("Sold properties cannot be canceled.") + record.state = 'cancelled' + + \ No newline at end of file diff --git a/estate-gasa/models/estate_property_offer.py b/estate-gasa/models/estate_property_offer.py new file mode 100644 index 00000000000..db9f4d4a80a --- /dev/null +++ b/estate-gasa/models/estate_property_offer.py @@ -0,0 +1,54 @@ +from odoo import api, models, fields +from datetime import date, timedelta +from odoo.exceptions import UserError + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Property Offer" + + price = fields.Float() + status = fields.Selection( + [('accepted', 'Accepted'), ('refused', 'Refused')], + copy=False + ) + partner_id = fields.Many2one("res.partner", string="Customer", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + store=True + ) + + @api.depends("validity", "create_date") + def _compute_date_deadline(self): + for record in self: + create_date = record.create_date or fields.Datetime.now() + record.date_deadline = create_date.date() + timedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + create_date = record.create_date or fields.Datetime.now() + record.validity = (record.date_deadline - create_date.date()).days + + def action_accept(self): + for offer in self: + if offer.property_id.state == 'sold': + raise UserError("Cannot accept an offer for a sold property.") + + # Refuse all other offers first + other_offers = offer.property_id.offer_ids.filtered(lambda o: o.id != offer.id) + other_offers.write({'status': 'refused'}) + + # Accept current offer + offer.status = 'accepted' + offer.property_id.selling_price = offer.price + offer.property_id.buyer = offer.partner_id + offer.property_id.state = 'offer_accepted' + + + def action_refuse(self): + for offer in self: + offer.status = 'refused' + \ No newline at end of file diff --git a/estate-gasa/models/estate_property_tag.py b/estate-gasa/models/estate_property_tag.py new file mode 100644 index 00000000000..a971c153523 --- /dev/null +++ b/estate-gasa/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import models, fields + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real Estate Property Tag" + _order = "name" + + name = fields.Char(required=True) diff --git a/estate-gasa/models/estate_property_type.py b/estate-gasa/models/estate_property_type.py new file mode 100644 index 00000000000..03e151991de --- /dev/null +++ b/estate-gasa/models/estate_property_type.py @@ -0,0 +1,7 @@ +from odoo import models, fields + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Property Type" + + name = fields.Char(required=True) diff --git a/estate-gasa/security/ir.model.access.csv b/estate-gasa/security/ir.model.access.csv index 976b61e8cb3..4c593ed42e4 100644 --- a/estate-gasa/security/ir.model.access.csv +++ b/estate-gasa/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 \ No newline at end of file +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 \ No newline at end of file diff --git a/estate-gasa/views/estate_menus.xml b/estate-gasa/views/estate_menus.xml index b99c341032c..0ba95a63a34 100644 --- a/estate-gasa/views/estate_menus.xml +++ b/estate-gasa/views/estate_menus.xml @@ -1,7 +1,16 @@ - + + + + + + + + \ No newline at end of file diff --git a/estate-gasa/views/estate_property_offer_views.xml b/estate-gasa/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..2ad1f95d623 --- /dev/null +++ b/estate-gasa/views/estate_property_offer_views.xml @@ -0,0 +1,39 @@ + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + + + +
+
+
+ + + Offers + estate.property.offer + list,form + +
\ No newline at end of file diff --git a/estate-gasa/views/estate_property_type_views.xml b/estate-gasa/views/estate_property_type_views.xml new file mode 100644 index 00000000000..568573b1f4b --- /dev/null +++ b/estate-gasa/views/estate_property_type_views.xml @@ -0,0 +1,7 @@ + + + Property Types + estate.property.type + list,form + + \ No newline at end of file diff --git a/estate-gasa/views/estate_property_views.xml b/estate-gasa/views/estate_property_views.xml index 91011597962..e36c0034349 100644 --- a/estate-gasa/views/estate_property_views.xml +++ b/estate-gasa/views/estate_property_views.xml @@ -5,19 +5,38 @@ estate.property
+
+

+ + + +
+ + @@ -34,10 +53,42 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ + Property Types estate.property.type diff --git a/estate-gasa/views/estate_property_views.xml b/estate-gasa/views/estate_property_views.xml index e36c0034349..4ddcbd964c7 100644 --- a/estate-gasa/views/estate_property_views.xml +++ b/estate-gasa/views/estate_property_views.xml @@ -10,27 +10,33 @@ type="object" string="Sold" class="btn-primary" - modifiers="{'invisible': [('state', 'in', ['sold', 'cancelled'])]}" /> - + invisible="context.get('readonly', False) or state in ('sold', 'cancelled')" /> - @@ -35,12 +25,10 @@ type="action" icon="fa-list" string="Offers" - context="{}" domain="[('property_type_id', '=', active_id)]" modifiers="{'invisible': [('offer_count', '=', 0)]}" /> + string="Number of Offers" /> diff --git a/estate-gasa/views/estate_property_views.xml b/estate_gasa/views/estate_property_views.xml similarity index 100% rename from estate-gasa/views/estate_property_views.xml rename to estate_gasa/views/estate_property_views.xml diff --git a/estate-gasa/views/estate_tag_views.xml b/estate_gasa/views/estate_tag_views.xml similarity index 100% rename from estate-gasa/views/estate_tag_views.xml rename to estate_gasa/views/estate_tag_views.xml diff --git a/estate-gasa/views/inherited_model.xml b/estate_gasa/views/inherited_model.xml similarity index 100% rename from estate-gasa/views/inherited_model.xml rename to estate_gasa/views/inherited_model.xml From baf79082bb7aa06fc6d4156b2dcd3c143180c5e2 Mon Sep 17 00:00:00 2001 From: gasa-odoo Date: Thu, 10 Jul 2025 10:07:27 +0530 Subject: [PATCH 8/8] [IMP] real_estate: apply Odoo coding guidelines to naming conventions In this tutorial, Renamed model and view files to follow standard Odoo naming (.py, _views.xml, etc.). Updated class names to use CamelCase as per convention. Adjusted method names to match Odoo patterns e.g., _compute_, _check_, action_). Ensured XML IDs and view names follow Odoo's structured naming scheme. Cleaned up import orders and variable names for readability and consistency. --- estate_gasa/__manifest__.py | 2 +- estate_gasa/models/inherited_model.py | 6 +++--- estate_gasa/views/estate_menus.xml | 6 +++--- estate_gasa/views/estate_property_offer_views.xml | 6 +++--- estate_gasa/views/estate_property_type_views.xml | 7 +++---- estate_gasa/views/estate_property_views.xml | 8 ++++---- estate_gasa/views/estate_tag_views.xml | 6 +++--- estate_gasa/views/inherited_model.xml | 4 ++-- 8 files changed, 22 insertions(+), 23 deletions(-) diff --git a/estate_gasa/__manifest__.py b/estate_gasa/__manifest__.py index a83604517b9..918517f1276 100644 --- a/estate_gasa/__manifest__.py +++ b/estate_gasa/__manifest__.py @@ -13,7 +13,7 @@ 'views/estate_property_offer_views.xml', 'views/estate_property_type_views.xml', 'views/estate_tag_views.xml', - 'views/estate_menus.xml', 'views/inherited_model.xml', + 'views/estate_menus.xml', ], } diff --git a/estate_gasa/models/inherited_model.py b/estate_gasa/models/inherited_model.py index 78e6108abe9..01f6e9433e7 100644 --- a/estate_gasa/models/inherited_model.py +++ b/estate_gasa/models/inherited_model.py @@ -5,8 +5,8 @@ class InheritedModel(models.Model): _inherit = "res.users" property_ids = fields.One2many( - comodel_name="estate.property", - inverse_name="seller", + "estate.property", + "seller", string="Properties", - domain=[('state', 'in', ['new', 'offer_received'])] + domain=[('state', '!=', 'cancelled')] ) diff --git a/estate_gasa/views/estate_menus.xml b/estate_gasa/views/estate_menus.xml index 0ba95a63a34..d5e5ea09055 100644 --- a/estate_gasa/views/estate_menus.xml +++ b/estate_gasa/views/estate_menus.xml @@ -4,13 +4,13 @@ - + action="estate_property_type_action" parent="estate_menu_settings" /> + parent="estate_menu_settings" action="estate_property_tag_action" /> \ No newline at end of file diff --git a/estate_gasa/views/estate_property_offer_views.xml b/estate_gasa/views/estate_property_offer_views.xml index 8f808c10fc5..83ea2bd034e 100644 --- a/estate_gasa/views/estate_property_offer_views.xml +++ b/estate_gasa/views/estate_property_offer_views.xml @@ -1,5 +1,5 @@ - + estate.property.offer.list estate.property.offer @@ -13,7 +13,7 @@ - + estate.property.offer.form estate.property.offer @@ -29,7 +29,7 @@ - + Offers estate.property.offer list,form diff --git a/estate_gasa/views/estate_property_type_views.xml b/estate_gasa/views/estate_property_type_views.xml index 4239db2b7ea..71a1ac08a9e 100644 --- a/estate_gasa/views/estate_property_type_views.xml +++ b/estate_gasa/views/estate_property_type_views.xml @@ -1,5 +1,5 @@ - + estate.property.type.form estate.property.type @@ -7,7 +7,6 @@ - @@ -21,7 +20,7 @@
-