Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1d9543b
[ADD] estate: create a new module for Real Estate Advertisement
anden-odoo Jan 19, 2026
03dde4d
[IMP] estate: add property model
anden-odoo Jan 19, 2026
b6e4d06
[IMP] estate: add informations field
anden-odoo Jan 19, 2026
b2ac4fa
[CLN] estate: make code more conventional
anden-odoo Jan 20, 2026
bfd650f
[IMP] give access rights to base.group_user
anden-odoo Jan 20, 2026
3490325
[IMP] estate: add a 3 levels architecture and an action (Chapter 5)
anden-odoo Jan 20, 2026
b330f0e
[IMP] estate: change attributes of some fields
anden-odoo Jan 20, 2026
c7b8c7f
[IMP] estate: add new fields and attributes
anden-odoo Jan 20, 2026
e97d000
[CLN] estate: clean some indentations and useless lines
anden-odoo Jan 20, 2026
f7fb822
[IMP] estate: custom view for list and form (Chapter 6)
anden-odoo Jan 20, 2026
00761e0
[IMP] estate: add search options (chapter 6)
anden-odoo Jan 20, 2026
2b04b99
[IMP] estate: add new module for property type (Chapter 7)
anden-odoo Jan 20, 2026
476383d
[IMP] estate: add salesperson and buyer as field
anden-odoo Jan 21, 2026
af15929
[IMP] estate: add tags to property model (Chapter 7)
anden-odoo Jan 21, 2026
39246b2
[IMP] estate: add offer model (Chapter 7)
anden-odoo Jan 21, 2026
4d9b290
[IMP] estate: add total_area and best_offer fields (chapter 8)
anden-odoo Jan 21, 2026
7e72d0a
[IMP] estate: add fields for validity and deadline (Chapter 8)
anden-odoo Jan 21, 2026
94a7e2e
[CLN] estate: clean fields declaration
anden-odoo Jan 21, 2026
71bbdcd
[IMP] estate: add buttons to property form view (Chapter 9)
anden-odoo Jan 21, 2026
c72754d
[FIX] estate: solve the computation of the selling price (Chapter 9)
anden-odoo Jan 22, 2026
abd4ecb
[IMP] estate: add constraints in property and offer (Chapter 10)
anden-odoo Jan 22, 2026
c90db3e
[IMP] estate: add inline list view in type form (Chapter 11)
anden-odoo Jan 22, 2026
6ce2304
[CLN] estate: clean code to respect robodoo recommendations
anden-odoo Jan 22, 2026
b9fc1dc
[IMP] estate: add new views and ordering (Chapter 11)
anden-odoo Jan 22, 2026
d9e02b1
[IMP] estate: change the state view depending on the offers (chapter 11)
anden-odoo Jan 22, 2026
87989e0
[IMP] estate: add options and color (chapter 11)
anden-odoo Jan 22, 2026
6fed5bd
[IMP] estate: add invisible fields (Chapter 11)
anden-odoo Jan 22, 2026
02774a3
[IMP] estate: add editable list views and optional field (Chapter 11)
anden-odoo Jan 23, 2026
e93b659
[IMP] estate: add decorations in some views (Chapter 11)
anden-odoo Jan 23, 2026
88975fc
[IMP] estate: add custom research (Chapter 11)
anden-odoo Jan 23, 2026
728c4b4
[IMP] estate: add stat button for property type (Chapter 11)
anden-odoo Jan 26, 2026
7a27ef5
[IMP] estate: prevent deletion for some property states (Chapter 12)
anden-odoo Jan 26, 2026
da2d02d
[IMP] add properties to users form (Chapter 12)
anden-odoo Jan 26, 2026
b606da3
[IMP] estate_account: add automatic invoice (Chapter 13)
anden-odoo Jan 26, 2026
a74c840
[CLN] estate: clean a white space
anden-odoo Jan 26, 2026
55edb0e
[IMP] estate: add Kanban view to property view (Chapter 14)
anden-odoo Jan 26, 2026
5c46462
[IMP] awesome_owl: add Counter in a sub component (Chapter 1)
anden-odoo Jan 26, 2026
284a46c
[IMP] awesome_owl: add Card component (Chapter 1)
anden-odoo Jan 27, 2026
1709e79
[IMP] awesome_owl: add markup support to card (Chapter 1)
anden-odoo Jan 27, 2026
daee9fb
[IMP] awesome_owl: add props validation for Card (Chapter 1)
anden-odoo Jan 27, 2026
1088fe5
[IMP] awesome_owl: add the sum of two Counter (Chapter 1)
anden-odoo Jan 27, 2026
25fe0a4
[IMP] awesome_owl: add todo list with dynamic attributes (Chapter 1)
anden-odoo Jan 27, 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/

# Not functionnal
estate/security/security.xml
12 changes: 12 additions & 0 deletions awesome_owl/static/src/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Component } from "@odoo/owl";


