diff --git a/.gitignore b/.gitignore index b6e47617de1..1b77d315ad1 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,4 @@ dmypy.json # Pyre type checker .pyre/ +.vscode/settings.json 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..03b5d0ec870 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': "Estate", + 'author': "delje", + 'license': "LGPL-3", + 'depends': ["base"], + 'application': True, + 'installable': True, + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_menus_views.xml', + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f2347942227 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,4 @@ +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..e2612888bd9 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,103 @@ +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero + + +class EstatePropertyModel(models.Model): + _name = "estate_property" + _description = "The details of a property" + + name = fields.Char('Estate Name', required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date(copy=False, default=fields.Date.today() + relativedelta(months=3)) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer(default=0) + facades = fields.Integer() + has_garage = fields.Boolean() + has_garden = fields.Boolean() + garden_area = fields.Integer(default=0) + garden_orientation = fields.Selection( + string='Garden Orientation', + selection=[ + ('east', 'East'), + ('west', 'West'), + ('north', 'North'), + ('south', 'South'), + ('', ''), + ], + ) + state = fields.Selection( + string='State', + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + default='new', + ) + active = fields.Boolean(default=True) + property_type_id = fields.Many2one("estate_property_type", string="Type") + buyer_id = fields.Many2one("res.partner", string="Buyer") + salesperson_id = fields.Many2one('res.users', string='Salesperson', index=True, default=lambda self: self.env.user) + property_tags_ids = fields.Many2many("estate_property_tag", string="Tags") + offers_ids = fields.One2many("estate_property_offer", "property_id", string="Offers") + total_area = fields.Integer(compute="_compute_total_area", readonly=True) + best_price = fields.Float(compute="_compute_best_price", string="Best Offer", readonly=True) + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends("offers_ids") + def _compute_best_price(self): + for record in self: + if record.offers_ids: + record.best_price = max(record.offers_ids.mapped('price')) + else: + record.best_price = 0 + + @api.onchange("has_garden") + def _onchange_has_garden(self): + self.garden_area = 10 if self.has_garden else 0 + self.garden_orientation = 'south' if self.has_garden else '' + + def action_sell_property(self): + for record in self: + if record.state == 'cancelled': + UserError("Cannot sell a cancelled property") + return False + record.state = 'sold' + return True + + def action_cancel_property(self): + for record in self: + if record.state == 'sold': + UserError("Cannot cancel a sold property") + return False + record.state = 'cancelled' + return True + + _check_expected_price_constraint = models.Constraint( + 'CHECK(expected_price > 0)', + 'The price of a property must be strictly positive', + ) + + _check_selling_price_constraint = models.Constraint( + 'CHECK(selling_price >= 0)', + 'The selling price must be positive', + ) + + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for record in self: + if (not float_is_zero(record.selling_price, 2)) and float_compare(record.selling_price, record.expected_price * 0.9, 2) < 0: + error_message = "The selling price is less than 90 percent of the asked price" + raise ValidationError(error_message) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..84dd52d7c33 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,54 @@ +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + + +class EstatePropertyOfferModel(models.Model): + _name = "estate_property_offer" + _description = "An offer for an estate" + + price = fields.Float() + status = fields.Selection(string='State', + selection=[ + ('refused', 'Refused'), + ('accepted', 'Offer Accepted'), + ], + copy=False, + ) + partner_id = fields.Many2one("res.partner", string="Buyer") + property_id = fields.Many2one("estate_property", string="Property") + validity = fields.Integer(default=7) + date_deadline = fields.Date(compute="_compute_deadline", inverse="_inverse_deadline") + + @api.depends("validity") + def _compute_deadline(self): + for record in self: + if record.create: + record.date_deadline = datetime.now() + relativedelta(days=record.validity) + else: + record.date_deadline = record.create_date.date() + relativedelta(days=record.validity) + + def _inverse_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days + + @api.depends("property_id") + def action_accept_offer(self): + for record in self: + if record.status == "accepted" or record.property_id.state in ['cancelled', 'sold', 'offer_accepted']: + return False + record.property_id.selling_price = record.price + record.property_id.state = 'offer_accepted' + record.property_id.buyer_id = record.partner_id + record.status = 'accepted' + return True + + @api.depends("property_id") + def action_refuse_offer(self): + for record in self: + if record.property_id.state in ['cancelled', 'sold']: + return False + record.status = 'refused' + return True diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..02da5445235 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class EstatePropertyTagModel(models.Model): + _name = "estate_property_tag" + _description = "The tag of an estate" + + name = fields.Char(required=True) + + _unique_name = models.Constraint( + 'UNIQUE(name)', + 'A property tag name must be unique', + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..82937d98448 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class EstatePropertyTypeModel(models.Model): + _name = "estate_property_type" + _description = "The type of an estate" + + name = fields.Char(required=True) + + _unique_name = models.Constraint( + 'UNIQUE(name)', + 'A property tag name must be unique', + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..fd53e8c2af8 --- /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_model,access_estate_model,model_estate_property,base.group_user,1,1,1,1 +access_estate_type_model,access_estate_type_model,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_tag_model,access_estate_tag_model,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_offer_model,access_estate_offer_model,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus_views.xml b/estate/views/estate_menus_views.xml new file mode 100644 index 00000000000..7e77c8e2109 --- /dev/null +++ b/estate/views/estate_menus_views.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..820eff413fc --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,19 @@ + + + + + estate.property.offer.list + estate_property_offer + + + + + +