Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
df8d524
[ADD] estate: create the app
jugurtha-gaci Oct 20, 2025
b84e916
[ADD] estate: chapter 3
jugurtha-gaci Oct 20, 2025
c3c8c66
[ADD] estate: define access rights (chapter 4)
jugurtha-gaci Oct 20, 2025
223bbb6
[ADD] estate: create the actions and views (chapter 5) && [FIX] fix P…
jugurtha-gaci Oct 21, 2025
c7523f7
[ADD] estate: customize the view (chapter 6)
jugurtha-gaci Oct 21, 2025
094f139
[IMP] estate: Add notes.
Mathilde411 Oct 21, 2025
a9e95fd
[ADD] estate: create relationship tables : offers, tags, types - (cha…
jugurtha-gaci Oct 22, 2025
ed6ba56
[ADD] estate: implement compute, inverse, and onchange methods && [IM…
jugurtha-gaci Oct 22, 2025
7b1a091
[ADD] estate: add actions (chapter 9) && [FIX] estate: fix runbot errors
jugurtha-gaci Oct 22, 2025
21a1729
[FIX] fix runbot errors
jugurtha-gaci Oct 22, 2025
0b46628
[FIX] fix runbot errors
jugurtha-gaci Oct 22, 2025
c8c8d80
[FIX] fix runbot errors
jugurtha-gaci Oct 22, 2025
491c499
[ADD] estate: add constraints (chapter 10)
jugurtha-gaci Oct 22, 2025
e165dc8
[ADD] estate: chapter 11 & 12
jugurtha-gaci Oct 24, 2025
e0ef55f
[IMP] fix runbot errors
jugurtha-gaci Oct 24, 2025
7e1411d
[FIX] fix runbot errors
jugurtha-gaci Oct 24, 2025
02054ed
[FIX] estate: fix runbot errors
jugurtha-gaci Oct 24, 2025
5fc2e02
[ADD] estate: create invoices for sold properties (chapter 13)
jugurtha-gaci Oct 24, 2025
f6db4a4
[IMP] estate: add the kanban view for the estate properties (chapter 14)
jugurtha-gaci Oct 24, 2025
3d7c3b6
[IMP] estate: refactoring
jugurtha-gaci Oct 27, 2025
1e70ae4
[FIX] estate: fix runbot errors
jugurtha-gaci Oct 27, 2025
afa71ba
[FIX] estate: fix runbot errors
jugurtha-gaci Oct 27, 2025
ae39b49
[ADD] awesome_owl: finish all the chapters
jugurtha-gaci Oct 28, 2025
5342ef1
[FIX] awesome_owl: fix toggle state
jugurtha-gaci Oct 28, 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
20 changes: 20 additions & 0 deletions awesome_owl/static/src/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component, useState } from "@odoo/owl";


export class Card extends Component {
static template = "awesome_owl.card.card";
static props = {
title: {type: String},
slots: {
type: Object,
shape: {
default: {}
}
}
}

setup() {
this.state = useState({visible: true})
}

}
17 changes: 17 additions & 0 deletions awesome_owl/static/src/card/card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_owl.card.card">
<button t-on-click="() => this.state.visible = !this.state.visible">toggle visibility</button>
<t t-if="this.state.visible">
<div
class="card d-inline-block m-2"
style="display: block; border: 1px solid #ccc; padding: 20px;">

<div class="card-body">
<h2 class="card-title" t-esc="props.title"></h2>
<t t-slot="default" />
</div>
</div>
</t>
</t>
</templates>
17 changes: 17 additions & 0 deletions awesome_owl/static/src/counter/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Component, useState } from "@odoo/owl"


export class Counter extends Component {
static template = "awesome_owl.counter.counter"
static props = ['onchange?']

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

increment() {
this.counter.value++;
if(this.props.onchange != null && this.props.onchange != undefined)
this.props.onchange()
}
}
17 changes: 17 additions & 0 deletions awesome_owl/static/src/counter/counter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_owl.counter.counter">
<div style="border: 1px solid #ccc; display: inline-block; min-width: 300px; padding: 20px">

<p>Counter: <t t-esc="counter.value"/></p>

<button
style="background: purple; color: white; padding: 8px; border-radius: 5px; border: 0; outline: none"
class="btn btn-primary"
t-on-click="increment">
Increment
</button>

</div>
</t>
</templates>
12 changes: 6 additions & 6 deletions awesome_owl/static/src/main.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { whenReady } from "@odoo/owl";
import { mountComponent } from "@web/env";
import { Playground } from "./playground";
import { whenReady } from "@odoo/owl"
import { mountComponent } from "@web/env"
import { Playground } from "./playground"


const config = {
dev: true,
name: "Owl Tutorial"
};
}

// Mount the Playground component when the document.body is ready
whenReady(() => mountComponent(Playground, document.body, config));

whenReady(() => mountComponent(Playground, document.body, config))
14 changes: 13 additions & 1 deletion awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { Component } from "@odoo/owl";
import { Component, useState } from "@odoo/owl";
import { Counter } from "./counter/counter"
import { Card } from "./card/card"
import { TodoList } from "./todo/list";


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

setup() {
this.sum = useState({value: 2})
}
incrementSum() {
this.sum.value += 1
}
}
21 changes: 19 additions & 2 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_owl.playground">
<div class="p-3">
hello world
<div>
<Counter onchange.bind="incrementSum" />
<Counter onchange.bind="incrementSum" />
<h2> SUM is <t t-esc="sum.value"></t> </h2>
</div>