export class Card extends Component {
static template = "awesome_owl.card";

static props = {
title: {type: String,},
content: {type: String,},
};
Comment on lines +7 to +10

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static props = {
title: {type: String,},
content: {type: String,},
};
static props = {
title: {type: String,},
content: {type: String,},
};


}
13 changes: 13 additions & 0 deletions awesome_owl/static/src/card/card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_owl.card">
<div class="card d-inline-block m-2" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title"><t t-esc="props.title"/></h5>
<p class="card-text"><t t-out="props.content"/></p>
</div>
</div>
</t>

</templates>
20 changes: 20 additions & 0 deletions awesome_owl/static/src/counter/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component, useState } from "@odoo/owl";


export class Counter extends Component {
static template = "awesome_owl.counter";

static props = {
onChange: {type: Function, optional: true,},
};
Comment on lines +7 to +9

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static props = {
onChange: {type: Function, optional: true,},
};
static props = {
onChange: {type: Function, optional: true,},
};


setup() {
this.state = useState({ value: 0 });
}

increment() {
this.state.value++;
if (typeof this.props.onChange === "function")
this.props.onChange()
}
}
12 changes: 12 additions & 0 deletions awesome_owl/static/src/counter/counter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_owl.counter">
<div class="p-3">
hello world
<p>Counter: <t t-esc="state.value"/></p>
<button class="btn btn-primary" t-on-click="increment">Increment</button>
</div>
</t>

</templates>
24 changes: 23 additions & 1 deletion awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import { Component } from "@odoo/owl";
import { Component, useState, markup } from "@odoo/owl";
import { Counter } from "./counter/counter";
import { Card } from "./card/card";
import { TodoList } from "./todo/todo_list";
import { TodoItem } from "./todo/todo_item";


const exampleHtml = "<div class='text-primary'>some content</div>";

export class Playground extends Component {
static template = "awesome_owl.playground";

static components = { Counter, Card, TodoItem, TodoList };
// static components = {Counter, Card};

html = exampleHtml;
markupHtml = markup(exampleHtml);

setup() {
this.state = useState({ sum: 0 });
}

onChange(){
this.state.sum ++;
}

}
19 changes: 17 additions & 2 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,24 @@
<templates xml:space="preserve">

<t t-name="awesome_owl.playground">
<div class="p-3">
hello world
<Counter/>
<Counter/>

<Card title="'Card 1'" content="'content of card 1'"/>
<Card title="'Card 2'" content="'content of card 2'"/>

<Card title="'Card 1'" content="html"/>
<Card title="'Card 2'" content="markupHtml"/>

<div class="p-3">
<Counter onChange.bind="onChange"/>
<Counter onChange.bind="onChange"/>
<p>The sum is: <t t-esc="state.sum"/></p>
</div>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

<!-- <TodoList todos="[{ id: 2, description: 'write tutorial', isCompleted: true }, { id: 3, description: 'buy milk', isCompleted: false }]"/> -->
<TodoList/>

</t>

</templates>
17 changes: 17 additions & 0 deletions awesome_owl/static/src/todo/todo_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Component } from "@odoo/owl";


export class TodoItem extends Component {

static template = "awesome_owl.todo_item";

static props = {
todo: {type: Object,
shape: {
id: Number,
description: String,
isCompleted: Boolean,
},
},
};
}
10 changes: 10 additions & 0 deletions awesome_owl/static/src/todo/todo_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_owl.todo_item">
<div t-att-class="props.todo.isCompleted ? 'text-muted text-decoration-line-through' : ''">
<t t-esc="props.todo.id"/>. <t t-esc="props.todo.description"/>
</div>
</t>

</templates>
25 changes: 25 additions & 0 deletions awesome_owl/static/src/todo/todo_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Component, useState } from "@odoo/owl";
import { TodoItem } from "./todo_item"


export class TodoList extends Component {
static template = "awesome_owl.todo_list";

static components = { TodoItem };

todos = useState([]);

setup() {
this.state = useState({ id_counter: 0 });
}


addTodo(ev){
if (ev.keyCode === 13 && ev.target.value !== ""){
this.todos.push({ id: this.state.id_counter, description: ev.target.value, isCompleted: false });
this.state.id_counter++;
ev.target.value = "";
}
}

}
20 changes: 20 additions & 0 deletions awesome_owl/static/src/todo/todo_list.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_owl.todo_list">
<div class="p-3">
<div class="mb-2">
<input
type="text"
class="form-control"
placeholder="Add a todo"
t-on-keyup="addTodo"
/>
</div>
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<TodoItem todo="todo"/>
</t>
</div>
</t>

