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..fd9114a636e --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,19 @@ +{ + 'name': 'Real Estate', + 'version': '1.0', + 'category': 'Real Estate', + 'depends': ['base'], + 'author': 'snrav-odoo', + 'license': 'LGPL-3', + 'description': 'Real estate purchase & sales', + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/res_user_views.xml', + 'views/estate_property_menu.xml' + ], + 'application': True, +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9a2189b6382 --- /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 res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..3c10e64ed70 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,108 @@ +from dateutil.relativedelta import relativedelta + +from odoo import models, api, fields, exceptions +from odoo.exceptions import ValidationError + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + copy=False, + default=lambda self: fields.Date.context_today(self) + relativedelta(months=3), + ) + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer(string="Garden Area (sqft)") + garden_orientation = fields.Selection([ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ]) + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + string="Status", + default="new", + ) + active = fields.Boolean(default=True) + property_type_id = fields.Many2one("estate.property.type", string="Property Type", required=True) + customer = fields.Many2one("res.partner", string="Customer", copy=False) + salesperson = fields.Many2one( + "res.users", string="Salesperson", default=lambda self: self.env.user + ) + tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offer") + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Integer(compute="_compute_best_price") + + _check_expected_price = models.Constraint( + "CHECK(expected_price > 0)", "Expected price must be strictly positive." + ) + + _check_selling_price = models.Constraint( + "CHECK(selling_price >= 0)", "Selling price must be positive." + ) + + @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: + if not record.mapped("offer_ids.price"): + record.best_price = 0 + else: + record.best_price = max(record.mapped("offer_ids.price")) + + @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 = None + + def action_sold_property(self): + for record in self: + if record.state == "cancelled": + raise exceptions.UserError("Properties which are Cancelled cannot be Sold") + else: + record.state = "sold" + + def action_cancel_offer(self): + for record in self: + if record.state == "sold": + raise exceptions.UserError("Properties which are Sold cannot be Cancelled") + else: + record.state = "cancelled" + + @api.constrains("selling_price", "expected_price") + def _check_selling_price_percentage_criteria(self): + for record in self: + selling_price_percentage = (record.selling_price / record.expected_price) * 100 + if selling_price_percentage >= 90 or selling_price_percentage == 0: + pass + else: + raise ValidationError( + "The selling price cannot be lower then 90% of the expected price." + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..8e0bf59dc96 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,63 @@ +from dateutil.relativedelta import relativedelta + +from odoo import models, api, fields +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + selection=[("accepted", "Accepted"), ("refused", "Refused")], copy=False + ) + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date(compute="_compute_deadline", inverse="_inverse_date") + + _check_price = models.Constraint( + "CHECK(price > 0)", "Price of an offer must be positive" + ) + + @api.depends("validity") + def _compute_deadline(self): + for record in self: + base_date = record.create_date or fields.Date.today() + record.date_deadline = base_date + relativedelta(days=record.validity) + + def _inverse_date(self): + for record in self: + base_date = record.create_date or fields.Date.today() + record.validity = (record.date_deadline - fields.Date.to_date(base_date)).days + + def action_accept(self): + for record in self: + if record.property_id.customer: + raise UserError("Only one offer can be accepted for one property") + record.status = "accepted" + record.property_id.state = "offer_accepted" + record.property_id.selling_price = record.price + record.property_id.customer = record.partner_id + + def action_refuse(self): + for record in self: + record.status = "refused" + record.property_id.selling_price = 0.00 + record.property_id.customer = None + + @api.model + def create(self, vals): + if len(vals) > 0: + property = self.env["estate.property"].browse(vals[0]["property_id"]) + for record in vals: + if property.state == "new": + property.state = "offer_received" + if record["price"] < property.best_price: + raise UserError( + "Offer with an amount lower than an existing offer cannot be created." + ) + 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..8ae4622493b --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class EstatePropertyTags(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer() + _check_tag_name = models.Constraint( + "UNIQUE(name)", "Property tag should be unique." + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..21b5b63b6be --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,22 @@ +from odoo import models, api, fields + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + _order = "sequence,name" + + name = fields.Char(required=True) + property_ids = fields.One2many("estate.property", "property_type_id") + sequence = fields.Integer("Sequence", default=1) + offer_ids = fields.One2many("estate.property.offer", "property_type_id") + offer_count = fields.Integer(compute="_compute_offer_count") + + _check_type_name = models.Constraint( + "UNIQUE(name)", "Property type should be unique." + ) + + @api.depends("offer_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.property_ids.mapped('offer_ids')) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..278040f3889 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResUser(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesperson", + domain="[('state', '!=', 'sold')]" + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..db5d5c3ef4f --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,6 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +estate.access_estate_property,"access_estate_property",estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,"access_estate_property_type",estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,"access_estate_property_tag",estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,"access_estate_property_offer",estate.model_estate_property_offer,base.group_user,1,1,1,1 + diff --git a/estate/views/estate_property_menu.xml b/estate/views/estate_property_menu.xml new file mode 100644 index 00000000000..4f050d2b83f --- /dev/null +++ b/estate/views/estate_property_menu.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..e996af41143 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,42 @@ + + + + Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + estate.property.offer.list + estate.property.offer + + + + + + +