Skip to content

Commit

Permalink
[ADD] estate: creating estate module
Browse files Browse the repository at this point in the history
Features :

- Creating properties, types of properties
- Handling customers offers
- Sorting your properties by tags or by type
- Generating invoices when properties are sold and much more...
  • Loading branch information
ouou-odoo committed Oct 24, 2024
1 parent b16e643 commit 5d1ae0f
Show file tree
Hide file tree
Showing 21 changed files with 536 additions and 0 deletions.
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
18 changes: 18 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
'name': 'Real Estate',
'depends': ['base'],
'category': 'Tutorials',
'application': True,
'data': ['data/ir.model.access.csv',
'views/res_users.xml',
'views/estate_property_offer_views.xml',
'views/estate_property_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_menus.xml',
],
'demo': [
"demo/demo.xml",
],
'license': 'AGPL-3'
}
5 changes: 5 additions & 0 deletions estate/data/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
access_estate_prop,access_estate_prop,model_estate_property,base.group_user,1,1,1,1
access_estate_prop_type,access_estate_prop_type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_prop_offer,access_estate_prop_offer,model_estate_property_offer,base.group_user,1,1,1,1
access_estate_prop_tag,access_estate_prop_tag,model_estate_property_tag,base.group_user,1,1,1,1
13 changes: 13 additions & 0 deletions estate/demo/demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<record id="model_estate_property_action_cancel" model="ir.actions.server">
<field name="name">Mass cancel</field>
<field name="model_id" ref="estate.model_estate_property"/>
<field name="binding_model_id" ref="estate.model_estate_property"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">action = records.action_cancel()</field>
</record>
</data>
</odoo>
5 changes: 5 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
91 changes: 91 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare
from odoo.fields import Many2many, One2many

from .helper import format_selection


class EstateProperty(models.Model):
_name = 'estate.property'
_description = 'Estate Property modelisation'
_order = 'id desc'
_sql_constraints = [
('expected_price_strictly_positive', 'CHECK(expected_price > 0)', 'Expected price must be stricly positive'),
('selling_price_positive', 'CHECK(selling_price >= 0)', 'Selling price must be positive'),
]

name = fields.Char(required=True, string="Name")
description = fields.Text(string="Description")
postcode = fields.Char(string="Postal code")
date_availability = fields.Date(default=lambda x: fields.Date.today() + relativedelta(months=3), copy=False,
string='Availability date')
expected_price = fields.Float(required=True, string='Expected price')
selling_price = fields.Float(readonly=True, copy=False, string='Selling price')
bedrooms = fields.Integer(default=2, string='Bedrooms')
living_area = fields.Integer(string='Living area')
facades = fields.Integer(string='Facades')
garage = fields.Boolean(string='Garage')
garden = fields.Boolean(string='Garden')
garden_area = fields.Integer(string='Garden area')
garden_orientation = fields.Selection(string='Orientation',
selection=format_selection(['north', 'south', 'east', 'west']),
)
active = fields.Boolean(default=True, string='Active')
state = fields.Selection(string='State',
selection=format_selection(
['new', 'offer received', 'offer accepted', 'sold', 'canceled']),
default='new')

property_type_id = fields.Many2one('estate.property.type', string='Property type')

buyer_id = fields.Many2one('res.partner', copy=False, string='Buyer')
salesperson_id = fields.Many2one('res.users', default=lambda self: self.env.user, string='Salesperson')

tag_ids = Many2many('estate.property.tag', string='Tags')
offer_ids = One2many('estate.property.offer', 'property_id', string='Offers')

total_area = fields.Integer(compute='_compute_total_area', string='Total area')
best_price = fields.Float(compute='_compute_best_price', string='Best offer price')

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

@api.depends('offer_ids')
def _compute_best_price(self):
for estate in self:
estate.best_price = max(estate.offer_ids.mapped('price'), default=0)

@api.onchange('garden')
def _onchange_garden(self):
self.garden = [0, 10][self.garden]
self.garden_orientation = ['', 'north'][self.garden]

@api.constrains('selling_price')
def _check_price_offer_reasonable(self):
if float_compare(self.selling_price, 0.9 * self.expected_price, 3) <= 0:
raise ValidationError('The selling price must be at least 90% of the expected price.')

@api.ondelete(at_uninstall=False)
def _unlink_property_if_not_new_nor_canceled(self):
self.ensure_one()
if self.state not in ('new', 'canceled'):
raise UserError('Only new and canceled properties can be deleted.')

def action_sold(self):
self.ensure_one()
if self.state == 'canceled':
raise UserError('Canceled properties cannot be sold.')
self.state = 'sold'
return True

def action_cancel(self):
self.ensure_one()
if self.state == 'sold':
raise UserError('Sold properties cannot be canceled.')
self.state = 'canceled'
return True
58 changes: 58 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from dateutil.relativedelta import relativedelta

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

from .helper import format_selection


class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_order = 'price desc'
_description = 'Offer modelisation'
_sql_constraints = [('offer_price_strictly_positive', 'CHECK(price > 0)', 'Offer price must be stricly positive')]

price = fields.Float(string='Price', required=True)
status = fields.Selection(copy=False, selection=format_selection(['accepted', 'refused']), string='Status')
partner_id = fields.Many2one('res.partner', required=True, string='Buyer')
property_id = fields.Many2one('estate.property', required=True, string='Property')

