diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..d6210b1285d --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..6cbfd5d0376 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,20 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Estate', + 'depends': [ + 'base', + ], + 'version': '19.0.0.0', + 'installable': True, + 'application': True, + 'data': [ + 'views/res_users_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', + 'security/ir.model.access.csv', + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..3ced267895e --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,9 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import ( + estate_property, + estate_property_offer, + estate_property_tag, + estate_property_type, + res_users, +) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..8bda1b7db24 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,86 @@ +from datetime import date, timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class Property(models.Model): + _name = "estate.property" + _description = "Test description for estate.property model" + _order = "id DESC" + + name = fields.Char(required=True) + expected_price = fields.Float(required=True) + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + state = fields.Selection( + selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")], + default="new", + ) + description = fields.Text() + postcode = fields.Char() + selling_price = fields.Float(copy=False, readonly=True) + date_availability = fields.Date(copy=False, default=lambda: date.today() + timedelta(days=90)) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string='Orientation', + selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")]) + active = fields.Boolean(default=True) + buyer_id = fields.Many2one("res.partner", string="Buyer") + salesperson_id = fields.Many2one("res.users", string="Salesperson", copy=False, default=lambda self: self.env.user) + tags_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + total_area = fields.Float(compute="_compute_total_area") + best_price = fields.Float(compute="_compute_best_price") + + _check_expected_price = models.Constraint( + "CHECK(expected_price > 0)", + "The expected price must be strictly positive", + ) + _check_selling_price = models.Constraint( + "CHECK (selling_price >= 0)", + "The selling price must be positive", + ) + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids") + def _compute_best_price(self): + for record in self: + record.best_price = max(o.price for o in record.offer_ids) if record.offer_ids else 0 + + @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 + + @api.ondelete(at_uninstall=False) + def _prevent_deletion_unless_new_or_cancelled(self): + for record in self: + if record.state not in ['new', 'cancelled']: + raise UserError(_("A property can only be deleted if its state is 'New' or 'Cancelled'")) + + def action_property_cancel(self): + for record in self: + if record.state == "sold": + raise UserError(_("A sold property cannot be cancelled.")) + record.state = "cancelled" + return True + + def action_property_sold(self): + for record in self: + if record.state == "cancelled": + raise UserError(_("A cancelled property cannot be sold.")) + record.state = "sold" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..a88388b3d52 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,64 @@ +from datetime import datetime, timedelta + +from odoo import _, api, exceptions, fields, models +from odoo.tools.float_utils import float_compare + + +class PropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Test description for estate.property.offer model" + _order = "price DESC" + + price = fields.Float() + status = fields.Selection( + string="Offer Status", + copy=False, + selection=[("accepted", "Accepted"), ("refused", "Refused")]) + partner_id = fields.Many2one("res.partner", required=True) + property_id = fields.Many2one("estate.property", required=True) + validity = fields.Integer(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) + + _check_price = models.Constraint( + "CHECK (price > 0)", + "The price must be strictly positive", + ) + + @api.depends("validity") + def _compute_date_deadline(self): + for record in self: + # record.create_date is "falsy" so if checking with `record.create_date if hasattr(record.create_date) else datetime.today()` then it's true because it hasattr but it's None so it's converted to false + record.date_deadline = ((record.create_date or datetime.today()) + timedelta(days=record.validity)).date() + + def _inverse_date_deadline(self): + for record in self: + record.validity = (record.date_deadline - record.create_date.date()).days + + @api.constrains("price") + def _check_selling_price_90_percent(self): + for record in self: + if float_compare(record.price, 0.9 * record.property_id.expected_price, precision_digits=2) == -1: + raise exceptions.UserError(_("The selling price cannot be lower than 90% of the expected price")) + + @api.depends("property_id", "property_id.offer_ids") + def action_offer_accept(self): + for record in self: + if any(o.status == "accepted" for o in record.property_id.offer_ids): + raise exceptions.UserError(_("Cannot accept more than one offer")) + if float_compare(record.price, 0.9 * record.property_id.expected_price, precision_digits=2) == -1: + raise exceptions.UserError(_("The selling price cannot be lower than 90% of the expected price")) + record.status = "accepted" + record.property_id.buyer_id = record.partner_id + record.property_id.state = 'offer_accepted' + record.property_id.selling_price = record.price + + @api.model_create_multi + def create(self, vals_list): + for record in vals_list: + property = self.env['estate.property'].browse(record['property_id']) + property.state = 'offer_received' + return super().create(vals_list) + + def action_offer_refuse(self): + self.status = "refused" # assigns the same value to all the records diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..04fb1174f93 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class PropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Test description for estate.property.tag model" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer() + + _check_name = models.Constraint( + "UNIQUE (name)", + "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..c5094ed90f2 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,23 @@ +from odoo import api, fields, models + + +class PropertyType(models.Model): + _name = "estate.property.type" + _description = "Test description for estate.property.type model" + _order = "name" + + name = fields.Char(required=True) + sequence = fields.Integer('Sequence', default=1, help="Used to order types.") + property_ids = fields.One2many("estate.property", "property_type_id", string="Properties") + offers_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers") + offer_count = fields.Integer(compute="_compute_offer_count", default=0) + + _check_name = models.Constraint( + "UNIQUE (name)", + "Property type name must be unique", + ) + + @api.depends("offers_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offers_ids or []) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..59bb967773d --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many("estate.property", "salesperson_id", string="Properties") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..89f97c50842 --- /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_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,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..d876331c341 --- /dev/null +++ b/estate/views/estate_menus.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..43b75a08428 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,36 @@ + + + + estate.property.offer.view.list + estate.property.offer + + + + + +