Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c21c5ba
[ADD] estate: initialize the estate app - Chapter 2
Faruk372742 Dec 15, 2025
dd58b67
[ADD] estate: adding estate property table - Chapter 3
Faruk372742 Dec 15, 2025
b4bd38a
[ADD] estate: adding security for estate_property table - Chapter 4
Faruk372742 Dec 15, 2025
923489a
[ADD] estate: adding menus and changing fields - Chapter 5
Faruk372742 Dec 16, 2025
7365fae
[IMP] estate: adding list,form, search views (filters + group by) - C…
Faruk372742 Dec 16, 2025
40660a8
[IMP] estate: adding relations between models - Chapter 7
Faruk372742 Dec 17, 2025
2c59655
[IMP] estate: add computed fields and onchanges - Chapter 8
Faruk372742 Dec 18, 2025
79ff504
[IMP] estate: adding actions to estate property and estate_property_o…
Faruk372742 Dec 18, 2025
5f480f5
[IMP] estate: add SQL and Python constraints - Chapter 10
Faruk372742 Dec 18, 2025
339b8eb
[REF] estate: resolving onboarder's comments
Faruk372742 Dec 18, 2025
a1670fa
[IMP] estate: adding sprinkles to the views - Chapter 11
Faruk372742 Dec 21, 2025
0fec157
[IMP] estate: res.user inheritance for properties - Chapter 12
Faruk372742 Dec 22, 2025
b7c50ca
[REF] estate: removing comments and endlining xmls
Faruk372742 Dec 22, 2025
9fa5a0b
[IMP] estate, estate_account: new link module and invoice creation - …
Faruk372742 Dec 23, 2025
8907fb7
[IMP] estate: kanban view for estate property - Chapter 14
Faruk372742 Dec 23, 2025
7bb016a
[REF] estate: delete unused parts
Faruk372742 Dec 23, 2025
75e9f58
[IMP] awesome_owl: finish chapter 1, learn owl
Faruk372742 Dec 26, 2025
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
13 changes: 13 additions & 0 deletions awesome_owl/static/src/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Component, useState, validate } from "@odoo/owl";

export class Card extends Component {
static template = "awesome_owl.card";
static props = {
isOpened: Boolean,
slots: {
type: Object, shape: {
default: { type: Object, optional: true }
}
}
}
}
7 changes: 7 additions & 0 deletions awesome_owl/static/src/card/cards.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<templates xml:space="preserve">
<t t-name="awesome_owl.card">
<div class="card d-inline-block m-2" style="width: 18rem;">
<t t-slot="default"/>
</div>
</t>
</templates>
13 changes: 13 additions & 0 deletions awesome_owl/static/src/counter/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Component, useState } from "@odoo/owl";

export class Counter extends Component {
static template = "awesome_owl.counter";
static props = {};
setup() {
this.state = useState({ value: 0 })
}
increment() {
this.state.value++;
}

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

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

</templates>
28 changes: 27 additions & 1 deletion awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
import { Component } from "@odoo/owl";
import { markup, Component, useState, useRef, onMounted, reactive } from "@odoo/owl";
import { Counter } from "./counter/counter";
import { Card } from "./card/card";
import { TodoList } from "./todo/todoList/todoList";
import { useAutoFocus } from "./utils";
import { TodoModel } from "./todo/todoModel";

export class Playground extends Component {
static template = "awesome_owl.playground";
static components = { Counter, Card, TodoList };
static props = {};


setup() {
this.todoInput = useRef("todoInputRef");
this.state = useState({ model: new TodoModel(), isOpened: true });
useAutoFocus({ refName: "todoInputRef" })
}
updateSum() {
this.state.sum++;
}
addTodo(ev) {
if (ev.keyCode === 13 && this.todoInput.el.value != "") {
this.state.model.addTodo(this.todoInput.el.value)
this.todoInput.el.value = "";
}
}
toggleIsOpened() {
this.state.isOpened = !this.state.isOpened;
}
}
24 changes: 21 additions & 3 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">

<t t-name="awesome_owl.playground">
<div class="p-3">
hello world
<div class="w-25">
<input
type="text"
class="form-control form-control-sm border-primary text-primary"
placeholder="Add a todo"
t-on-keyup="addTodo"
t-ref="todoInputRef"
/>
</div>
<Card isOpened="state.isOpened">
<div>
<h5>
<p>Card 1</p>
<button t-on-click="toggleIsOpened">Toggle</button>
</h5>
<t t-if="state.isOpened">
<Counter />
</t>
</div>
</Card>
<TodoList model="state"/>
</t>

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

export class TodoItem extends Component {
static template = "awesome_owl.todoItem";
static props = { id: Number, model: Object }
}
20 changes: 20 additions & 0 deletions awesome_owl/static/src/todo/todoItem/todoItem.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.todoItem">
<div style="display: flex; flex-direction:row; width: fit-content; align-items:center; margin-left: 20px">
<t t-set="todo" t-value="props.model.model.getTodo(props.id)"/>
<div style="margin-right: 20px;">
<input type="checkbox" t-on-change="() => props.model.model.toggleState(props.id)" t-att-checked="todo.isCompleted"/>
</div>
<div t-att-class="{'text-muted text-decoration-line-through':todo.isCompleted}">
<t t-esc="props.id"/>
<span>. </span>
<t t-esc="todo.description"/>
<span class="fa fa-trash ms-3 text-danger" t-on-click="() =>
props.model.model.removeTodos(props.id)"/>
</div>
</div>
</t>

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

export class TodoList extends Component {
static template = "awesome_owl.todoList";
static components = { TodoItem };
static props = { model: Object }
}
10 changes: 10 additions & 0 deletions awesome_owl/static/src/todo/todoList/todoList.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.todoList">
<p t-foreach="props.model.model.getTodos()" t-as="item" t-key="item.id">
<TodoItem id="item.id" model="props.model"/>
</p>
</t>

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

export class TodoModel {

constructor() {
this.todos = [];
}

findRealIndex(id) {
return this.todos.findIndex((elem) => elem.id == id)
}
getTodos() {
return this.todos;
}
getTodo(id) {
id = this.findRealIndex(id)
return this.todos[id];
}
addTodo(s) {
this.todos.push(
{ id: this.todos.length === 0 ? 1 : this.todos[this.todos.length - 1].id + 1, description: s, isCompleted: false });

}
toggleState(todoId) {
todoId = this.findRealIndex(todoId);
this.todos[todoId].isCompleted = !this.todos[todoId].isCompleted;
}
removeTodos(todoId) {
todoId = this.findRealIndex(todoId);
this.todos.splice(todoId, 1);
}
}
8 changes: 8 additions & 0 deletions awesome_owl/static/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useEffect, useRef } from "@odoo/owl";

