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..07cd665019f --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,32 @@ +{ + 'name': "Real Estate", + + 'summary': """ + Real Estate Tuto" + """, + + 'description': """ + Starting tutorial real estate" + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials', + 'version': '0.1', + 'application': True, + 'depends': ['base'], + + 'data': [ + 'security/ir.model.access.csv', + 'views/users_views.xml', + 'views/estate_property_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_menus.xml', + ], + 'assets': { + + }, + 'license': 'AGPL-3' +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..a9459ed5906 --- /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 \ 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..de7b35d10d2 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,98 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_is_zero, float_compare +from dateutil.relativedelta import relativedelta + + +class Estateproperty(models.Model): + _name = "estate.property" + _description = "Estate property" + _order = "id desc" + + name = fields.Char('Title', required=True) + description = fields.Text('Description', required=True) + notes = fields.Html() + postcode = fields.Char('Postcode', required=True) + date_availability = fields.Date('Available From', copy=False, default=fields.Datetime.now() + relativedelta(months=3)) + expected_price = fields.Float('Expected Price', required=True) + selling_price = fields.Float('Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer('Bedrooms', default=2) + living_area = fields.Integer('Living Area (sqm)') + facades = fields.Integer('Facades') + garage = fields.Boolean('Garage') + garden = fields.Boolean('Garden') + garden_area = fields.Integer('Garden area (sqm)') + garden_orientation = fields.Selection( + string='Garden Orientation', + selection=[('north', 'North'), ('south', 'South'), ('west', 'West'), ('east', 'East')], + ) + active = fields.Boolean('Active', default=True) + state = fields.Selection( + string='State', + selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], + help="Estate state", + default=('new') + ) + property_type_id = fields.Many2one("estate.property.type", string="Type") + salesperson = fields.Many2one('res.users', string='Salesperson') + buyer = fields.Many2one('res.users', string='Buyer') + tag_ids = fields.Many2many('estate.property.tag', string='Tag') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offer') + total_area = fields.Float(compute="_compute_total_area") + best_offer = fields.Float(compute="_compute_best_offer") + offer_accepted = fields.Boolean() + + _expected_property_price_strictly_positive = models.Constraint( + 'CHECK(expected_price > 0)', + ) + + _property_selling_price_positive = models.Constraint( + 'CHECK(selling_price >= 0)', + ) + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends("offer_ids") + def _compute_best_offer(self): + for record in self: + record.best_offer = max(record.offer_ids.mapped("price") or [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 = None + + def set_property_sold(self): + for record in self: + if record.state != 'cancelled': + record.state = 'sold' + else: + raise(UserError("Can not cancel sold property")) + + def set_property_cancelled(self): + for record in self: + if record.state != 'sold': + record.state = 'cancelled' + else: + raise(UserError("Can not cancel sold property")) + + @api.onchange('selling_price', 'expected_price') + @api.constrains('selling_price') + def _check_selling_price(self): + for record in self: + if not(float_is_zero(record.selling_price,2)) and record.selling_price < 0.9 * record.expected_price: + raise(UserError("Selling price can not be less than 90'%' of Excpected price")) + + @api.ondelete(at_uninstall=False) + def unlink(self): + for record in self: + if record.state not in {'new', 'cancelled'}: + raise(UserError("Can not delete properties at this state")) + return super.unlink() diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..014ec5a56d5 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,58 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError +from dateutil.relativedelta import relativedelta +from datetime import datetime + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Offer for the property' + _order = 'price desc' + + price = fields.Float() + status = fields.Selection( + string='Status', + selection=[('accepted', 'Accepted'), ('refused', 'Refused')], + copy=False + ) + partner_id = fields.Many2one('res.users', required=True, default=lambda self: self.env.user) + property_id = fields.Many2one('estate.property', required=True) + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline") + property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) + + _offer_price_strictly_positive = models.Constraint( + 'CHECK(price > 0)', + ) + + @api.depends("validity") + def _compute_date_deadline(self): + for record in self: + record.date_deadline = fields.Datetime.now() + relativedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + deadline = datetime(record.date_deadline.year, record.date_deadline.month, record.date_deadline.day) + record.validity = int((deadline - fields.Datetime.now()).days) + + def accept_offer(self): + for record in self: + if not record.property_id.offer_accepted: + record.status = 'accepted' + record.property_id.selling_price = record.price + record.property_id.buyer = record.partner_id + record.property_id.state = 'offer accepted' + record.property_id.offer_accepted = True + else: + raise(UserError("Can not accept more than one offer")) + + def reject_offer(self): + for record in self: + record.status = 'refused' + + @api.model + def create(self, vals_list): + if vals_list[0].get('price') < min(self.env['estate.property'].browse(vals_list[0].get('property_id')).offer_ids.mapped('price')): + raise(UserError("Can not have an offer that is less the minimum offer")) + self.env['estate.property'].browse(vals_list[0]['property_id']).state = 'offer received' + return super().create(vals_list) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..d7da9553776 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Tags for the estate" + _order = 'name' + + name = fields.Char('Tag', required=True) + description = fields.Char('Description') + color = fields.Integer() + + _unique_tag = models.Constraint( + 'UNIQUE(name)', + 'Tag already exists' + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..320dd618d5b --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,24 @@ +from odoo import fields, models, api + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + _order = 'sequence, name, id' + + name = fields.Char('Type', required=True) + description = fields.Text() + 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="_computer_offers_count") + + _unique_type = models.Constraint( + 'UNIQUE(name)', + 'Property type name exists' + ) + + @api.depends("offer_ids") + def _compute_offers_count(self): + for record in self: + record.offer_count = len(record.offer_ids) \ No newline at end of file diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..e86e9d241d1 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = ['res.users'] + + property_ids = fields.One2many('estate.property', 'salesperson') diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..b26774ee0ce --- /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 +access_estate_property_tag,estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property,estate.property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,estate.property.type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_offer,estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1 +access_res_users,res.users,model_res_users,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..e8e185599d3 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..38fd4fe3f78 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,44 @@ + + + + + estate.property.offer.form + estate.property.offer + +
+ + +

+ + + + +

+
+
+
+
+
+ + + estate.property.offer.list + estate.property.offer + + + + + + +