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..062910e1e9d
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,5 @@
+from . import building
+from . import building_type
+from . import tag
+from . import offer
+from . import salesperson
diff --git a/estate/models/building.py b/estate/models/building.py
new file mode 100644
index 00000000000..1a522a23436
--- /dev/null
+++ b/estate/models/building.py
@@ -0,0 +1,99 @@
+from datetime import timedelta
+
+from odoo import models, fields, api
+from odoo.exceptions import UserError
+
+
+class Building(models.Model):
+ _name = 'estate.building'
+ _description = 'Buildings'
+ _order = "id desc"
+
+ 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.offer", "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)
+
+ _price_constraint = models.Constraint(
+ "CHECK (value > 0)", "Price must be POSITIVE."
+ )
+ _name_constraint = models.Constraint(
+ "UNIQUE(name)", "Building name must be UNIQUE."
+ )
+
+ @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(self.env._("Canceled buildings cannot be sold."))
+ record.state = "sold"
+
+ def action_set_canceled(self):
+ for record in self:
+ if record.state == "sold":
+ raise UserError(self.env._("Sold buildings cannot be canceled."))
+ record.state = "canceled"
+
+ @api.ondelete(at_uninstall=False)
+ def _check_if_sold(self):
+ for record in self:
+ if record.state not in ("new", "canceled"):
+ raise UserError(self.env._("This building cannot be deleted."))
+ return self
diff --git a/estate/models/building_type.py b/estate/models/building_type.py
new file mode 100644
index 00000000000..bbb0047bad2
--- /dev/null
+++ b/estate/models/building_type.py
@@ -0,0 +1,32 @@
+from odoo import models, fields
+
+
+class BuildingType(models.Model):
+ _name = 'estate.building_type'
+ _description = 'Building Type'
+ _order = "sequence, name"
+
+ name = fields.Char(required=True)
+ building_ids = fields.One2many(
+ "estate.building", "building_type_id", string="Buildings"
+ )
+ offer_ids = fields.One2many("estate.offer", "property_type_id", string="Offers")
+ offers_count = fields.Integer(
+ string="Offers Count",
+ compute="_compute_offers_count",
+ )
+
+ _name_uniqueness_constraint = models.Constraint(
+ "UNIQUE (name)", "Building type name must be UNIQUE."
+ )
+
+ sequence = fields.Integer(
+ default=1,
+ help="Gives the sequence order when displaying a list of building types.",
+ )
+
+ def _compute_offers_count(self):
+ for record in self:
+ record.offers_count = self.env["estate.offer"].search_count(
+ [("property_type_id", "=", record.id)]
+ )
diff --git a/estate/models/offer.py b/estate/models/offer.py
new file mode 100644
index 00000000000..0cbaef376a4
--- /dev/null
+++ b/estate/models/offer.py
@@ -0,0 +1,84 @@
+from datetime import timedelta
+
+from odoo import models, fields, api
+from odoo.exceptions import UserError
+from odoo.tools.float_utils import float_compare
+
+
+class Offer(models.Model):
+ _name = 'estate.offer'
+ _description = 'Offers'
+ _order = "price desc"
+
+ price = fields.Integer(required=True)
+ status = fields.Selection(
+ [("accepted", "Accepted"), ("refused", "Refused")],
+ string="Status",
+ required=False,
+ )
+ building_id = fields.Many2one("estate.building", 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",
+ )
+ property_type_id = fields.Many2one(
+ related="building_id.building_type_id", string="Property Type", store=True
+ )
+
+ _price_positive_constraint = models.Constraint(
+ "CHECK (price > 0)", "Offer price must be positive."
+ )
+
+ @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(self.env._("Cannot accept offers for sold or canceled buildings."))
+ else:
+ raise UserError(self.env._("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(self.env._("Offer is already refused."))
+
+ @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) > 0:
+ raise UserError(self.env._("Offer price must be at least 90% of the building's value."))
+
+ @api.model
+ def create(self, vals):
+ for val in vals:
+ self.env["estate.building"].browse(
+ val["building_id"]
+ ).state = "offer_received"
+ return super().create(vals)
diff --git a/estate/models/salesperson.py b/estate/models/salesperson.py
new file mode 100644
index 00000000000..0542543b9c7
--- /dev/null
+++ b/estate/models/salesperson.py
@@ -0,0 +1,8 @@
+from odoo import models, fields
+
+
+class ResUsers(models.Model):
+ _name = 'res.users'
+ _inherit = ['res.users']
+
+ building = fields.One2many("estate.building", "salesperson_id", string="Listings")
diff --git a/estate/models/tag.py b/estate/models/tag.py
new file mode 100644
index 00000000000..7c5f3462693
--- /dev/null
+++ b/estate/models/tag.py
@@ -0,0 +1,14 @@
+from odoo import models, fields
+
+
+class BuildingTag(models.Model):
+ _name = 'estate.building_tags'
+ _description = 'Building Tags'
+ _order = "name"
+
+ name = fields.Char(required=True)
+ color = fields.Integer()
+
+ _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..187a0e391ac
--- /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_building,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_offer,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..d433aa6e186
--- /dev/null
+++ b/estate/views/menus.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/estate/views/views.xml b/estate/views/views.xml
new file mode 100644
index 00000000000..cdb0748a70a
--- /dev/null
+++ b/estate/views/views.xml
@@ -0,0 +1,196 @@
+
+
+
+ Buildings
+ estate.building
+ list,form,kanban
+ {'search_default_Available': True}
+
+
+
+ Building Types
+ estate.building_type
+ list,form
+
+
+
+ Offers
+ estate.offer
+ list
+ [('property_type_id', '=', active_id)]
+
+
+
+ estate.building.list
+ estate.building
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.building.form
+ estate.building
+
+
+
+
+
+
+ estate.building.search
+ estate.building
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.building_type.form
+ estate.building_type
+
+
+
+
+
+
+ estate.building_type.list
+ estate.building_type
+
+
+
+
+
+
+
+
+ test_inheretance
+ res.users
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.building.kanban
+ estate.building
+
+
+
+
+
+
+
+ Price:
+
+
+
+ State:
+
+
+
+
+
+
+
+
+
+
diff --git a/estate_accounting/__init__.py b/estate_accounting/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/estate_accounting/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/estate_accounting/__manifest__.py b/estate_accounting/__manifest__.py
new file mode 100644
index 00000000000..f42afcf9283
--- /dev/null
+++ b/estate_accounting/__manifest__.py
@@ -0,0 +1,10 @@
+{
+ "name": "Realestate accounting",
+ "description": "Real Estate Accounting System",
+ "category": "Tutorials",
+ "version": "1.0",
+ "application": True,
+ "depends": ["estate", "account"],
+ "author": "Odoo S.A.",
+ "license": "LGPL-3",
+}
diff --git a/estate_accounting/models/__init__.py b/estate_accounting/models/__init__.py
new file mode 100644
index 00000000000..02b688798a3
--- /dev/null
+++ b/estate_accounting/models/__init__.py
@@ -0,0 +1 @@
+from . import estate_account
diff --git a/estate_accounting/models/estate_account.py b/estate_accounting/models/estate_account.py
new file mode 100644
index 00000000000..164e6c316ca
--- /dev/null
+++ b/estate_accounting/models/estate_account.py
@@ -0,0 +1,37 @@
+from odoo import models, Command
+
+
+class estateAccount(models.Model):
+ _inherit = "estate.building"
+
+ def action_set_sold(self):
+ self.env["account.move"].create(
+ {
+ "partner_id": self.buyer_id.id,
+ "move_type": "out_invoice",
+ "line_ids": [
+ Command.create(
+ {
+ "name": "Property Sale",
+ "quantity": 1.0,
+ "price_unit": self.value,
+ }
+ ),
+ Command.create(
+ {
+ "name": "Taxes",
+ "quantity": 1.0,
+ "price_unit": self.value * 0.06,
+ }
+ ),
+ Command.create(
+ {
+ "name": "Administrative Fees",
+ "quantity": 1.0,
+ "price_unit": 100.0,
+ }
+ ),
+ ],
+ }
+ )
+ return super().action_set_sold()