Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5e0797d
[ADD] estate: initialize module structure (Chapter 2)
vikvi-odoo Apr 2, 2026
c222c92
[FIX] estate: Fixed the commit message for estate module (Chapter 2)
vikvi-odoo Apr 2, 2026
f2e7913
[FIX] estate: Fix the space issue in the __init__.py file
vikvi-odoo Apr 2, 2026
b645552
[ADD] estate: implement property module (Chapter 4)
vikvi-odoo Apr 6, 2026
50d2e13
[ADD] estate: add access rights for estate.property (Chapter 4)
vikvi-odoo Apr 6, 2026
33446e0
[ADD] estate: add chapter 5 UI and property fields
vikvi-odoo Apr 8, 2026
25fe23b
[FIX] estate: improve code clarity and consistency
vikvi-odoo Apr 8, 2026
e80d110
[FIX] estate: improve code consistency
vikvi-odoo Apr 8, 2026
cf4d3fa
[ADD] estate: Add list, form, and search views for [chapter 6]
vikvi-odoo Apr 10, 2026
e81d39e
[ADD] estate: establish relational fields for estate models
vikvi-odoo Apr 14, 2026
285c47f
[FIX] estate: add missing description and license
vikvi-odoo Apr 14, 2026
2797b5a
[ADD] estate: add property actions and offer management logic
vikvi-odoo Apr 17, 2026
eefa010
[FIX] estate: add title to Accept and Reject buttons in the offer lis…
vikvi-odoo Apr 17, 2026
3184755
[ADD] estate: implement SQL and Python integrity constraints (Chapter…
vikvi-odoo Apr 20, 2026
e6714e8
[REF] estate: remove unused ValidationError import in property offer
vikvi-odoo Apr 20, 2026
a324897
[IMP] estate: refine constraints and implement automatic offer refusal.
vikvi-odoo Apr 27, 2026
09a857c
[IMP] estate: implement advanced UI features and business logic (Chap…
vikvi-odoo Apr 27, 2026
c8ccaf7
[IMP] estate: implement advanced UI features and business logic (Chap…
vikvi-odoo Apr 28, 2026
671f8ca
[ADD] estate: implement business logic overrides and model inheritanc…
vikvi-odoo Apr 29, 2026
f0f2c0a
[FIX] estate: remove debug print statement from property offer
vikvi-odoo Apr 29, 2026
d57c957
[FIX] estate: remove an unused field
vikvi-odoo Apr 29, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/


shop/
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': 'Estate',
'depends': ['base'],
'category': 'Tutorials',
'application': True,
'installable': True,
'author': 'vikvi',
'license': 'LGPL-3',
'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/res_user_views.xml',
'views/estate_menus.xml'
],
}
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 inherit_res_users
111 changes: 111 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from odoo import _, api, fields, models
from odoo.tools import date_utils, float_compare, float_is_zero

from odoo.exceptions import UserError, ValidationError


class EstateProperty(models.Model):
Comment thread
vikvi-odoo marked this conversation as resolved.
_name = "estate.property"
_description = "Estate Property for purchasing and selling properties"
_order = "id desc"

name = fields.Char(required=True, string="Property Name")
description = fields.Char()
postcode = fields.Char()
date_availibility = fields.Date(copy=False, default=lambda x: fields.Date.today() + date_utils.get_timedelta(3, "month"))
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(
selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West'),

Comment thread
vikvi-odoo marked this conversation as resolved.
],
default='north'
)
state = fields.Selection(
selection=[
('new', 'New'),
('offer received', 'Offer received'),
('offer accepted', 'Offer accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled')
],
default='new'
)
active = fields.Boolean(default=True)
total_area = fields.Integer(compute='_compute_total_area')
best_price = fields.Float(compute='_compute_best_price', store=True)
property_type_id = fields.Many2one("estate.property.type")
tag_ids = fields.Many2many("estate.property.tag", string="Property Tags")
salesperson_id = fields.Many2one('res.users', default=lambda self: self.env.user)
buyer_id = fields.Many2one('res.partner', copy=False)
offer_ids = fields.One2many('estate.property.offer', 'property_id')

Comment thread
vikvi-odoo marked this conversation as resolved.
_check_positive = models.Constraint(
'check(expected_price > 0 )',
'Expected price must be positive'
)

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

@api.depends('offer_ids.price')
def _compute_best_price(self):
for record in self:
prices = record.offer_ids.mapped('price')

if prices:
record.best_price = max(prices)
else:
record.best_price = 0

