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..c3be819db08 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,26 @@ +{ + 'name': 'Real Estate', + 'version': '1.0', + 'depends': ['base'], + 'author': 'viwar-odoo', + 'category': 'real estate', + 'description': "real estate App.", + "data": [ + "security/security_groups.xml", + "security/ir.model.access.csv", + "views/estate_maintainance_form.xml", + "views/estate_property_visit_calender.xml", + "views/estate_property_visit_kanban_view.xml", + "views/estate_property_offer_view.xml", + "views/estate_property_tag_view.xml", + "views/estate_property_type_view.xml", + "views/estate_property_visit_action.xml", + "views/estate_property_view.xml", + "views/estate_menus.xml", + # "data/estate_property_demo.xml" + ], + 'application': True, + 'installable': True, + 'license': 'LGPL-3', + 'website': 'https://odoo.com', +} diff --git a/estate/data/estate_property_demo.xml b/estate/data/estate_property_demo.xml new file mode 100644 index 00000000000..3e60cc1424a --- /dev/null +++ b/estate/data/estate_property_demo.xml @@ -0,0 +1,31 @@ + + + + + + + 1 + tag1 + + + + 1 + type1 + + + + 1 + property + + 1 + + + + 1 + 100000 + 1 + + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..64e711efa88 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,6 @@ +from . import estate_maintainance_request +from . import estate_property +from . import estate_property_offer +from . import estate_property_tag +from . import estate_property_type +from . import estate_property_visit diff --git a/estate/models/estate_maintainance_request.py b/estate/models/estate_maintainance_request.py new file mode 100644 index 00000000000..4b8f11ec6b2 --- /dev/null +++ b/estate/models/estate_maintainance_request.py @@ -0,0 +1,37 @@ +from odoo import api, models, fields + + +class EstateMaintainanceRequest(models.Model): + _name = 'estate.maintainance.request' + _description = 'table for the technician maintainance request' + + property_id = fields.Many2one('estate.property', required=True, readonly=True) + buyer_id = fields.Many2one('res.users', required=True, default='self.env.uid') + technician_id = fields.Many2one('res.partner', required=False) + state = fields.Selection( + string='state', + default='new', + selection=[ + ('new', "New"), + ('assigned', "Assigned"), + ('inprogress', "Inprogress"), + ('done', "Done"), + ('cancelled', "Cancelled"), + ], + ) + estimate_cost = fields.Float(required=False) + actual_cost = fields.Float(compute='_compute_actual_cost', store=True) + current_stage = fields.Char(compute='_compute_state_after_assigned', store=True) + + @api.depends('state', 'estimate_cost') + def _compute_actual_cost(self): + if self.state == 'done': + self.actual_cost = self.estimate_cost * 1.18 + + @api.depends('technician_id', 'state') + def _compute_state_after_assigned(self): + if self.state == 'new' and self.technician_id: + self.state = 'assigned' + self.current_stage = 'assigned' + else: + self.current_stage = self.state diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..0a1324011ca --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,151 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Test Model for real estate' + _check_positive_expected_price = models.Constraint( + 'CHECK (expected_price >= 0)', 'expected_price should be positive' + ) + _check_positive_selling_price = models.Constraint( + 'CHECK (selling_price >= 0)', 'selling_price should be positive' + ) + _order = 'id desc' + + name = fields.Char(default='Unknown') + last_seen = fields.Datetime('Last Seen', default=fields.Datetime.now) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + copy=False, default=fields.Date.add(fields.Date.today(), months=3) + ) + expected_price = fields.Float() + selling_price = fields.Float(copy=False, readonly=True, default=0) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + state = fields.Selection( + string='status', + selection=[ + ('new', "New"), + ('offer received', "Offer Received"), + ('offer accepted', "Offer Accepted"), + ('sold', "Sold"), + ('cancelled', "Cancelled"), + ], + default='new', + compute='_compute_statusbar', + store=True, + ) + active = fields.Boolean(default=True) + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string='garden orientation direction', + selection=[ + ('north', "North"), + ('south', "South"), + ('east', "East"), + ('west', "West"), + ], + ) + property_type_id = fields.Many2one('estate.property.type') + salesperson_id = fields.Many2one( + 'res.users', + string='Salesperson', + index=True, + default=lambda self: self.env.user, + ) + buyer_id = fields.Many2one('res.partner', default='None', copy=False) + tag_ids = fields.Many2many('estate.property.tag') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='offers') + total_area = fields.Float(compute='_compute_total_area') + max_offer_price = fields.Float( + default=None, compute='_compute_max_offer_price', store=True + ) + estate_maintainance_id = fields.One2many( + 'estate.maintainance.request', 'property_id' + ) + visit_ids = fields.One2many('estate.property.visit', 'property_id', string='visits') + + @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.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = None + self.garden_orientation = None + + def button_cancel(self): + if self.state == 'cancelled': + raise UserError('The property is already cancelled') + elif self.state == 'sold': + raise UserError('The property is already sold, you cannot cancel it') + else: + self.state = 'cancelled' + + def button_sold(self): + if self.state == 'sold': + raise UserError('The property is already sold') + elif self.state == 'cancelled': + raise UserError('The property is already cancelled, and cannot be sold') + else: + self.state = 'sold' + + @api.depends('offer_ids.price') + def _compute_max_offer_price(self): + if self.offer_ids: + new_max_offer_price = max(self.offer_ids.mapped('price')) + if self.max_offer_price != new_max_offer_price: + self.max_offer_price = new_max_offer_price + + def accept_best_offer(self): + for offers in self.offer_ids: + if self.max_offer_price == offers.price: + offers.action_accept_offer() + ################################## + # old code + # offers.status = 'accepted' + # self.state = 'offer accepted' + # self.buyer_id = offers.partner_id + # self.selling_price = offers.price + ################################## + else: + offers.status = 'refused' + + @api.depends('offer_ids') + def _compute_statusbar(self): + for record in self: + if record.offer_ids and record.state == 'new': + record.state = 'offer received' + + @api.constrains('selling_price', 'expected_price') + def check_percentage(self): + for record in self: + if record.selling_price and record.expected_price: + if ( + float_compare( + record.selling_price, + record.expected_price * 0.9, + precision_digits=1, + ) + < 0 + ): + raise ValidationError( + 'the selling perice cant be less than 90% of the expected price' + ) + + @api.ondelete(at_uninstall=False) + def _unlink_if_user_inactive(self): + for record in self: + if record.state not in ['cancelled', 'new']: + raise UserError('cannot delete - only delete from the state `new` and `cancelled`') diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..8a198117945 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,87 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'estate property offer' + _check_positive_offer_price = models.Constraint( + 'CHECK (price >= 0)', 'price should be positive' + ) + _order = 'price desc' + + price = fields.Float(copy=False) + status = fields.Selection( + copy=False, + string='status', + selection=[('accepted', "Accepted"), ('refused', "Refused")], + ) + partner_id = fields.Many2one( + 'res.partner', required=True, default=lambda self: self.env.user.partner_id.id + ) + property_id = fields.Many2one('estate.property', required=True) + validity = fields.Integer(default=7, copy=False) + deadline = fields.Date( + compute='_compute_deadline', store=True, inverse='_inverse_deadline' + ) + + property_type_id = fields.Many2one( + 'estate.property.type', related='property_id.property_type_id', store=True + ) + + @api.depends('validity', 'deadline') + def _compute_deadline(self): + for record in self: + record.deadline = fields.Date.add(fields.Date.today(), days=record.validity) + + def _inverse_deadline(self): + for record in self: + today_date = fields.Date.today() + record.validity = (record.deadline - today_date).days + + def action_accept_offer(self): + for offer in self: + for offer.property_id in offer.property_id: + if ( + offer.property_id.state == 'offer accepted' + or offer.property_id.state == 'sold' + ): + raise UserError( + 'An offer has already been accepted for this property.' + ) + else: + offer.status = 'accepted' + offer.property_id.state = 'offer accepted' + offer.property_id.buyer_id = offer.partner_id + offer.property_id.selling_price = offer.price + for offers in self.property_id.offer_ids: + if offers != self: + offers.status = 'refused' + + def action_reject_offer(self): + for offer in self: + if offer.status == 'accepted': + if offer.property_id.state == 'sold': + raise UserError( + 'the property is sold. CANT REJECT THE OFFER - THANK YOU' + ) + else: + offer.status = 'refused' + offer.property_id.state = 'offer received' + offer.property_id.buyer_id = False + offer.property_id.selling_price = 0 + elif offer.status == 'refused': + raise UserError('This offer has already been refused.') + else: + offer.status = 'refused' + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + property_id = self.env['estate.property'].browse(vals.get('property_id')) + if property_id.offer_ids: + max_offer_price = max(property_id.offer_ids.mapped('price')) + if vals.get('price', 0) <= max_offer_price: + raise UserError("The offer price must be higher than the current highest offer (%s)." % max_offer_price) + 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..3c9d5fe31cb --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class EstatePropertytTag(models.Model): + _name = 'estate.property.tag' + _description = 'Estate_property_tag' + _unique_tag = models.UniqueIndex('(name)', 'The name of the tag must be unique') + _order = 'name asc' + + 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..faa784b7ed1 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,21 @@ +from odoo import fields, models, api + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'Estate_property_type' + _unique_type = models.UniqueIndex('(name)', 'property type should be unique') + _order = 'sequence' + + name = fields.Char(required=True) + sequence = fields.Integer( + default=1, help='Used to order the property types. Lower is better.' + ) + offer_ids = fields.One2many('estate.property.offer', 'property_type_id') + offer_count = fields.Integer(default=0, compute='_compute_total_count', store=True) + property_ids = fields.One2many('estate.property', 'property_type_id') + + @api.depends('offer_ids') + def _compute_total_count(self): + for rec in self: + rec.offer_count = len(rec.offer_ids.mapped('id')) diff --git a/estate/models/estate_property_visit.py b/estate/models/estate_property_visit.py new file mode 100644 index 00000000000..07ac4046892 --- /dev/null +++ b/estate/models/estate_property_visit.py @@ -0,0 +1,35 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class EstatePropertyVisit(models.Model): + _name = 'estate.property.visit' + _description = 'estate property visit' + _order = 'date desc' + _sql_const = models.UniqueIndex( + '(date, property_id)', + 'This property is already booked for a visit on this date!', + ) + + property_id = fields.Many2one('estate.property', required=True) + visitor_id = fields.Many2one('res.partner', required=True) + date = fields.Date(required=True) + comment = fields.Text() + state = fields.Selection( + string='status', + selection=[ + ('new', "New"), + ('scheduled', "Scheduled"), + ('done', "Done"), + ('cancelled', "Cancelled"), + ], + default='new', + ) + + @api.constrains('date') + def _compute_date_clash(self): + for rec in self: + if rec.date < fields.Date.today(): + raise ValidationError('The visit date cannot be in the past.') + if rec.date: + rec.state = 'scheduled' diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..5894250aa78 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,25 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property_admin,estate_property_admin,model_estate_property,estate.property_admin,1,1,1,1 +access_estate_maintainance_request,access_estate_maintainance_request,model_estate_maintainance_request,estate.property_admin,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,estate.property_admin,1,1,1,1 +access_estate_property_visit,access_estate_property_visit,model_estate_property_visit,estate.property_admin,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,estate.property_admin,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,estate.property_admin,1,1,1,1 +access_estate_property_agent,estate_property_agent,model_estate_property,estate.property_agent,1,1,1,0 +access_estate_property_offer_agent,estate_property_offer_agent,model_estate_property_offer,estate.property_agent,1,1,1,0 +access_estate_maintainance_request_agent,access_estate_maintainance_request_agent,model_estate_maintainance_request,estate.property_agent,1,0,0,0 +access_estate_property_visit_agent,access_estate_property_visit_agent,model_estate_property_visit,estate.property_agent,1,0,0,0 +access_estate_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate.property_agent,1,1,1,0 +access_estate_property_tag_agent,access_estate_property_tag_agent,model_estate_property_tag,estate.property_agent,1,1,1,0 +access_estate_property_manager,estate_property_manager,model_estate_property,estate.property_manager,1,1,1,1 +access_estate_property_offer_manager,estate_property_offer_manager,model_estate_property_offer,estate.property_manager,1,1,1,1 +access_estate_maintainance_request_manager,access_estate_maintainance_request_manager,model_estate_maintainance_request,estate.property_manager,1,0,0,0 +access_estate_property_visit_manager,access_estate_property_visit_manager,model_estate_property_visit,estate.property_manager,1,1,1,1 +access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate.property_manager,1,1,1,1 +access_estate_property_tag_manager,access_estate_property_tag_manager,model_estate_property_tag,estate.property_manager,1,1,1,1 +access_estate_property_maintainance_staff,estate_property_maintainance_staff,model_estate_property,estate.property_maintainance_staff,1,1,0,0 +access_estate_property_offer_maintainance_staff,estate_property_offer_maintainance_staff,model_estate_property_offer,estate.property_maintainance_staff,1,0,0,0 +access_estate_maintainance_request_maintainance_staff,access_estate_maintainance_request_maintainance_staff,model_estate_maintainance_request,estate.property_maintainance_staff,1,1,1,1 +access_estate_property_visit_maintainance_staff,access_estate_property_visit_maintainance_staff,model_estate_property_visit,estate.property_maintainance_staff,1,0,0,0 +access_estate_property_type_maintainance_staff,access_estate_property_type_maintainance_staff,model_estate_property_type,estate.property_maintainance_staff,1,0,0,0 +access_estate_property_tag_maintainance_staff,access_estate_property_tag_maintainance_staff,model_estate_property_tag,estate.property_maintainance_staff,1,0,0,0 diff --git a/estate/security/security_groups.xml b/estate/security/security_groups.xml new file mode 100644 index 00000000000..27c9063452e --- /dev/null +++ b/estate/security/security_groups.xml @@ -0,0 +1,26 @@ + + + + Real Estate + + + Admin + + + + + Agent + + + + + Manager + + + + + Maintainance Staff + + + + diff --git a/estate/views/estate_maintainance_form.xml b/estate/views/estate_maintainance_form.xml new file mode 100644 index 00000000000..d4a04b0f56d --- /dev/null +++ b/estate/views/estate_maintainance_form.xml @@ -0,0 +1,42 @@ + + + + estate_maintainance_action + estate.maintainance.request + list,form + + + estate.maintainance.type.list.view + estate.maintainance.request + + + + + + + + + + + + + + estate.maintainance.type.form.view + estate.maintainance.request + +
+ + + + + + + + + + + +
+
+
+
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..b1d01b028b9 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml new file mode 100644 index 00000000000..683c13da3ac --- /dev/null +++ b/estate/views/estate_property_offer_view.xml @@ -0,0 +1,43 @@ + + + + estate_property_offer + estate.property.offer + list,form + + + offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + estate.property.offer.list.view + estate.property.offer + + + + + + + + + + + estate.property.offer.form.view + estate.property.offer + +
+ + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_tag_view.xml b/estate/views/estate_property_tag_view.xml new file mode 100644 index 00000000000..1fa40197226 --- /dev/null +++ b/estate/views/estate_property_tag_view.xml @@ -0,0 +1,30 @@ + + + + estate_property_tag + estate.property.tag + list,form + + + estate.property.tag.list.view + estate.property.tag + + + + + + + + estate.property.tag.form.view + estate.property.tag + +
+ + + + + +
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_type_view.xml b/estate/views/estate_property_type_view.xml new file mode 100644 index 00000000000..420c9f7b4d6 --- /dev/null +++ b/estate/views/estate_property_type_view.xml @@ -0,0 +1,51 @@ + + + + estate_property_type_action + estate.property.type + list,form + + + estate.property.type.tree.view + estate.property.type + + + + + + + + + + estate.property.type.form.view + estate.property.type + +
+ + +
+ +
+ + + + + + + + +
+
+
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_view.xml b/estate/views/estate_property_view.xml new file mode 100644 index 00000000000..7e101a701af --- /dev/null +++ b/estate/views/estate_property_view.xml @@ -0,0 +1,168 @@ + + + + Real-estate Property"s + estate.property + {'search_default_available': 1} + kanban,list,form + + + estate.property.list.view + estate.property + + + + + + + + + + + + + + + estate.property.form.view + estate.property + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +