From 4c47a4bde488b8afd1d733dedfa04c67bb0340fc Mon Sep 17 00:00:00 2001 From: vikri-odoo Date: Tue, 16 Dec 2025 13:41:17 +0100 Subject: [PATCH] [ADD] new addon for real estate onboarding project [ADD] add the new model for property types Completed till chapter 7 many to one [LINT] format the code using ruff Added new tags model to understand many2many [REF] address the PR comments and resole it Check the style guide and resolve the issues [REF] update the default availability method to lamda [ADD] created new model for estate offers 1.create new model and view for offers 2.complete chapter 7 [ADD] chapter 9: action added for buttons 1. Sold and cancel button and actions added 2. Accept and reject button added with actions for offers [LINT] format the code to fix the ci\style issue [FIX] typo fix the string parameter to lower case for property_type in estate_property.py [ADD] constrains added for selling and expected prices expected and offer price must be strictly positive Selling price must be positive Tag name should be unique [ADD] complete the chapter 11 1.use status bar widget to show the state of the property 2. add default sorting order for the models 3. add colors for tag model 4. add invisible in the needed places as mentioned in the task 5. make the offer and tag editable in list view [REM] unused print statement removed [ADD] new estate account module created 1. As accounting is option for estate module we create new module and inherited from estate 2. created kanban view for the properties [FIX] run-bot to make it green 1. use private _ondelete method instead of using unlink in estate_property.py [REF] use "in" for multiple possible value instead of "of" [REF] use "self.env._" to make the strings translatable --- real_estate/__init__.py | 1 + real_estate/__manifest__.py | 22 +++ real_estate/data/res_estate_data.xml | 103 ++++++++++++++ real_estate/models/__init__.py | 5 + real_estate/models/estate_property.py | 122 ++++++++++++++++ real_estate/models/estate_property_offers.py | 60 ++++++++ real_estate/models/estate_property_tags.py | 12 ++ real_estate/models/estate_property_type.py | 16 +++ real_estate/models/res_users.py | 12 ++ real_estate/security/ir.model.access.csv | 5 + real_estate/views/estate_menu.xml | 7 + .../estate_property_offers_templates.xml | 22 +++ .../views/estate_property_tags_templates.xml | 17 +++ .../views/estate_property_templates.xml | 130 ++++++++++++++++++ .../views/estate_property_type_templates.xml | 42 ++++++ real_estate/views/res_users_templates.xml | 15 ++ real_estate_account/__init__.py | 1 + real_estate_account/__manifest__.py | 17 +++ real_estate_account/models/__init__.py | 1 + real_estate_account/models/estate_account.py | 29 ++++ 20 files changed, 639 insertions(+) create mode 100644 real_estate/__init__.py create mode 100644 real_estate/__manifest__.py create mode 100644 real_estate/data/res_estate_data.xml create mode 100644 real_estate/models/__init__.py 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_type.py create mode 100644 real_estate/models/res_users.py create mode 100644 real_estate/security/ir.model.access.csv create mode 100644 real_estate/views/estate_menu.xml create mode 100644 real_estate/views/estate_property_offers_templates.xml create mode 100644 real_estate/views/estate_property_tags_templates.xml create mode 100644 real_estate/views/estate_property_templates.xml create mode 100644 real_estate/views/estate_property_type_templates.xml create mode 100644 real_estate/views/res_users_templates.xml create mode 100644 real_estate_account/__init__.py create mode 100644 real_estate_account/__manifest__.py create mode 100644 real_estate_account/models/__init__.py create mode 100644 real_estate_account/models/estate_account.py diff --git a/real_estate/__init__.py b/real_estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/real_estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/real_estate/__manifest__.py b/real_estate/__manifest__.py new file mode 100644 index 00000000000..8aa2c3a3dd0 --- /dev/null +++ b/real_estate/__manifest__.py @@ -0,0 +1,22 @@ +{ + "name": "Real Estate Application", + "summary": "This is a custom real estate application for understanding Odoo", + "description": "This is a custom real estate application for understanding Odoo", + "author": "Odoo S.A.", + "website": "https://www.odoo.com", + "category": "Tutorials", + "version": "0.1", + "application": True, + "data": [ + "data/res_estate_data.xml", + "security/ir.model.access.csv", + "views/res_users_templates.xml", + "views/estate_property_templates.xml", + "views/estate_property_offers_templates.xml", + "views/estate_property_type_templates.xml", + "views/estate_property_tags_templates.xml", + "views/estate_menu.xml", + ], + "assets": {}, + "license": "AGPL-3", +} diff --git a/real_estate/data/res_estate_data.xml b/real_estate/data/res_estate_data.xml new file mode 100644 index 00000000000..53393bf5c9b --- /dev/null +++ b/real_estate/data/res_estate_data.xml @@ -0,0 +1,103 @@ + + + + + House + + + Apartment + + + Castle + + + Property 1 + Description 1 + 1000 + 10000 + 0 + True + 2 + 2 + 2 + True + True + 22 + new + north + + + + + + + + + + + + 10000.00 + + + + 20000.00 + + + + + + Property 2 + Description 2 + 2000 + 10000 + 0 + True + 2 + 2 + 2 + True + True + 22 + new + north + + + + + + + + + 10000.00 + + + + 20000.00 + + + + + + Property 3 + Description 3 + 1040 + 10000 + 0 + True + 2 + 2 + 2 + True + True + 22 + new + north + + + + + + + + + diff --git a/real_estate/models/__init__.py b/real_estate/models/__init__.py new file mode 100644 index 00000000000..0be9d8ead4c --- /dev/null +++ b/real_estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tags +from . import estate_property_offers +from . import res_users diff --git a/real_estate/models/estate_property.py b/real_estate/models/estate_property.py new file mode 100644 index 00000000000..979b239763c --- /dev/null +++ b/real_estate/models/estate_property.py @@ -0,0 +1,122 @@ +from datetime import date + +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare, float_is_zero + + +class Estate(models.Model): + _name = 'estate.property' + _description = "Real_Estate_Property" + _order = "id desc" + + name = fields.Char(string="Name", required=True) + description = fields.Text(string="Description") + postcode = fields.Char(string="Postcode") + date_availability = fields.Date( + string="Date Availability", + default=lambda self: date.today() + relativedelta(months=3), + copy=False, + ) + expected_price = fields.Float(string="Expected Price", required=True) + selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) + active = fields.Boolean(string="Active", default=True) + bedrooms = fields.Integer(string="Bedrooms", default=2) + living_area = fields.Integer(string="Living Area") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area") + state = fields.Selection( + string="State", + selection=[ + ('new', "New"), + ('offered', "Offer Received"), + ('accepted', "Offer Accepted"), + ('sold', "Sold"), + ('cancelled', "Cancelled"), + ], + required=True, + copy=False, + ) + garden_orientation = fields.Selection( + [ + ('north', "North"), + ('south', "South"), + ('east', "East"), + ('west', "West"), + ] + ) + sales_person = fields.Many2one( + 'res.users', + string="Sales Person", + copy=False, + default=lambda self: self.env.user, + ) + buyer = fields.Many2one('res.partner', string="Buyer") + property_type = fields.Many2one( + comodel_name="estate.property.type", + string="Property Type", + ) + tags = fields.Many2many('estate.property.tags', string="Tags") + offer_ids = fields.One2many('estate.property.offers', 'property_id', string="Offers") + total_area = fields.Integer(compute='_compute_total_area') + best_offer = fields.Float(compute='_compute_best_offer') + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends('offer_ids') + def _compute_best_offer(self): + for record in self: + offers = record.offer_ids.mapped('price') + if offers: + record.best_offer = max(offers) + else: + record.best_offer = 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 = 0 + + def action_set_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError(self.env._("This property cannot be sold.")) + record.state = 'sold' + return True + + def action_set_cancel(self): + for record in self: + if record.state == 'sold': + raise UserError(self.env._("This property is sold.")) + record.state = 'cancelled' + return True + + @api.constrains('expected_price') + def _check_expected_price(self): + for record in self: + if record.expected_price <= 0: + raise ValidationError(self.env._("Expected price cannot be negative.")) + + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for record in self: + if record.selling_price < 0: + raise ValidationError(self.env._("Selling price cannot be negative.")) + if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) == -1: + raise ValidationError(self.env._("The selling price cannot be lower than 90% of the expected price!")) + + @api.ondelete(at_uninstall=False) + def _ondelete(self): + for record in self: + if record.state not in ('new', 'cancelled'): + raise UserError(self.env._("This property cannot be deleted.")) diff --git a/real_estate/models/estate_property_offers.py b/real_estate/models/estate_property_offers.py new file mode 100644 index 00000000000..72f708df17f --- /dev/null +++ b/real_estate/models/estate_property_offers.py @@ -0,0 +1,60 @@ +from datetime import date + +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class EstatePropertyOffers(models.Model): + _name = "estate.property.offers" + _description = "Real Estate Property Offers" + _order = "price desc" + + price = fields.Float(string="Price") + status = fields.Selection( + string="Status", + selection=[('accepted', "Accepted"), ('rejected', "Rejected")], + copy=False, + readonly=True, + ) + partner_id = fields.Many2one(comodel_name='res.partner', required=True) + property_id = fields.Many2one(comodel_name='estate.property', required=True) + property_type_id = fields.Many2one(related='property_id.property_type', store=True) + validity = fields.Integer(string="Validity", default=7) + date_deadline = fields.Date(compute='_compute_date_deadline') + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for record in self: + start_date = record.create_date if record.create_date else date.today() + record.date_deadline = start_date + relativedelta(days=record.validity) + + def action_accept_offer(self): + for record in self: + record.status = 'accepted' + record.property_id.state = 'accepted' + record.property_id.buyer = record.partner_id + record.property_id.selling_price = record.price + return True + + def action_reject_offer(self): + for record in self: + record.status = 'rejected' + return True + + @api.constrains('price') + def _check_price(self): + for record in self: + if record.price <= 0: + raise ValidationError(self.env._("Offer price cannot be negative.")) + + @api.model_create_multi + def create(self, offer_list): + for record in offer_list: + property_id = self.env['estate.property'].browse(record['property_id']) + if property_id.offer_ids: + max_offer = max(property_id.offer_ids.mapped('price')) + if record.get('price', 0) < max_offer: + raise UserError(self.env._("The offer must be higher than existing offers!")) + property_id.state = 'offered' + return super().create(offer_list) diff --git a/real_estate/models/estate_property_tags.py b/real_estate/models/estate_property_tags.py new file mode 100644 index 00000000000..25e2136ac2d --- /dev/null +++ b/real_estate/models/estate_property_tags.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstatePropertyTags(models.Model): + _name = "estate.property.tags" + _description = "Real Estate Property Tags" + _order = "name asc" + + name = fields.Char(string="Name", required=True) + color = fields.Integer(string="Color") + + _name_uniq = models.Constraint('unique (name)', "This tag is already available") diff --git a/real_estate/models/estate_property_type.py b/real_estate/models/estate_property_type.py new file mode 100644 index 00000000000..c90384084ca --- /dev/null +++ b/real_estate/models/estate_property_type.py @@ -0,0 +1,16 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = "Real Estate Property Type" + + name = fields.Char(string="Name", required=True) + property_ids = fields.One2many('estate.property', 'property_type') + offer_ids = fields.One2many(comodel_name="estate.property.offers", inverse_name="property_type_id", string="Offers") + offer_count = fields.Integer(compute='_compute_offer_count') + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/real_estate/models/res_users.py b/real_estate/models/res_users.py new file mode 100644 index 00000000000..9aaf1e87966 --- /dev/null +++ b/real_estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'estate.property', + 'sales_person', + string="Real Estate Properties", + domain="['|', ('state', '=', 'new'), ('state', '=', 'offer_received')]", + ) diff --git a/real_estate/security/ir.model.access.csv b/real_estate/security/ir.model.access.csv new file mode 100644 index 00000000000..58710e125d0 --- /dev/null +++ b/real_estate/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access.estate.property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access.estate.property.type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tags,access.estate.property.tags,model_estate_property_tags,base.group_user,1,1,1,1 +access_estate_property_offers,access.estate.property.offers,model_estate_property_offers,base.group_user,1,1,1,1 diff --git a/real_estate/views/estate_menu.xml b/real_estate/views/estate_menu.xml new file mode 100644 index 00000000000..925907a4c07 --- /dev/null +++ b/real_estate/views/estate_menu.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/real_estate/views/estate_property_offers_templates.xml b/real_estate/views/estate_property_offers_templates.xml new file mode 100644 index 00000000000..856094faf2e --- /dev/null +++ b/real_estate/views/estate_property_offers_templates.xml @@ -0,0 +1,22 @@ + + + + Property Offers + estate.property.offers + list,form + [('property_type_id', '=', active_id)] + + + estate.property.offers.list + estate.property.offers + + + + + +