diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /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 0000000000..5f5f859fc4 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,23 @@ +# A custom real estate module, created for learning purposes + +{ + 'name': '[TUTO] Estate', + 'summary': 'Track your real estate properties', + 'depends': [ + 'base_setup' + ], + 'data': [ + 'security/ir.model.access.csv', + + 'views/estate_property_views.xml', + 'views/estate_settings_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_menu_views.xml' + ], + 'installable': True, + 'application': True, + 'demo': [ + "demo/estate_demo.xml", + ], + 'auto_install': True +} diff --git a/estate/demo/estate_demo.xml b/estate/demo/estate_demo.xml new file mode 100644 index 0000000000..389e05122f --- /dev/null +++ b/estate/demo/estate_demo.xml @@ -0,0 +1,97 @@ + + + + + + + Appartment + + + House + + + + + cosy + + + renovated + + + renovation needed + + + + + 2000000 + + 12 + + + 200000 + + 10 + + + 220000 + + 25 + + + 260000 + + + + + + + Beach House + canceled + A brand new house by the beach + 8300 + + 2500000 + 0 + 6 + 1050 + 4 + True + True + 420 + south + + + + + + + City Apt + offer_accepted + A shitty appartment in Brussels + 1090 + + 250000 + 0 + 2 + 95 + 1 + False + False + 0 + + + + + + + + + Mass cancel + + + list + code + action = records.action_cancel() + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 0000000000..a94838fc6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property_infos, estate_property, estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 0000000000..0b6d60b866 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,139 @@ +from odoo import api, exceptions, fields, models +from datetime import date +from dateutil.relativedelta import relativedelta + + +class Estate_Property(models.Model): + _name = "estate_property" + _description = "Estate properties" + _order = "id desc" + active = False + + name = fields.Char(required=True, string="Title") + + status = fields.Selection( + [ + ("new", "New"), + ("offer_received", "Offer received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled"), + ], + default="new", + readonly=True, + copy=False, + string="Sale State", + ) + + description = fields.Text(copy=False, string="Description") + + postcode = fields.Char( + help="Une adresse serait mieux mais bon...", string="Postcode" + ) + + date_availability = fields.Date( + default=(date.today() + relativedelta(months=+3)), + copy=False, + string="Available From", + ) + + expected_price = fields.Float( + default=0.0, required=True, copy=False, string="Expected Price" + ) + + selling_price = fields.Float(readonly=True, string="Selling Price") + + bedrooms = fields.Integer(default=2, string="Bedrooms") + + living_area = fields.Integer(string="Living Area (sqm)") + + facades = fields.Integer(default=2, string="Facades") + + garage = fields.Boolean(default=False, string="Garage") + + garden = fields.Boolean(default=False, string="Garden") + + garden_area = fields.Integer(default=0, string="Garden Area (sqm)") + + garden_orientation = fields.Selection( + [("north", "North"), ("south", "South"), ("west", "West"), ("east", "East")], + string="Garden Orientation", + ) + + type_id = fields.Many2one( + "estate_property_type", required=True, string="Property Type" + ) + + buyer = fields.Many2one("res.partner", copy=False, string="Buyer") + + salesperson = fields.Many2one( + "res.users", default=(lambda self: self.env.user), string="Salesman" + ) + + tag_ids = fields.Many2many("estate_property_tag", string="Property Tags") + + offer_ids = fields.One2many("estate_property_offer", "property_id", string="Offers") + + total_area = fields.Integer(compute="_compute_total_area", string="Total Area") + + best_offer = fields.Float( + compute="_compute_best_offer", default=0.0, string="Best Offer" + ) + + _sql_constraints = [ + ( + "check_positive_expected_price", + "CHECK(expected_price >= 0.0)", + "Expected Price should be a positive number.", + ), + ( + "check_positive_selling_price", + "CHECK(selling_price >= 0.0)", + "Selling Price should be a positive number.", + ), + ] + + @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")) + + @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 = "" + + @api.constrains("selling_price", "expected_price") + def _check_expected_vs_selling_price(self): + for record in self: + if (record.selling_price > 0.0) and ( + record.selling_price < 0.9 * record.expected_price + ): + raise exceptions.ValidationError( + r"Cannot sell for less than 90% of expected price." + ) + + def action_sold(self): + for record in self: + if record.status != "canceled": + record.status = "sold" + else: + raise exceptions.UserError("Canceled properties cannot be sold.") + return True + + def action_cancel(self): + for record in self: + if record.status != "sold": + record.status = "canceled" + else: + raise exceptions.UserError("Sold properties cannot be canceled.") + return True diff --git a/estate/models/estate_property_infos.py b/estate/models/estate_property_infos.py new file mode 100644 index 0000000000..f4521f1534 --- /dev/null +++ b/estate/models/estate_property_infos.py @@ -0,0 +1,42 @@ +from odoo import fields, models + + +class Estate_Property_Type(models.Model): + _name = "estate_property_type" + _description = "Estate property Types" + _order = "name" + + name = fields.Char( + required=True, + string="Type" + ) + + property_ids = fields.One2many( + "estate_property", + "type_id", + string="Estate Properties" + ) + + _sql_constraints = [ + ("check_unique_type", "UNIQUE(name)", "Property types must be unique.") + ] + + +class Estate_Property_Tag(models.Model): + _name = "estate_property_tag" + _description = "Estate property Tags" + _order = "name" + + name = fields.Char( + required=True, + string="Type" + ) + + property_estate_ids = fields.Many2many( + "estate_property", + string="Estate Properties" + ) + + _sql_constraints = [ + ("check_unique_tag", "UNIQUE(name)", "Property tags must be unique.") + ] diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 0000000000..9c44323807 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,76 @@ +from odoo import api, exceptions, fields, models +from datetime import date +from dateutil.relativedelta import relativedelta + + +class Estate_Property_Offer(models.Model): + _name = "estate_property_offer" + _description = "Estate Property Offers" + _order = "price desc" + + price = fields.Float( + string="Price" + ) + + status = fields.Selection( + [("accepted", "Accepted"), ("refused", "Refused")], + readonly=True, + copy=False, + string="Status" + ) + + partner_id = fields.Many2one( + 'res.partner', + required=True, + string="Partner" + ) + + property_id = fields.Many2one( + 'estate_property', + string="Property" + ) + + validity = fields.Integer( + default=7, + string="Validity (days)" + ) + + deadline = fields.Date( + compute="_compute_deadline", + copy=False, + string="Deadline" + ) + + _sql_constraints = [ + ("check_positive_price", "CHECK(price > 0.0)", "Offer Price should be a positive number (higher than 0).") + ] + + @api.depends("validity") + def _compute_deadline(self): + for record in self: + record.deadline = date.today() + relativedelta(days=+record.validity) + + def _inverse_deadline(self): + for record in self: + record.validity = relativedelta(date.today(), record.deadline) + + def action_accept(self): + for record in self: + if not any(offer_status == "accepted" for offer_status in record.property_id.offer_ids.mapped("status")): + # Set values in the Property itself + record.property_id.selling_price = record.price + record.property_id.buyer = record.partner_id + + record.status = "accepted" + else: + raise exceptions.UserError("An offer has already been accepted.") + return True + + def action_refuse(self): + for record in self: + if record.status == "accepted": + # Set values in the Property itself + record.property_id.selling_price = 0.0 + record.property_id.buyer = None + record.status = "refused" + return True diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 0000000000..0db13e578e --- /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 \ No newline at end of file diff --git a/estate/static/description/icon.webp b/estate/static/description/icon.webp new file mode 100644 index 0000000000..9758ad2b58 Binary files /dev/null and b/estate/static/description/icon.webp differ diff --git a/estate/views/estate_menu_views.xml b/estate/views/estate_menu_views.xml new file mode 100644 index 0000000000..a4cdc3455e --- /dev/null +++ b/estate/views/estate_menu_views.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 0000000000..5599dbeae3 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,27 @@ + + + + + + estate_property_offer_list + estate_property_offer + + + + + + +