validity = fields.Integer(default=7, string="Validity") # in days
date_deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline', string='Deadline')

property_type_id = fields.Many2one(related='property_id.property_type_id', string='Property type')

@api.depends('validity')
def _compute_deadline(self):
for offer in self:
offer.date_deadline = fields.Date.today() + relativedelta(days=offer.validity)

def _inverse_deadline(self):
for offer in self:
offer.validity = (offer.date_deadline - fields.Date.today()).days

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
property_id = self.env['estate.property'].browse(vals['property_id'])
if any(vals['price'] < offer.price for offer in property_id.offer_ids):
raise UserError('Offer price must be higher than existing offers.')
property_id.state = 'offer received'
return super().create(vals_list)

def action_accept(self):
self.ensure_one()
if self.property_id.state == 'sold':
raise UserError('The house was already sold.')
self.status = 'accepted'
self.property_id.state = 'offer accepted'
self.property_id.buyer_id = self.partner_id
self.property_id.selling_price = self.price
return True

def action_refuse(self):
for offer in self:
if offer.status == 'accepted':
raise UserError('The offer was already accepted.')
offer.status = 'refused'
return True
11 changes: 11 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from odoo import fields, models


class EstatePropertyTag(models.Model):
_name = 'estate.property.tag'
_order = 'name asc'
_description = 'Tag modelisation'
_sql_constraints = [('unique_name', 'unique(name)', 'Tag name must be unique')]

name = fields.Char(required=True, string='Tag Name')
color = fields.Integer(string='Color Index')
19 changes: 19 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from odoo import fields, models


class EstatePropertyType(models.Model):
_name = 'estate.property.type'
_description = 'Estate Property Type modelisation'
_order = 'sequence asc, name asc'
_sql_constraints = [('unique_name', 'unique(name)', 'Type name must be unique')]

name = fields.Char(required=True, string='Name')
property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties')
sequence = fields.Integer(default=1)

offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers')
offer_count = fields.Integer(compute='_compute_offer_count', string='Offer Count')

def _compute_offer_count(self):
for record in self:
record.offer_count = len(record.offer_ids)
2 changes: 2 additions & 0 deletions estate/models/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def format_selection(l: list):
return [(field, field.title()) for field in l]
8 changes: 8 additions & 0 deletions estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import fields, models


class Users(models.Model):
_inherit = 'res.users'

property_ids = fields.One2many('estate.property', 'salesperson_id',
domain=[('state', 'not in', ['sold', 'canceled'])])
8 changes: 8 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<odoo>
<menuitem id="root_menu" name="Real Estate"/>
<menuitem id="ads_menu" name="Advertisements" parent="root_menu"/>
<menuitem id="prop_settings_menu" name="Settings" parent="root_menu"/>
<menuitem id="prop_menu" action="property_action" parent="ads_menu"/>
<menuitem id="prop_type_menu" action="property_type_action" parent="prop_settings_menu"/>
<menuitem id="prop_tag_menu" action="property_tag_action" parent="prop_settings_menu"/>
</odoo>
28 changes: 28 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<odoo>
<record id="property_offer_action" model="ir.actions.act_window">
<field name="name">Property Offers</field>
<field name="res_model">estate.property.offer</field>
<field name="domain">[('property_type_id', '=', active_id)]</field>
<field name="view_mode">list</field>
</record>


<record id="estate_property_offer_view_list" model="ir.ui.view">
<field name="name">estate.property.offer.list</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<list decoration-success="status == 'accepted'"
decoration-danger="status == 'refused'">
<field name="price"/>
<field name="partner_id"/>
<field name="validity" string="Validity (days)"/>
<field name="date_deadline" string="Deadline"/>
<button name="action_accept" type="object" icon="fa-check"
invisible="status"/>
<button name="action_refuse" type="object" icon="fa-times"
invisible="status"/>
<field name="status" string="Status"/>
</list>
</field>
</record>
</odoo>
7 changes: 7 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<odoo>
<record id="property_tag_action" model="ir.actions.act_window">
<field name="name">Properties Tags</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list</field>
</record>
</odoo>
51 changes: 51 additions & 0 deletions estate/views/estate_property_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<odoo>
<record id="property_type_view_list" model="ir.ui.view">
<field name="name">estate.property.type.list</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<list string="Property Type List">
<field name="sequence" widget="handle"/>
<field name="name"/>
</list>
</field>
</record>

<record id="property_type_view_form" model="ir.ui.view">
<field name="name">estate.property.type.form</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<form string="Property Form">
<sheet>
<div name="button_box" position="inside">
<button class="oe_stat_button" name="%(action_property_offer)d" type="action"
string="View Offers"
icon="fa-money">
<field string="Offers" name="offer_count" widget="statinfo"/>
</button>
</div>
<group>
<field name="name"/>
</group>

<notebook>
<page string="Properties">
<field name="property_ids">
<list>
<field name="name" string="Title"/>
<field name="expected_price"/>
<field name="state"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>

<record id="property_type_action" model="ir.actions.act_window">
<field name="name">Properties Types</field>
<field name="res_model">estate.property.type</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
Loading

0 comments on commit 5d1ae0f

Please sign in to comment.