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..84981126d40
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,14 @@
+{
+ "name": "Real Estate",
+ "description": "Real Estate Management System",
+ "category": "Tutorials",
+ "version": "1.1",
+ "application": True,
+ "data": [
+ "security/ir.model.access.csv",
+ "views/views.xml",
+ "views/menus.xml"
+ ],
+ "author": "Odoo S.A.",
+ "license": "LGPL-3",
+}
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..e8decb89cb0
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,4 @@
+from . import buildings_model
+from . import building_type_model
+from . import tags_model
+from . import offers_model
diff --git a/estate/models/building_type_model.py b/estate/models/building_type_model.py
new file mode 100644
index 00000000000..a81f5b2aba6
--- /dev/null
+++ b/estate/models/building_type_model.py
@@ -0,0 +1,12 @@
+from odoo import models, fields
+
+
+class BuildingType(models.Model):
+ _name = "estate.building_type"
+ _description = "Building Type Model"
+
+ name = fields.Char(required=True)
+
+ _name_uniqueness_constraint = models.Constraint(
+ "UNIQUE (name)", "Building type name must be UNIQUE."
+ )
diff --git a/estate/models/buildings_model.py b/estate/models/buildings_model.py
new file mode 100644
index 00000000000..ef36125d639
--- /dev/null
+++ b/estate/models/buildings_model.py
@@ -0,0 +1,90 @@
+from odoo import models, fields, api
+from odoo.exceptions import UserError
+from datetime import timedelta
+
+
+class Building(models.Model):
+ _name = "estate.buildings"
+ _description = "Buildings Model"
+
+ name = fields.Char()
+ description = fields.Text()
+ value = fields.Integer(copy=False)
+ availability_date = fields.Date(
+ default=lambda self: fields.Date.today() + timedelta(days=90), copy=False
+ )
+ number_of_rooms = fields.Integer(default=2)
+ garden_area = fields.Integer()
+ building_area = fields.Integer()
+ garden_orientation = fields.Selection(
+ [("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")],
+ "garden Orientation",
+ )
+ active = fields.Boolean(default=True)
+ state = fields.Selection(
+ [
+ ("new", "New"),
+ ("offer received", "Offer Received"),
+ ("offer accepted", "Offer Accepted"),
+ ("sold", "Sold"),
+ ("canceled", "Canceled"),
+ ],
+ default="new",
+ )
+ post_code = fields.Integer(default=1000)
+ building_type_id = fields.Many2one("estate.building_type", string="Building Type")
+ buyer_id = fields.Many2one("res.partner", string="Buyer")
+ salesperson_id = fields.Many2one(
+ "res.users", string="Salesperson", default=lambda self: self.env.user
+ )
+ tag_ids = fields.Many2many("estate.building_tags", string="Tags")
+ offer_ids = fields.One2many("estate.offers", "building_id", string="Offers")
+
+ total_area = fields.Integer(string="Total Area", compute="_compute_total_area")
+
+ best_price = fields.Integer(
+ string="Best Offer Price",
+ compute="_compute_best_price",
+ )
+ has_garden = fields.Boolean(string="Has Garden", default=False)
+
+ @api.depends("building_area", "garden_area")
+ def _compute_total_area(self):
+ for record in self:
+ record.total_area = record.building_area + record.garden_area
+
+ @api.depends("offer_ids.price")
+ def _compute_best_price(self):
+ for record in self:
+ if record.offer_ids:
+ record.best_price = max(record.offer_ids.mapped("price"))
+ else:
+ record.best_price = 0
+
+ @api.onchange("has_garden")
+ def _onchange_garden_area(self):
+ if self.has_garden:
+ self.garden_area = 10
+ self.garden_orientation = "north"
+ else:
+ self.garden_area = 0
+ self.garden_orientation = False
+
+ def action_set_sold(self):
+ for record in self:
+ if record.state == "canceled":
+ raise UserError("Canceled buildings cannot be sold.")
+ record.state = "sold"
+
+ def action_set_canceled(self):
+ for record in self:
+ if record.state == "sold":
+ raise UserError("Sold buildings cannot be canceled.")
+ record.state = "canceled"
+
+ _price_constraint = models.Constraint(
+ "CHECK (value > 0)", "Price must be POSITIVE."
+ )
+ _name_constraint = models.Constraint(
+ "UNIQUE(name)", "Building name must be UNIQUE."
+ )
diff --git a/estate/models/offers_model.py b/estate/models/offers_model.py
new file mode 100644
index 00000000000..8261696fab3
--- /dev/null
+++ b/estate/models/offers_model.py
@@ -0,0 +1,81 @@
+from odoo import models, fields, api
+from odoo.exceptions import UserError
+from datetime import timedelta
+from odoo.tools.float_utils import float_compare
+
+
+class Offer(models.Model):
+ _name = "estate.offers"
+ _description = "Offers Model"
+
+ price = fields.Integer(required=True)
+ status = fields.Selection(
+ [("accepted", "Accepted"), ("refused", "Refused")],
+ string="Status",
+ required=False,
+ )
+ building_id = fields.Many2one("estate.buildings", string="Building")
+ partner_id = fields.Many2one("res.partner", string="Partner")
+ validity = fields.Integer(string="Validity (days)", default=7)
+ date_deadline = fields.Date(
+ string="Deadline",
+ compute="_compute_date_deadline",
+ inverse="_inverse_date_deadline",
+ )
+
+ @api.depends("validity")
+ def _compute_date_deadline(self):
+ for record in self:
+ record.date_deadline = fields.Date.today() + timedelta(days=record.validity)
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ record.validity = (record.date_deadline - fields.Date.today()).days
+
+ def action_accept_offer(self):
+ for record in self:
+ if record.status != "accepted" and record.building_id.state not in [
+ "sold",
+ "canceled",
+ ]:
+ record.status = "accepted"
+ record.building_id.state = "offer accepted"
+ record.building_id.buyer_id = record.partner_id
+ record.building_id.value = record.price
+ other_offers = self.search(
+ [
+ ("building_id", "=", record.building_id.id),
+ ("id", "!=", record.id),
+ ]
+ )
+ other_offers.write({"status": "refused"})
+ elif record.building_id.state in ["sold", "canceled"]:
+ raise UserError("Cannot accept offers for sold or canceled buildings.")
+ else:
+ raise UserError("Offer is already accepted.")
+
+ def action_refuse_offer(self):
+ for record in self:
+ if record.status != "refused":
+ record.status = "refused"
+ record.building_id.state = "offer received"
+ record.building_id.buyer_id = False
+ else:
+ raise UserError("Offer is already refused.")
+
+ _price_positive_constraint = models.Constraint(
+ "CHECK (price > 0)", "Offer price must be positive."
+ )
+
+ @api.constrains("building_id", "price")
+ def _check_price(self):
+ for record in self:
+ if (
+ float_compare(
+ 0.9 * record.building_id.value, record.price, precision_digits=2
+ )
+ == 1
+ ):
+ raise UserError(
+ "Offer price must be at least 90% of the building's value."
+ )
diff --git a/estate/models/tags_model.py b/estate/models/tags_model.py
new file mode 100644
index 00000000000..4efbbb1633a
--- /dev/null
+++ b/estate/models/tags_model.py
@@ -0,0 +1,12 @@
+from odoo import models, fields
+
+
+class BuildingTag(models.Model):
+ _name = "estate.building_tags"
+ _description = "Building Tags Model"
+
+ name = fields.Char(required=True)
+
+ _name_uniqueness_constraint = models.Constraint(
+ "UNIQUE (name)", "Building 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..937aef532c1
--- /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_first_model,access_first_model,model_estate_buildings,base.group_user,1,1,1,1
+access_building_type_model,access_building_type_model,model_estate_building_type,base.group_user,1,1,1,1
+access_building_tags_model,access_building_tags_model,model_estate_building_tags,base.group_user,1,1,1,1
+access_offers_model,access_offers_model,model_estate_offers,base.group_user,1,1,1,1
diff --git a/estate/views/menus.xml b/estate/views/menus.xml
new file mode 100644
index 00000000000..99577eb74a4
--- /dev/null
+++ b/estate/views/menus.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/estate/views/views.xml b/estate/views/views.xml
new file mode 100644
index 00000000000..9d1eb4b93d6
--- /dev/null
+++ b/estate/views/views.xml
@@ -0,0 +1,99 @@
+
+
+
+ Test action 1
+ estate.buildings
+ list,form
+
+
+ estate.buildings.list
+ estate.buildings
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.buildings.form
+ estate.buildings
+
+
+
+
+
+
+ estate.buildings.search
+ estate.buildings
+
+
+
+
+
+
+
+
+
+
+
+