@api.constrains('selling_price', 'expected_price')
def _check_selling_price_90(self):
if float_is_zero(self.selling_price, precision_digits=2):
return
if float_compare(self.selling_price, 0.9 * self.expected_price, precision_digits=2) == -1:
raise ValidationError(_('Offer price should not be lower then 90% of 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 = ""

@api.ondelete(at_uninstall=False)
def _prevent_property_deletion(self):
if self.state not in ['new', 'cancelled']:
raise UserError(_("You can only delete property if it's in either new or cancelled state"))
return True

def action_sold_property(self):
if self.state != 'offer accepted':
raise UserError(_("The property cannot be set as sold because the offer is not accepted yet."))
return False
if self.state == 'cancelled':
raise UserError(_("A cancelled property cannot be set as sold."))
return False

self.state = 'sold'
return True

def action_cancel_property(self):
if self.state == 'sold':
raise UserError(_("A sold property cannot be set as cancelled."))

self.state = 'cancelled'
return True
80 changes: 80 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from odoo import fields, models, api, _
from datetime import date, timedelta
from odoo.exceptions import UserError


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

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

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

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

def _inverse_validity(self):
for record in self:
create_date = record.create_date.date() if record.create_date else date.today()
record.validity = (record.date_deadline - create_date).days

@api.model
def create(self, vals_list):
if vals_list:
record = self.env['estate.property'].browse(vals_list[0]['property_id'])
if vals_list[0].get("price", 0) < record.best_price:
raise UserError(_("Offer can't be created because current offer's amount is lower than an existing offer"))

if record.state != 'offer received':
record.state = 'offer received'
return super().create(vals_list)

def action_accept_offer(self):
for record in self.property_id.offer_ids:
if record.status == 'accepted':
raise UserError(_('Offer is already accepted'))
if self.property_id.state in ['sold', 'cancelled']:
raise UserError(_('The offer cannot be accepted because the property is already sold or cancelled'))

# method 1:to to refuse all the other offers
(self.property_id.offer_ids - self).status = 'refused'

# method 2
# (self.property_id.offer_ids - self).write({'status': 'refused'})

# method 3
# newl = self.property_id.offer_ids.mapped('id')
# records = self.env['estate.property.offer'].browse(newl) - self
# records.write({'status': 'refused'})

Comment thread
vikvi-odoo marked this conversation as resolved.
self.property_id.selling_price = self.price
self.property_id.buyer_id = self.partner_id
self.status = 'accepted'
self.property_id.state = 'offer accepted'
return True

def action_reject_offer(self):
if self.property_id.state in ['sold', 'cancelled']:
raise UserError(_('The offer cannot be refused because the property is already sold or cancelled'))

self.status = 'refused'
self.property_id.selling_price = 0
self.property_id.buyer_id = False
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)
Comment thread
vikvi-odoo marked this conversation as resolved.
color = fields.Integer()

_unique_tag = models.Constraint(
'unique(name)',
'Property tag should 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('sequence')
property_ids = fields.One2many('estate.property', 'property_type_id')
offer_ids = fields.One2many('estate.property.offer', "property_type_id")
offer_count = fields.Integer(compute='_compute_offer_count')

_unique_type = models.Constraint(
'unique(name)',
'Property type should be unique'
)

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


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

property_ids = fields.One2many('estate.property', 'salesperson_id', domain=[('state', 'in', ['new', 'offer received'])])
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
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
12 changes: 12 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<menuitem id="estate_root_menu" name="Real Estate">
<menuitem id="estate_first_level_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" name="Properties" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_setings_menu" name="Settings">
<menuitem id="estate_property_type_menu_action" name="Property Types" action="estate_property_type_action" />
<menuitem id="estate_property_tag_menu_action" name="Property Tags" action="estate_property_tag_action" />
</menuitem>
</menuitem>
</odoo>
24 changes: 24 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version='1.0' encoding='utf-8'?>

<odoo>
<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">Property Offers</field>
<field name="res_model">estate.property.offer</field>
<field name="view_mode">list,form</field>
<field name="domain">[('property_type_id', '=', active_id)]</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>
<field name="price" />
<field name='partner_id'/>
<field name='property_type_id'/>
<field name='validity'/>
<field name='date_deadline'/>
</list>
</field>
</record>
</odoo>
9 changes: 9 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>

<odoo>
<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">Property Tags</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
52 changes: 52 additions & 0 deletions estate/views/estate_property_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version='1.0' encoding='utf-8'?>

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

<record id="estate_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>
<field name="sequence" widget="handle"/>
<field name='name'/>
</list>
</field>
</record>

<record id="estate_proerty_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>
<sheet>
<div name="button_box">
<button type="action" name="%(estate_property_offer_action)d" icon="fa-usd">
<field name="offer_count" string="Offers" widget="statinfo"/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" />
</h1>
</div>
<notebook>
<page string = "Properties">
<field name="property_ids" readonly="1">
<list >
<field name="name" />
<field name="expected_price" />
<field name="state" string="Status" />
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</odoo>
Loading