Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ dmypy.json

# Pyre type checker
.pyre/

i18n
1 change: 1 addition & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
23 changes: 23 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
'name': "Real Estate",
'version': "1.0",
'category': "Real Estate",
'summary': "Manage real estate properties",
'description': "This module allows managing properties",
'depends': ['base'],
'author': "jakan",
'license': "LGPL-3",
'application': True,
'installable': True,
'data': [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_property_maintenance_views.xml",
"views/estate_property_investor_views.xml",
'views/res_users_views.xml',
"views/estate_menus.xml",
]
}
7 changes: 7 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from . import estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import estate_property_maintenance
from . import estate_property_investor
from . import res_users
135 changes: 135 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero
from odoo import _


class EstateProperty(models.Model):
_name = 'estate.property'
_description = 'Real Estate Property'
_order = 'id desc'

# Each field becomes a column in PostgreSQL table
name = fields.Char(required=True, default="Unknown")
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
default=lambda self: fields.Date.today() + relativedelta(months=3),
copy=False
)
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(
string="Direction",
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"),
('cancelled', "Cancelled"),
],
required=True,
copy=False,
default="new",
)
property_type_id = fields.Many2one("estate.property.type", string="Property Type")
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
seller_id = fields.Many2one("res.users", string="Seller", default=lambda self: self.env.user)
tag_ids = fields.Many2many("estate.property.tag", string="Property Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Property Offers")
total_area = fields.Float(string="Total Area", compute="_compute_total_area", store=True)
best_price = fields.Float(string="Best Offer", compute="_compute_best_price")
maintenance_ids = fields.One2many("estate.property.maintenance", "property_id")
total_cost = fields.Float(string="Total Cost", compute="_compute_total_cost")

_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
'Expected price must be strictly positive.',
)

_check_selling_price = models.Constraint(
'CHECK(selling_price > 0)',
'Selling price must be positive.',
)

@api.depends('living_area', 'garden_area')
def _compute_total_area(self):
for property in self:
property.total_area = property.living_area + property.garden_area

@api.depends('offer_ids.price')
def _compute_best_price(self):
for property in self:
if property.offer_ids:
property.best_price = max(property.offer_ids.mapped('price'))
else:
property.best_price = 0

@api.depends('maintenance_ids.cost')
def _compute_total_cost(self):
for maintenance in self:
maintenance.total_cost = sum(maintenance.maintenance_ids.mapped('cost'))

@api.constrains('expected_price', 'selling_price')
def _check_selling_price(self):
for property in self:
if float_is_zero(property.selling_price, precision_digits=2):
continue

minimum_price = property.expected_price * 0.9

