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..4de8b7cd357 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,23 @@ +{ + 'name': "A Real Estade Advestisement Demo", + 'version': '1.0', + 'depends': ['base'], + 'author': "Odoo S.A.", + 'category': 'Sales', + 'description': """ + Test module for managing real estade advertisement + created as part of the onboarding. + """, + 'license': 'LGPL-3', + # data files always loaded at installation + 'data': [ + 'security/ir.model.access.csv', + + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_views.xml', + + 'views/estate_menus.xml', + ], +} 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..72fafd7459e --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,117 @@ +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError, ValidationError + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate property" + _order = "id desc" + + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + + buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) + salesperson_id = fields.Many2one('res.users', string='Saleswoman', 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='Offers') + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + 'Availale From', + copy=False, + default=fields.Date.add(fields.Date.today(), months=3)) + expected_price = fields.Float(required=True) + _check_expected_price = models.Constraint( + 'check (expected_price > 0)', + 'The expected price can not be negative or zero') + selling_price = fields.Float(readonly=True, copy=False) + _check_selling_price = models.Constraint( + 'check (selling_price >= 0)', + 'The selling price can not be negative, it can be zero though') + bedrooms = fields.Integer(default=2) + living_area = fields.Integer('Living area (m²)') + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer('Garden area (m²)') + garden_orientation = fields.Selection( + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + help="Garden orientation in respect to main compass directions") + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled')], + default='new', + help="State of the estate property") + + total_area = fields.Integer( + 'Total area (m²)', + compute='_compute_total_area', + help='Total area of the estate defined as a sum of living and garden area') + + best_price = fields.Float( + 'Best Offer', + compute='_compute_best_price', + help='Best offer from the availale offers or zero') + + @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): + if len(self.offer_ids) == 0: + for record in self: + record.best_price = 0.0 + else: + for record in self: + record.best_price = min([offer.price for offer in record.offer_ids]) + + @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 = None + + def action_set_property_as_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError(_("Estate property can not be marked as sold if it was cancelled")) + + record.state = "sold" + + return True + + def action_cancel_property(self): + for record in self: + if record.state == 'sold': + raise UserError(_("Estate property can not be marked as cancelled if it was already sold")) + + record.state = "cancelled" + + return True + + @api.constrains('selling_price', 'expected_price') + def check_selling_price_in_respect_to_expected_price(self): + for record in self: + if not tools.float_is_zero(record.selling_price, 8)\ + and record.selling_price < (record.expected_price * 0.9): + raise ValidationError(_("Selling price can not be lower than 90% of the expected price")) + + @api.ondelete(at_uninstall=False) + def _unlink_if_state_is_new_or_cancelled(self): + for record in self: + if record.state not in ('new', 'cancelled'): + raise UserError(_("You shall not delete estate properties which are not new or cancelled")) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..09e6e363c1b --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,74 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate property offer" + _order = "price desc" + + price = fields.Float() + _check_price = models.Constraint( + 'check (price > 0)', + 'The offer price can not be negative or zero') + status = fields.Selection( + selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused')], + copy=False, + help="State of the estate property offer") + 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( + 'estate.property.type', + related='property_id.property_type_id', + store=True) + + validity = fields.Integer('Validity (days)', default=7) + date_deadline = fields.Date( + 'Deadline', + compute='_compute_date_deadline', + inverse='_onchange_date_deadline', + help='Deadline defined as date from creation separated by validy dates') + + @api.depends('validity') + def _compute_date_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = fields.Date.add(record.create_date, days=record.validity) + else: + record.date_deadline = fields.Date.add(fields.Datetime.now(), days=record.validity) + + @api.onchange('date_deadline') + def _onchange_date_deadline(self): + for record in self: + if record.create_date: + record.validity = (record.date_deadline - record.create_date.date()).days + + def action_accept_offer(self): + for record in self: + if any(o.status == 'accepted' for o in record.property_id.offer_ids): + raise UserError(_("One does not simply accept multiple offers on the property")) + + record.status = 'accepted' + record.property_id.buyer_id = record.partner_id + record.property_id.selling_price = record.price + + def action_refuse_offer(self): + for record in self: + record.status = 'refused' + + def action_reset_status(self): + for record in self: + record.status = None + + @api.model + def create(self, vals_list): + for vals in vals_list: + estate_property = self.env['estate.property'].browse(vals['property_id']) + + if vals['price'] < estate_property.best_price: + raise UserError(f"You can not create an offer with lower price than est offer: {estate_property.best_price}") + + estate_property.state = 'offer_received'; + + 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..a37727c3d3c --- /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 = "Estate property tag, i.e. simple boolean characteristics of the estate outside the normal properties" + _order = "name" + + name = fields.Char(required=True) + _check_name = models.Constraint( + 'unique (name)', + 'The property tag name must be unique, choose different name') + visual_code = fields.Char(required=True, help="Visual character for purpose of brief visualization") + _check_visual_code = models.Constraint( + 'unique (visual_code)', + 'The property tag visual code must be unique, choose different code') + color = fields.Integer(default=0, help="Colour of the tag") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..190c105b54b --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,31 @@ +from odoo import api, fields, models + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate property type, i.e. type of the building accourding to the use" + _order = "sequence, name" + + property_ids = fields.One2many('estate.property', 'property_type_id', string='Property') + offer_ids = fields.One2many( + 'estate.property.offer', + 'property_type_id') + + name = fields.Char(required=True) + _check_name = models.Constraint( + 'unique (name)', + 'The property type name must be unique, choose different name') + description = fields.Text(help="Description of thus estate property type for better user understanding") + code = fields.Char(required=True, help="Single to double character code for identification when space is scarce") + _check_code = models.Constraint( + 'unique (code)', + 'The property type code must be unique, choose different code') + sequence = fields.Integer(default=1, help="Ordering sequence, ower is first") + offer_count = fields.Integer( + 'Amount of offers', + compute='_compute_offer_count', + help='Computed field with amount of the offers related to this type') + + @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/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..c525fe2cc7d --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError, ValidationError + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'estate.property', + 'salesperson_id', + string='Available properties') diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..0c0b62b7fee --- /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 +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_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..438c585b6f4 --- /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..d17b5280936 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,94 @@ + + + + Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + +

+ Create a new estate property offer. +

+ The estate properties offers are bids for estate buy. +

+ ... go and create some 🔏 +

+
+
+ + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + estate.property.offer.search + estate.property.offer + + + + + + + + + + + + + + estate.property.offer.form + estate.property.offer + +
+
+
+ + + + + + + + + +
+
+
+ +
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..1aa324b834c --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,71 @@ + + + + Property Tags + estate.property.tag + list,form + +

+ Create a new estate property tag. +

+ The estate properties tags characterize estates in additional ways. +

+ ... go and create some 🐈‍⬛ +

+
+
+ + + estate.property.tag.list + estate.property.tag + + + + + + + + + + + estate.property.tag.search + estate.property.tag + + + + + + + + + + + + estate.property.tag.form + estate.property.tag + +
+ +
+

+
+ + + +
+
+
+
+ +
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..28159d21b44 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,93 @@ + + + + Property Types + estate.property.type + list,form + +

+ Create a new estate property type. +

+ The estate properties types which can be sold and followed by this application. +

+ ... go and create some 🏘️ +

+
+
+ + + estate.property.type.list + estate.property.type + + + + + + + + + + + + estate.property.type.search + estate.property.type + + + + + + + + + + + + + estate.property.type.form + estate.property.type + +
+ +
+ +
+ +
+

+
+ + + + + + + + + + + + + + + +
+
+
+
+ +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..9b86bdc2f88 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,223 @@ + + + + Properties + estate.property + list,form + + { + 'search_default_available': True + } + + +

+ Create a new estate property. +

+ The estate properties can be sold and followed by this application. +

+ ... go hunting for some open oportunities 🦌 +

+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+
+ + +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +