diff --git a/estate/__manifest__.py b/estate/__manifest__.py
index d7cfa57d4c5..edae98ea504 100644
--- a/estate/__manifest__.py
+++ b/estate/__manifest__.py
@@ -1,10 +1,16 @@
{
- "name": "Estate", # The name that will appear in the App list
- "version": "16.0.0", # Version
+ "name": "Real Estate", # The name that will appear in the App list
+ "version": "1.0.0", # Version
"application": True, # This line says the module is an App, and not a module
"depends": ["base"], # dependencies
"data": [
-
+ "security/ir.model.access.csv",
+ "views/estate_menus.xml",
+ "views/estate_property_views.xml",
+ "views/estate_property_type_views.xml",
+ "views/estate_property_tag_views.xml",
+ "views/estate_property_offer_views.xml",
+ "views/res_users_views.xml",
],
"installable": True,
'license': 'LGPL-3',
diff --git a/estate/models.py b/estate/models.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..a9459ed5906
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,5 @@
+from . import estate_property
+from . import estate_property_type
+from . import estate_property_tag
+from . import estate_property_offer
+from . import res_users
\ No newline at end of file
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
new file mode 100644
index 00000000000..d7117ed2a2b
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+from odoo import fields, models, api, _
+
+from dateutil.relativedelta import relativedelta
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools.float_utils import float_compare, float_is_zero
+
+class EstateProperty(models.Model):
+ _name = "estate.property"
+ _description = "Real Estate"
+ _sql_constraints = [
+ ('check_expected_price', 'CHECK(expected_price >= 0 )',
+ 'The Expected Price should be positive.'),
+ ('check_selling_price', 'CHECK(selling_price >= 0 )',
+ 'The Selling Price should be positive.')
+ ]
+ _order = "id desc"
+
+ def _default_date_availability(self):
+ return fields.Datetime.today(self) + relativedelta(months=3)
+
+ name = fields.Char(required=True, string="Title")
+ description = fields.Text()
+ postcode = fields.Char()
+ date_availability = fields.Date(copy=False, default=lambda self: self._default_date_availability())
+ expected_price = fields.Float(required=True)
+ selling_price = fields.Float(readonly=True, copy=False)
+ 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([('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')])
+ active = fields.Boolean(default=True)
+ state = fields.Selection([('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('canceled', 'Canceled')], required=True, copy=False, default='new')
+
+ property_type_id = fields.Many2one("estate.property.type", string="Property Type")
+ user_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user)
+ buyer_id = fields.Many2one("res.partner", string="Buyer", readonly=True, copy=False)
+ tag_ids = fields.Many2many("estate.property.tag", string="Tags")
+ offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
+
+ total_area = fields.Float(copy=False, string='Total Area (sqm)', compute='_compute_total_area')
+ best_price = fields.Float(copy=False, string='Best Offer', compute='_compute_best_price')
+
+ @api.depends('living_area', 'garden_area')
+ def _compute_total_area(self):
+ for record in self:
+ record.total_area = record.living_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.0
+
+ @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 = False
+
+ def action_sold(self):
+ if self.state == 'canceled':
+ raise UserError(_("Canceled properties cannot be sold."))
+ else:
+ self.state = 'sold'
+
+ # Recommend This Way
+ # def action_sold(self):
+ # if "canceled" in self.mapped("state"):
+ # raise UserError("Canceled properties cannot be sold.")
+ # return self.write({"state": "sold"})
+
+ def action_cancel(self):
+ if self.state == 'sold':
+ raise UserError(_("Sold properties cannot be canceled."))
+ else:
+ self.state = 'canceled'
+
+ # Recommend This Way
+ # def action_cancel(self):
+ # if "sold" in self.mapped("state"):
+ # raise UserError("Sold properties cannot be canceled.")
+ # return self.write({"state": "canceled"})
+
+ @api.constrains('selling_price')
+ def _check_selling_price(self):
+ for record in self:
+ if not float_is_zero(record.selling_price, precision_rounding=0.01) and float_compare(record.selling_price, record.expected_price * 90 / 100, precision_rounding=0.01) < 0:
+ raise ValidationError("The selling price cannot be lower than 90% of the expected price.")
+
+ def unlink(self):
+ if not set(self.mapped("state")) <= {"new", "canceled"}:
+ raise UserError("Only new and canceled properties can be deleted.")
+ return super().unlink()
\ No newline at end of file
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..8b7687aac74
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+from odoo import fields, models, api, _
+
+from dateutil.relativedelta import relativedelta
+from odoo.exceptions import UserError
+from odoo.tools import float_compare
+
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _sql_constraints = [
+ ('check_price', 'CHECK(price > 0 )',
+ 'The Price should be positive.'),
+ ]
+ _order = "price desc"
+
+
+ price = fields.Float()
+ status = fields.Selection([('accepted','Accepted'), ('refused', 'Refused')], copy=False)
+ 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(
+ "estate.property.type", related="property_id.property_type_id", string="Property Type", store=True
+ )
+
+ @api.depends('create_date', 'validity')
+ def _compute_date_deadline(self):
+ for record in self:
+ if record.create_date:
+ date = record.create_date.date()
+ else:
+ date = fields.Date.today()
+
+ record.date_deadline = date + relativedelta(days=record.validity)
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ if record.create_date:
+ date = record.create_date.date()
+ else:
+ date = fields.Date.today()
+
+ record.validity = (record.date_deadline - date).days
+
+ @api.model
+ def create(self, vals):
+ if vals.get("property_id") and vals.get("price"):
+ record = self.env["estate.property"].browse(vals["property_id"])
+ if record.offer_ids:
+ max_offer = max(record.mapped("offer_ids.price"))
+ if float_compare(vals["price"], max_offer, precision_rounding=0.01) <= 0:
+ raise UserError("The offer must be higher that %.2f" % max_offer)
+ record.state = "offer_received"
+ return super().create(vals)
+
+ # def create(self, vals):
+ # record = super().create(vals)
+ # if record:
+ # record.update_state()
+ # return record
+ #
+ # def update_state(self):
+ # return self.mapped("property_id").write(
+ # {
+ # "state": "offer_received",
+ # }
+ # )
+ # def action_accepted(self):
+ # if "accepted" in self.mapped("property_id.offer_ids.status"):
+ # raise UserError(_("An offer has already been accepted."))
+ # else:
+ # self.status = 'accepted'
+ # self.property_id.selling_price = self.price
+ # self.property_id.buyer_id = self.partner_id
+
+ def action_accepted(self):
+ if "accepted" in self.mapped("property_id.offer_ids.status"):
+ raise UserError(_("An offer as already been accepted."))
+ self.write(
+ {
+ "status": "accepted",
+ }
+ )
+ return self.mapped("property_id").write(
+ {
+ "state": "offer_accepted",
+ "selling_price": self.price,
+ "buyer_id": self.partner_id,
+ }
+ )
+
+ # def action_refused(self):
+ # self.status = 'refused'
+ # self.property_id.selling_price = 0
+ # self.property_id.buyer_id = False
+
+ def action_refused(self):
+ self.write(
+ {
+ "status": "refused"
+ }
+ )
+ return self.mapped("property_id").write(
+ {
+ "state": "offer_received",
+ "selling_price": 0,
+ "buyer_id": False,
+ }
+ )
\ No newline at end of file
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..7c3a4a2eccc
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+from odoo import fields, models
+
+class EstatePropertyTag(models.Model):
+ _name = "estate.property.tag"
+ _sql_constraints = [
+ ('check_name', 'unique(name)',
+ 'The Name should be unique.'),
+ ]
+ _order = "name desc"
+
+ name = fields.Char(required=True)
+ color = fields.Integer()
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 00000000000..e63778b6bb1
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+from odoo import fields, models
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.type"
+ _sql_constraints = [
+ ('check_name', 'unique(name)',
+ 'The Name should be unique.'),
+ ]
+ _order = "name desc"
+
+ name = fields.Char(required=True)
+ property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
+ sequence = fields.Integer('Sequence', default=1, help="Used to order stages. Lower is better.")
+
+ offer_count = fields.Integer(string="Offers Count", compute="_compute_offer")
+ offer_ids = fields.Many2many("estate.property.offer", string="Offers", compute="_compute_offer")
+
+ def _compute_offer(self):
+ data = self.env["estate.property.offer"].read_group(
+ [("property_id.state", "!=", "canceled"), ("property_type_id", "!=", False)],
+ ["ids:array_agg(id)", "property_type_id"],
+ ["property_type_id"],
+ )
+ mapped_count = {d["property_type_id"][0]: d["property_type_id_count"] for d in data}
+ mapped_ids = {d["property_type_id"][0]: d["ids"] for d in data}
+ for prop_type in self:
+ prop_type.offer_count = mapped_count.get(prop_type.id, 0)
+ prop_type.offer_ids = mapped_ids.get(prop_type.id, [])
+
+ def action_view_offers(self):
+ res = self.env.ref("estate.estate_property_offer_action").read()[0]
+ res["domain"] = [("id", "in", self.offer_ids.ids)]
+ return res
\ No newline at end of file
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 00000000000..7cd18d68033
--- /dev/null
+++ b/estate/models/res_users.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+from odoo import fields, models
+
+class ResUsers(models.Model):
+ _inherit = "res.users"
+
+ property_ids = fields.One2many(
+ "estate.property", "user_id", string="Properties", domain=[("state", "in", ["new", "offer_received"])])
\ No newline at end of file
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..6398ab5d5a4
--- /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,estate.model_estate_property,base.group_user,1,1,1,1
+access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1
+access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1
+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/views/estate_menus.xml b/estate/views/estate_menus.xml
new file mode 100644
index 00000000000..0a61fcc2f4d
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
new file mode 100644
index 00000000000..0a793492d8f
--- /dev/null
+++ b/estate/views/estate_property_offer_views.xml
@@ -0,0 +1,53 @@
+
+
+
+ estate.property.offer.form
+ estate.property.offer
+
+
+
+
+
+
+ estate.property.offer.tree
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.search
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+ Property Offers
+ estate.property.offer
+ [('property_type_id','=', active_id)]
+ tree,form
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
new file mode 100644
index 00000000000..17d87f120eb
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,44 @@
+
+
+
+ estate.property.tag.form
+ estate.property.tag
+
+
+
+
+
+
+ estate.property.tag.tree
+ estate.property.tag
+
+
+
+
+
+
+
+
+ estate.property.tag.search
+ estate.property.tag
+
+
+
+
+
+
+
+
+ Property Tags
+ estate.property.tag
+ tree,form
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..b449632f4a3
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,61 @@
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
+
+ estate.property.type.tree
+ estate.property.type
+
+
+
+
+
+
+
+
+
+ estate.property.type.search
+ estate.property.type
+
+
+
+
+
+
+
+
+ Property Types
+ estate.property.type
+ tree,form
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
new file mode 100644
index 00000000000..9954874ed28
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,141 @@
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+ estate.property.tree
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.kanban
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+ Expected Price:
+
+
+ Best Offer:
+
+
+ Selling Price:
+
+
+
+
+
+
+
+
+
+
+ Properties
+ estate.property
+ tree,form
+ {'search_default_available': True}
+
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml
new file mode 100644
index 00000000000..63c4c9729a1
--- /dev/null
+++ b/estate/views/res_users_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ res.users.form.inherit.estate
+ res.users
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate_account/__init__.py b/estate_account/__init__.py
new file mode 100644
index 00000000000..5305644df14
--- /dev/null
+++ b/estate_account/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import models
\ No newline at end of file
diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py
new file mode 100644
index 00000000000..87916382b4d
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+
+{
+ "name": "Real Estate Accounting",
+ "depends": [
+ "estate",
+ "account",
+ ],
+ "auto_install": True,
+}
\ No newline at end of file
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
new file mode 100644
index 00000000000..f4c8fd6db6d
--- /dev/null
+++ b/estate_account/models/__init__.py
@@ -0,0 +1 @@
+from . import estate_property
\ No newline at end of file
diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py
new file mode 100644
index 00000000000..9ffa366cc16
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models
+
+
+class EstateProperty(models.Model):
+ _inherit = "estate.property"
+
+ def action_sold(self):
+ res = super().action_sold()
+ journal = self.env["account.journal"].search([("type", "=", "sale")], limit=1)
+ for prop in self:
+ self.env["account.move"].create(
+ {
+ "partner_id": prop.buyer_id.id,
+ "move_type": "out_invoice",
+ "journal_id": journal.id,
+ "invoice_line_ids": [
+ (
+ 0,
+ 0,
+ {
+ "name": prop.name,
+ "quantity": 1.0,
+ "price_unit": prop.selling_price * 6.0 / 100.0,
+ },
+ ),
+ (
+ 0,
+ 0,
+ {
+ "name": "Administrative fees",
+ "quantity": 1.0,
+ "price_unit": 100.0,
+ },
+ ),
+ ],
+ }
+ )
+ return res
\ No newline at end of file