<div style="margin: 15px 0; display : flex">
<Card title="'this is the card 1'">
<Counter />
</Card>
<Card title="'this is the card 2'">
<p>hello</p>
</Card>
</div>

<div style="margin: 15px 0">
<h2> My todo list for this week </h2>
<TodoList />
</div>
</div>
</t>

Expand Down
7 changes: 7 additions & 0 deletions awesome_owl/static/src/todo/item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Component } from "@odoo/owl"


export class TodoItem extends Component {
static template = "awesome_owl.todo.item"
static props = ['todo', 'toggleState', 'deleteTodo']
}
10 changes: 10 additions & 0 deletions awesome_owl/static/src/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">
<input type="checkbox" t-on-change="props.toggleState" />
<span t-esc="props.todo.description" t-att-style="props.todo.isCompleted ? 'text-decoration: line-through' : ''"></span>
<button t-on-click="() => props.deleteTodo(props.todo.id)" style="color: red; margin-left: 15px; font-weight: bold">x</button>
</t>

</templates>
33 changes: 33 additions & 0 deletions awesome_owl/static/src/todo/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Component, useState, useRef, onMounted } from "@odoo/owl"
import { TodoItem } from "./item"
import { useAutofocus } from './../utils'


export class TodoList extends Component {
static template = "awesome_owl.todo.list"
static components = {TodoItem}

setup() {
this.todos = useState([])
useAutofocus('todoInputRef')
}
addTodo(ev) {
if(ev.keyCode == '13' && ev.target.value != '') {
this.todos.push({
id: this.todos.length + 1,
description: ev.target.value,
isCompleted: false
})
ev.target.value = ''
}
}
toggleState(id) {
const [res] = this.todos.filter(todo => todo.id == id)
res.isCompleted = !res.isCompleted

}
deleteTodo(id) {
const [res] = this.todos.filter(todo => todo.id == id)
this.todos.splice(this.todos.indexOf(res), 1)
}
}
20 changes: 20 additions & 0 deletions awesome_owl/static/src/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">
<input t-ref="todoInputRef" placeholder="Add a todo" t-on-keyup="addTodo" style="padding: 13px; border-radius: 5px; min-width: 200px"/>
<ol>
<li t-foreach="todos" t-as="todo" t-key="todo.id">
<TodoItem
todo="todo"
toggleState.bind="() => this.toggleState(todo.id)"
deleteTodo.bind="() => this.deleteTodo(todo.id)"
/>
</li>
</ol>
<p>
You have <t t-esc="todos.length" /> tasks to do
</p>
</t>

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


const useAutofocus = (refName) => {
const ref = useRef(refName)
onMounted(() => ref.el.focus())
}


export {
useAutofocus
}
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
17 changes: 17 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
'name': "Estate",
'version': '1.0',
'depends': ['base'],
'author': "GACI Jugurtha (jugac)",
'application': True,
'license': 'LGPL-3',
'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_offer_views.xml',
'views/estate_menus.xml',
'views/res_users_views.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 res_users
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 odoo import models, api
from odoo.fields import Char, Text, Html, Float, Integer, Date, Boolean, Selection, Many2many, Many2one, One2many
from odoo.tools import float_compare, float_is_zero
from odoo.exceptions import UserError, ValidationError


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

name = Char(required=True)
description = Text()
notes = Html()
postcode = Char()
date_availability = Date(
string="Available From",
copy=False,
default=lambda self: Date.add(Date.today(), months=3)
)

expected_price = Float(required=True)
selling_price = Float(copy=False)
best_offer = Float(copy=False, compute="_compute_best_offer")

bedrooms = Integer(default=2)
living_area = Integer(string="Living Area (sqm)")
total_area = Integer(
compute="_compute_total_area",
store=True,
string="Total Area (sqm)",
)
facades = Integer()
garage = Boolean()

garden = Boolean()
garden_area = Integer()
garden_orientation = Selection(
selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West')
]
)

active = Boolean(default=True)
state = Selection(
selection=[
('new', 'New'),
('received', 'Offer Received'),
('accepted', 'Offer Accepted'),
('sold', 'Sold'),
('canceled', 'Canceled')
],
default="new"
)

# relations
property_type_id = Many2one("estate.property.type")
buyer_id = Many2one("res.partner")
salesman_id = Many2one("res.users", default=lambda self: self.env.user)
tag_ids = Many2many("estate.property.tag")
offer_ids = One2many("estate.property.offer", "property_id")

@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_offer(self):
for record in self:
record.best_offer = max((offer.price for offer in record.offer_ids), default=0)

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

def action_set_sold(self):
for record in self:
if record.state == 'canceled':
raise UserError('A canceled property cannot be sold')
record.state = 'sold'
return True

def action_set_canceled(self):
for record in self:
if record.state == 'sold':
raise UserError('A sold property cannot be canceled')
record.state = 'canceled'
return True

_check_expected_price = models.Constraint(
'CHECK(expected_price >= 0)',
'The expected price of the property should be strictly postitive',
)

@api.constrains('selling_price')
def _check_selling_price(self):
for record in self.filtered(lambda p: p.state not in ("new", "received")):

if float_compare(record.selling_price, (record.expected_price * 0.9), 2) < 0:
raise ValidationError("The selling price cannot be lower than 90%% of the expected price")

if float_is_zero(record.selling_price, 2):
raise ValidationError("The selling price should be positive")

@api.ondelete(at_uninstall=False)
def _unlink_property(self):
if self.state not in ('new', 'canceled'):
raise UserError("This property can't be deleted")

return super().unlink()
Loading