if float_compare(property.selling_price, minimum_price, precision_digits=2) < 0:
raise ValidationError("Selling price cannot be lower than 90% of the expected 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 = False

@api.ondelete(at_uninstall=False)
def _check_property_deletion(self):
for property in self:
if property.state not in ('new', 'cancelled'):
raise UserError("You can only delete properties in New or Cancelled state")

def action_sold(self):
for property in self:
if property.state == 'cancelled':
raise UserError(_("Sold property cannot be cancelled"))
if not property.buyer_id:
raise UserError(_("Without accept any offer we can't sold it"))
if property.maintenance_ids:
for maintenance in property.maintenance_ids:
if maintenance.status in ('new', 'cancle'):
raise UserError(_("Maintenance cost must be Approved or Done"))
property.state = 'sold'
return True

def action_cancel(self):
for property in self:
if property.state == 'sold':
raise UserError("Cancelled property cannot be sold")
property.state = 'cancelled'
return True
9 changes: 9 additions & 0 deletions estate/models/estate_property_investor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from odoo import fields, models


class EstatePropertyInvestor(models.Model):
_name = 'estate.property.investor'
_description = 'Property Investor'
_inherits = {'res.partner': 'partner_id'}

partner_id = fields.Many2one('res.partner', required=True, ondelete='cascade')
36 changes: 36 additions & 0 deletions estate/models/estate_property_maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from odoo import fields, models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_is_zero


class EstatePropertyMaintenance(models.Model):
_name = 'estate.property.maintenance'
_description = 'Estate Property maintenance'

title = fields.Char(required=True)
cost = fields.Float(string="Cost")
status = fields.Selection(
[
('new', "New"),
('approved', "Approved"),
('done', "Done"),
('cancle', "Cancle")
],
required=True,
default="new",
)

property_id = fields.Many2one('estate.property', required=True)

def maintenance_accept(self):
for maintenance in self:
if float_is_zero(maintenance.cost, precision_digits=2):
raise UserError("Maintenance cost must be greater than zero")

maintenance.status = "approved"
return True

def maintenance_refuse(self):
for maintenance in self:
maintenance.status = "cancle"
return True
99 changes: 99 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError


class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = 'Estate Property Offer'
_order = 'price desc'

price = fields.Float(string="Price")
status = fields.Selection(
[
('accepted', 'Accepted'),
('refused', 'Refused'),
],
string="Status",
copy=False
)
partner_id = fields.Many2one('res.partner', string="Partner", required=True)
property_id = fields.Many2one('estate.property', string="Property", required=True)
validity = fields.Integer(default=7)
date_deadline = fields.Date(
compute="_compute_date_deadline",
inverse="_inverse_date_deadline",
store=True
)
property_type_id = fields.Many2one("estate.property.type", related="property_id.property_type_id", store=True, readonly=True)

_check_offer_price_positive = models.Constraint(
'CHECK(price > 0)',
'Offer price must be positive.',
)

@api.depends("validity", "create_date")
def _compute_date_deadline(self):
for offer in self:
if offer.create_date:
offer.date_deadline = (
offer.create_date.date()
+ relativedelta(days=offer.validity)
)
else:
offer.date_deadline = fields.Date.today() + relativedelta(days=offer.validity)

def _inverse_date_deadline(self):
for offer in self:
if offer.create_date and offer.date_deadline:
offer.validity = (
offer.date_deadline
- offer.create_date.date()
).days

@api.model
def create(self, vals_list):
for vals in vals_list:
property_id = vals.get("property_id")
price = vals.get("price")
property_rec = self.env["estate.property"].browse(property_id)

# Prevent lower offer
existing_prices = property_rec.offer_ids.mapped("price")
if existing_prices and price < max(existing_prices):
raise UserError("You cannot create an offer lower than an existing offer")

# Set property state
property_rec.state = "offer_received"

return super().create(vals_list)

@api.constrains('property_id')
def _check_property_state(self):
for offer in self:
if offer.property_id.state in ('sold', 'cancelled'):
raise ValidationError("You cannot add an offer on a Sold or Cancelled property")

def action_accept(self):
for offer in self:
property_rec = offer.property_id

if property_rec.buyer_id:
raise UserError("Property already accepted")

other_offer = property_rec.offer_ids - offer
other_offer.write({'status': 'refused'})

offer.status = "accepted"
property_rec.write({
'buyer_id': offer.partner_id.id,
'selling_price': offer.price,
'active': False,
})
return True

def action_refuse(self):
for offer in self:
offer.status = "refused"
return True
15 changes: 15 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import fields, models


class EstatePropertyTag(models.Model):
_name = 'estate.property.tag'
_description = 'Estate Property Tag'
_order = 'name'

name = fields.Char(required=True)
color = fields.Integer()

_unique_property_tag_name = models.Constraint(
'UNIQUE(name)',
'Property tag name must be unique.',
)
23 changes: 23 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from odoo import fields, models, api


class EstatePropertyType(models.Model):
_name = 'estate.property.type'
_description = 'Estate Property Type'
_order = 'sequence, name'

name = fields.Char(required=True)
sequence = fields.Integer(default=1)
property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
offer_ids = fields.One2many("estate.property.offer", "property_type_id")
offer_count = fields.Integer(string="Offers", compute="_compute_offer_count")

_unique_property_type_name = models.Constraint(
'UNIQUE(name)',
'Property type name must be unique.',
)

@api.depends("offer_ids")
def _compute_offer_count(self):
for record in self:
record.offer_count = len(record.offer_ids)
21 changes: 21 additions & 0 deletions estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from odoo import api, fields, models


class ResUsers(models.Model):
_inherit = "res.users"

property_ids = fields.One2many(
'estate.property',
'seller_id',
)
total_expected = fields.Float(compute="_compute_total_expected")

@api.depends('property_ids.expected_price', 'property_ids.state')
def _compute_total_expected(self):
sum_expected = dict(self.env['estate.property']._read_group(
domain=[('state', '!=', 'sold'), ('seller_id', 'in', self.ids)],
aggregates=['expected_price:sum'],
groupby=['seller_id']
))
for record in self:
record.total_expected = sum_expected.get(record, 0.0)
7 changes: 7 additions & 0 deletions estate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
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
access_estate_property_maintenance,access_estate_property_maintenance,model_estate_property_maintenance,base.group_user,1,1,1,1
access_estate_property_investor,access_estate_property_investor,model_estate_property_investor,base.group_user,1,1,1,1
Loading