From 57d1ae46d204c37d8f6c87213155c6832aa57f6e Mon Sep 17 00:00:00 2001 From: A-Yehia19 Date: Tue, 20 May 2025 11:52:03 +0200 Subject: [PATCH] [ADD] add estate app with empty view [ADD] added views of adding, listing properties and search filters [ADD] added 'offers' and 'other info' tabs in the property form and some new attributes added the tags and property types, buyer, amd seller models and linked them in the property model. also make view of offers list [ADD] compute fields in property models add the `total_area` and the `best_price` as computed attributes in the property modules and finish thier configuration added `validity` and `date_deadline` in the offer model as inverse computed attributes and configure them [FIX] styling issues replaced harcooded font style with bootstrap classes, and make the code follow sake_case naming [ADD] constrains to property fields and added some buttons for interactions ad constrains in the `selling_price, expected_price` attributes in the property model to be positive and the `name, property_type_id` to be unique together added consrains in the `price` attribute in the offer model to also be positive removed unused imports [ADD] interactive UI effects to the app add colors to records according to its state, and hide some buttons if according to the step the property is in. added default filter to show available properties [ADD] action button between property typers and offer [ADD] invoicing model and model inheritance inherit and add some vield to the user model make property invoicing model that create invoice after selling property [ADD] kanban view chapter 14 [FIX] warnings and style issues fixed issue in the property type action button in the form view, the manifist data order was incorrect fixed the warning of batch create in th property offer, fixed some code style issues in the code [FIX] handeled comments in the PR - modify the way of getting the best price - removed hard coded styles - remove unwanted dependencies - make code more clear --- estate/__init__.py | 1 + estate/__manifest__.py | 19 +++ estate/models/__init__.py | 5 + estate/models/estate_property.py | 102 +++++++++++++++ estate/models/estate_property_offer.py | 66 ++++++++++ estate/models/estate_property_tag.py | 10 ++ estate/models/estate_property_type.py | 18 +++ estate/models/estate_user.py | 7 ++ estate/security/ir.model.access.csv | 5 + estate/views/estate_menus.xml | 12 ++ estate/views/estate_property_tag_views.xml | 32 +++++ estate/views/estate_property_type_offer.xml | 45 +++++++ estate/views/estate_property_type_views.xml | 39 ++++++ estate/views/estate_property_views.xml | 131 ++++++++++++++++++++ estate/views/estate_user_views.xml | 15 +++ estate_account/__init__.py | 1 + estate_account/__manifest__.py | 13 ++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 26 ++++ estate_account/security/ir.model.access.csv | 1 + 20 files changed, 549 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 create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/models/estate_user.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_offer.xml create mode 100644 estate/views/estate_property_type_views.xml create mode 100644 estate/views/estate_property_views.xml create mode 100644 estate/views/estate_user_views.xml 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 estate_account/security/ir.model.access.csv diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..c54057781f3 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,19 @@ +{ + 'name': 'Real Estate', + 'version': '0.0', + 'category': 'Sale/estate', + 'summary': 'Manage your Real Estate Assets', + 'license': 'LGPL-3', + 'application': True, + 'installable': True, + 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_type_offer.xml', + 'views/estate_property_type_views.xml', + 'views/estate_user_views.xml', + 'views/estate_menus.xml', + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..089ce4cd63e --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import estate_user diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..2ae1d7d9544 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,102 @@ +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare + + +class TestModel(models.Model): + _name = 'estate.property' + _description = 'Test Estate Model' + _order = 'id desc' + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date(default=fields.Date.today() + relativedelta(months=3), copy=False) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=False, 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(string='Garden Area (sqm)') + garden_orientation = fields.Selection( + string='Garden Orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + ) + active = fields.Boolean(default=True) + state = fields.Selection( + string='State', + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + default='new', + readonly=True, + ) + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + seller_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) + buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False, readonly=True) + tag_ids = fields.Many2many('estate.property.tag', string='Tags') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + total_area = fields.Integer(compute='_compute_total_area', string='Total Area (sqm)') + best_price = fields.Float(compute='_compute_best_price') + + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must be a strictly positive value.'), + ('check_selling_price', 'CHECK(selling_price >= 0)', 'The expected price must be a positive value'), + ('unique_tag_name_and_type', 'UNIQUE(name, property_type_id)', 'The Name and the type should be unique.'), + ] + + @api.constrains('selling_price') + def _check_date_end(self): + for record in self: + if record.state == 'offer_accepted' and float_compare(record.selling_price, 0.9 * record.expected_price, 2): + raise UserError(self.env._('the seeling price must be atleast 90% of the expected price.')) + + @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.price') + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped('price')) if record.offer_ids else 0.0 + + @api.onchange('garden') + def _onchange_garden(self): + for record in self: + if record.garden: + record.garden_area = 10 + record.garden_orientation = 'north' + else: + record.garden_area = 0 + record.garden_orientation = '' + + def action_sell_property(self): + for record in self: + if record.state == 'cancelled': + raise UserError(self.env._('Cancelled properties cant be sold')) + else: + record.state = 'sold' + return True + + def action_cancel_property(self): + for record in self: + if record.state == 'sold': + raise UserError(self.env._('Sold properties cant be cancelled')) + else: + record.state = 'cancelled' + return True + + @api.model + def ondelete(self): + if self.state in ['new', 'cancelled']: + return super().ondelete() + raise UserError(self.env._('You cannot delete a property unless cancelled.')) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..7b35b839c8b --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,66 @@ +from datetime import timedelta + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class TestModel(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Model Offer' + _order = 'price desc' + + price = fields.Float(default=0.00) + status = fields.Selection(copy=False, selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) + partner_id = fields.Many2one('res.partner', required=True) + property_id = fields.Many2one('estate.property', required=True) + validity = fields.Integer(compute='_compute_validity', inverse='_inverse_validity', string='Validity (days)') + date_deadline = fields.Date(string='Deadline') + property_type_id = fields.Many2one(related='property_id.property_type_id', string='Property Type', store=True) + + _sql_constraints = [ + ('check_offer_price', 'CHECK(price > 0)', 'The offer price must be a strictly positive value.'), + ] + + @api.depends('date_deadline') + def _compute_validity(self): + for record in self: + if record.date_deadline: + record.validity = (record.date_deadline - fields.Date.today()).days + else: + record.validity = 0 + + def _inverse_validity(self): + for record in self: + record.date_deadline = fields.Date.today() + if record.validity: + record.date_deadline += timedelta(days=record.validity) + + def action_accept_offer(self): + for record in self: + if record.property_id.state in ['new', 'offer_received'] and record.status != 'refused': + record.property_id.selling_price = record.price + record.status = 'accepted' + record.property_id.state = 'offer_accepted' + else: + message = { + 'sold': 'cannot accept an offer on a sold property', + 'cancelled': 'cannot accept an offer on a cancelled property', + 'offer_accepted': 'cannot accept another offer on an accepted property', + } + raise UserError(self.env._(message[record.property_id.state])) + return True + + def action_refuse_offer(self): + for record in self: + record.status = 'refused' + return True + + @api.model_create_multi + def create(self, vals): + for val in vals: + if val['price'] <= self.env['estate.property'].browse(val['property_id']).best_price: + raise UserError(self.env._('The offer price must be greater than the best offer.')) + + if self.env['estate.property'].browse(val['property_id']).state == 'new': + self.env['estate.property'].browse(val['property_id']).state = 'offer_received' + return super().create(vals) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..a5db2bc13d6 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class TestModel(models.Model): + _name = 'estate.property.tag' + _description = 'Estate Model Tag' + _order = 'name' + + name = fields.Char(required=True) + color = fields.Integer() diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..e7d882dc9c7 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,18 @@ +from odoo import api, fields, models + + +class TestModel(models.Model): + _name = 'estate.property.type' + _description = 'Estate Model Type' + _order = 'name' + + name = fields.Char(required=True) + property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties') + sequence = fields.Integer('Sequence', default=1) + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers') + offer_count = fields.Integer(compute='_compute_offer_count', string='Offers Count') + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/estate_user.py b/estate/models/estate_user.py new file mode 100644 index 00000000000..69af36c719d --- /dev/null +++ b/estate/models/estate_user.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class TestModel(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many('estate.property', 'seller_id', string='Properties') diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..49bca99cac8 --- /dev/null +++ b/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_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..9725a186cb6 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..ca5c0b15f34 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,32 @@ + + + + estate.property.tag.view.form + estate.property.tag + +
+ + + + + +
+
+
+ + + estate.property.tag.view.list + estate.property.tag + + + + + + + + + Property Tags + estate.property.tag + list,form + +
diff --git a/estate/views/estate_property_type_offer.xml b/estate/views/estate_property_type_offer.xml new file mode 100644 index 00000000000..164627cc193 --- /dev/null +++ b/estate/views/estate_property_type_offer.xml @@ -0,0 +1,45 @@ + + + + estate.property.offer.view.list + estate.property.offer + + + + + + +