</templates>
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',
'author': 'anden',
'license': 'LGPL-3',
'depends': ['base'],
'category': 'Real Estate/Brokerage',
'application': True,
'data': [
# 'security/security.xml',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# 'security/security.xml',

'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_users_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 property
from . import property_type
from . import property_tag
from . import property_offer
from . import res_users
106 changes: 106 additions & 0 deletions estate/models/property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from odoo import api, fields, models, exceptions
from odoo.tools.float_utils import float_compare, float_is_zero


class EstateProperty(models.Model):
_name = 'estate_property'
_description = 'estate property'
_order = "id desc"

name = fields.Char(string='Title', required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(string='Available From', copy=False, default=fields.Date.add(fields.Date.today(), months=3))
expected_price = fields.Float(required=True)

_check_expected_price = models.Constraint(
'CHECK(0 < expected_price)',
'A property expected price must be strictly positive')

selling_price = fields.Float(readonly=True, copy=False)

_check_selling_price = models.Constraint(
'CHECK(0 <= selling_price)',
'A property selling price must be positive')

@api.constrains('selling_price', 'expected_price')
def _check_selling_price(self):
for record in self:
if not float_is_zero(record.selling_price, 2):
if float_compare(record.selling_price, 0.9 * record.expected_price, 2) == -1:
raise exceptions.ValidationError("The selling price cannot be lower than 90% of the expected price.")

bedrooms = fields.Integer(default=2)
living_area = fields.Integer(string='Living Area (sqm)')
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer(string='Garden Area (sqm)')
garden_orientation = fields.Selection(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')])
active = fields.Boolean(default=True)
state = fields.Selection(
string='Status',
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')
salesperson = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user)
buyer = fields.Many2one('res.partner', copy=False)
property_tag_id = fields.Many2many('estate_property_tag', string='Property Tag')
property_offer_id = fields.One2many('estate_property_offer', 'property_id', string='Property Offer')
total_area = fields.Float(string='Total Area (sqm)', compute='_compute_area')
best_price = fields.Float(string='Best offer', compute='_compute_best_price')

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

@api.depends('property_offer_id.price')
def _compute_best_price(self):
for record in self:
record.best_price = max(record.property_offer_id.mapped('price'), default=False)

@api.onchange('garden')
def _onchange_garden(self):
for record in self:
if record.garden:
record.garden_area = 10
record.garden_orientation = 'north'
else:
record.garden_area = 0
record.garden_orientation = False

def action_property_sold(self):
if self.state == 'cancelled':
raise exceptions.UserError('Cancelled properties cannot be sold')
else:
self.state = 'sold'

def action_property_cancel(self):
if self.state == 'sold':
raise exceptions.UserError('Sold properties cannot be cancelled')
else:
self.state = 'cancelled'

@api.onchange('property_offer_id')
def _onchange_property_offer_id(self):
for record in self:
if len(record.property_offer_id) > 0:
if record.state == 'new':
record.state = 'offer received'
elif record.state != 'cancelled':
record.state = 'new'

@api.ondelete(at_uninstall=False)
def _unlink_for_specific_state(self):
if any(record.state not in ('new', 'cancelled') for record in self):
raise exceptions.UserError('Can\'t delete a property if its state is not \'New\' or \'Cancelled\'')
65 changes: 65 additions & 0 deletions estate/models/property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from odoo import api, fields, models, exceptions


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

price = fields.Float()
status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False, readonly=True)
partner_id = fields.Many2one('res.partner', required=True, string='Partner')
property_id = fields.Many2one('estate_property', required=True)
validity = fields.Integer(string='Validity (days)', default=7)
date_deadline = fields.Date(string='Deadline', compute='_compute_deadline', inverse='_inverse_deadline')
property_type_id = fields.Many2one("estate_property_type", related="property_id.property_type_id", store=True, string="Property Type")

@api.depends('create_date', 'validity')
def _compute_deadline(self):
for record in self:
if record.create_date:
record.date_deadline = fields.Date.add(record.create_date.date(), days=record.validity)
else:
record.date_deadline = fields.Date.add(fields.Date.today(), days=record.validity) # If no create_date we take the date of today

def _inverse_deadline(self):
for record in self:
if record.create_date:
record.validity = (record.date_deadline - record.create_date.date()).days
else:
record.validity = (record.date_deadline - fields.Date.today()).days # If no create_date we take the date of today

def action_status_accepted(self):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

if len(self) > 1:
raise exceptions.UserError('Only one offer can be accepted')

if self.status != 'accepted' and 'accepted' in self.mapped('property_id.property_offer_id.status'):
raise exceptions.UserError('Another offer is already accepted')

self.status = 'accepted'
self.property_id.selling_price = self.price
self.property_id.buyer = self.partner_id
self.property_id.state = 'offer accepted'

def action_status_refused(self):
for record in self:
if record.status == 'accepted':
record.property_id.selling_price = False
record.property_id.buyer = False
record.property_id.state = 'offer received'
record.status = 'refused'

_check_offer_price = models.Constraint(
'CHECK(0 < price)',
'An offer price must be strictly positive')
Comment on lines +53 to +55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here


@api.model
def create(self, vals_list):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

for vals in vals_list:
max_existing_price = max((offer.price for offer in self.env['estate_property'].browse(vals_list[0]['property_id']).property_offer_id), default=0)
if vals['price'] < max_existing_price:
raise exceptions.UserError('The offer must be higher than ' + str(max_existing_price))

return super().create(vals_list)
Loading