Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,13 @@ dmypy.json

# Pyre type checker
.pyre/

# Ruff linter
ruff.toml

# Pre-commit config files
.pre-commit-config.yaml
.pre-commit-hooks.yaml

# Checkstyle file
check-style
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
20 changes: 20 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
'name': 'Real Estate',
'version': '1.9',
'category': 'Real Estates',
'summary': 'Manage real estate operations',
'author': 'Haroune Hassine',
'license': 'LGPL-3',
'depends': [
'base_setup',
],
'application': True,
'installable': True,
'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_menus.xml',
],
}
6 changes: 6 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from . import (
estate_property,
estate_property_offer,
estate_property_tag,
estate_property_type,
)
116 changes: 116 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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
from odoo.tools.translate import _

DEFAULT_GARDEN_AREA = 10


class EstateProperty(models.Model):
_name = "estate.property"
_description = "Real Estate Property."

active = fields.Boolean('Active', default=True)

name = fields.Char('Real Estate Name', required=True)
description = fields.Text('Description')
postcode = fields.Char('Postcode')
date_availability = fields.Date('Availability Date', copy=False, default=fields.Datetime.today() + relativedelta(months=+3))
expected_price = fields.Float('Expected Price', required=True)
selling_price = fields.Float('Selling Price', readonly=True, copy=False)
bedrooms = fields.Integer('Bedrooms', default=2)
living_area = fields.Integer('Living Area (sqm)')
facades = fields.Integer('Facades')
garage = fields.Boolean('Garage')
garden = fields.Boolean('Garden')
garden_area = fields.Integer('Garden Area (sqm)')
garden_orientation = fields.Selection(
string='Garden Orientation',
selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')],
)
state = fields.Selection(
string='State',
selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')],
required=True,
copy=False,
default='new',
)
total_area = fields.Integer(compute='_compute_total_area', string='Total Area (sqm)')
best_price = fields.Float(compute='_compute_best_price', string='Best Price')

sales_person_id = fields.Many2one('res.users', string='Salesman', index=True, default=lambda self: self.env.user)
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False, readonly=True, compute='_compute_buyer')

tag_ids = fields.Many2many('estate.property.tag', string="Tags")

offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')

## CONSTRATINS ##
_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
'The expected price must be strictly positive.',
)

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

@api.constrains('expected_price', 'selling_price')
def _check_expected_vs_selling_price_ratio(self):
for property in self:
if any(offer.status == 'accepted' for offer in property.offer_ids):
if float_compare(property.selling_price, property.expected_price * 0.9, precision_digits=2) < 0:
raise ValidationError(_('The selling price cannot be lower than 90 precent of the expected price: \n Selling Price: %s, Expected Price: %s') % (property.selling_price, property.expected_price))

# or not float_is_zero(abs(property.expected_price * 0.9 - property.selling_price)):
## COMPUTE FUNCTIONS ##

@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.depends('offer_ids.status')
def _compute_buyer(self):
for record in self:
for offer in record.offer_ids:
if offer.status == 'accepted':
record.buyer_id = offer.partner_id
return
record.buyer_id = None

## ONCHAGE FUNCTIONS ##

@api.onchange('garden')
def _onchange_garden(self):
if self.garden:
self.garden_area = DEFAULT_GARDEN_AREA
self.garden_orientation = 'north'
else:
self.garden_area = 0
self.garden_orientation = None

## ACTIONS ##

def action_cancel_property(self):
if self.state == 'sold':
raise UserError(_('Sold properties cannot be canceled.'))
self.state = 'cancelled'
return True

def action_sold_property(self):
if self.state == 'cancelled':
raise UserError(_('Cancelled properties cannot be sold.'))
self.state = 'sold'
return True
62 changes: 62 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from datetime import date

from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.translate import _


class EstateProperOffer(models.Model):
_name = "estate.property.offer"
_description = "Property Offer."

price = fields.Float('Price')
status = fields.Selection(
string='Status',
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('Validity (days)', default=7)
date_deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline', string='Deadline')

## CONSTRAINTS ##

_check_offer_price = models.Constraint(
'CHECK(price > 0)',
'The offer price must be strictly positive.',
)

## COMPUTE FUNCTIONS ##

@api.depends('create_date', 'validity')
def _compute_deadline(self):
for record in self:
if record.create_date:
record.date_deadline = record.create_date + relativedelta(days=+record.validity)
else:
record.date_deadline = date.today() + relativedelta(days=+record.validity)

def _inverse_deadline(self):
for record in self:
record.validity = (record.date_deadline - record.create_date.date()).days

## ACTIONS ##

def action_accept(self):
if self.status != 'accepted':
for offer in self.property_id.offer_ids:
if offer.status == 'accepted':
raise UserError(_('Only one offer can be accepted.'))
self.status = 'accepted'
self.property_id.selling_price = self.price
return True

def action_refuse(self):
if self.status == 'accepted':
self.status = 'refused'
self.property_id.selling_price = None
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 EstateProperTag(models.Model):
_name = "estate.property.tag"
_description = "Property Tag."

name = fields.Char('Property Tag', required=True)

## CONSTRAINTS ##

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


class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Property Type."

name = fields.Char('Property Type', required=True)

## CONSTRAINTS ##

_check_name = models.Constraint(
'UNIQUE(name)',
'The type must be unique.',
)
5 changes: 5 additions & 0 deletions estate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"estate_property_model","estate_property_model","model_estate_property","base.group_user",1,1,1,1
"estate_property_type_model","estate_property_type_model","model_estate_property_type","base.group_user",1,1,1,1
"estate_property_tag_model","estate_property_tag_model","model_estate_property_tag","base.group_user",1,1,1,1
"estate_property_offer_model","estate_property_offer_model","model_estate_property_offer","base.group_user",1,1,1,1
15 changes: 15 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0"?>

<odoo>

<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_properties" name="Advertisements">
<menuitem id="estate_property_model_menu_action" action="load_properties_action"/>
</menuitem>
<menuitem id="settings" name="Settings">
<menuitem id="estate_property_type_model_menu_action" action="property_types_action"/>
<menuitem id="estate_property_tags_model_menu_action" action="property_tags_action"/>
</menuitem>
</menuitem>

</odoo>
Loading