export function useAutoFocus({ refName }) {
const ref = useRef(refName);
useEffect((el) => {
el?.focus();
}, () => [ref.el])
}
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
22 changes: 22 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "estate",
"website": "",
"category": "Tutorials",
"version": "0.1",
"application": True,
"installable": True,
"depends": ["base"],
"data": [
"security/security.xml",
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/res_users_views.xml",
"views/estate_menus.xml",
],
"assets": {},
"author": "Odoo S.A.",
"license": "LGPL-3",
}
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
125 changes: 125 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from odoo import api, fields, models
from dateutil.relativedelta import relativedelta
from datetime import datetime
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare


class EstateProperty(models.Model):
_name = "estate.property"
_description = "Estate Property"
_check_expected_price = models.Constraint(
"CHECK(expected_price > 0)", "The expected price must be positive"
)
_check_selling_price = models.Constraint(
"CHECK(selling_price > 0)", "The selling price must be positive"
)

_order = "id desc"

name = fields.Char("Title", required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
copy=False, default=lambda self: datetime.now() + relativedelta(months=3)
)
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer("Living Area (sqm)")
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
garden_orientation = fields.Selection(
string="Orientation",
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
)
active = fields.Boolean(default=True)
state = fields.Selection(
readonly=True,
string="State",
selection=[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
default="new",
)
type_id = fields.Many2one("estate.property.type", string="Property Type")
salesperson_id = fields.Many2one(
"res.users", string="Salesperson", default=lambda self: self.env.user
)
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
tag_ids = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
total_area = fields.Integer("Total Area (sqm)", compute="_compute_total_area")
best_price = fields.Float("Best Price", compute="_compute_best_price")

@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:
record.best_price = max(record.offer_ids.mapped("price") or [0])

@api.onchange("garden")
def _onchange_garden(self):
if self.garden:
self.garden_area = 10
self.garden_orientation = "north"
else:
self.garden_area = False
self.garden_orientation = False

def action_cancel_property(self):
if self.state == "sold":
raise UserError("Sold property cannot be cancelled!")

for record in self:
record.state = "cancelled"

return True

def action_sell_property(self):
if self.state == "cancelled":
raise UserError("Cancelled property cannot be sold!")

for record in self:
record.state = "sold"

return True

@api.constrains("expected_price", "selling_price")
def _check_selling_price(self):
for record in self:
if len(record.offer_ids) > 0 and (
float_compare(record.selling_price, record.expected_price * 0.9, 5)
== -1
):
raise ValidationError(
"The selling price cannot be lower than the 90% of the expected price"
)

@api.ondelete(at_uninstall=False)
def _unlink_except_few_states(self):
if any(
(record.state != "new" and record.state != "cancelled") for record in self
):
raise UserError(
"You cannot remove the property except when the state is new or cancelled!"
)

def offer_received(self):
if self.state == "new":
self.state = "offer_received"
Loading