Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
15bd461
[ADD] estate: property model with fields, views, and menu access
karimgamaleldin Feb 16, 2026
8fa7278
[IMP] estate: property types, tags, and offers
karimgamaleldin Feb 17, 2026
fa5ac28
[IMP] estate: computed fields and onchange methods
karimgamaleldin Feb 17, 2026
06ea437
[IMP] estate: action methods and offer management buttons
karimgamaleldin Feb 17, 2026
24285ef
[IMP] estate: SQL constraints and validation rules
karimgamaleldin Feb 17, 2026
7984802
[FIX] estate: compute default availability date at record creation time
karimgamaleldin Feb 17, 2026
d5ad3b1
[CLN] estate: add missing newlines at end of files
karimgamaleldin Feb 17, 2026
0b3bd09
[CLN] estate: rename constraints to avoid ambiguity
karimgamaleldin Feb 17, 2026
17fdb75
[FIX] estate: prevent accepting multiple offers
karimgamaleldin Feb 17, 2026
6f3ba59
[IMP] estate: add UI sprinkles and improve usability
karimgamaleldin Feb 18, 2026
b5dc994
[CLN] estate: clean up code to comply with linting standards
karimgamaleldin Feb 18, 2026
c62c4eb
[IMP] estate: add business logic to CRUD and extend users view
karimgamaleldin Feb 18, 2026
2eb4dc8
[ADD] estate_account: new module to generate invoices on property sale
karimgamaleldin Feb 18, 2026
222c565
[REF] estate: refactor salesperson model and views to res.users
karimgamaleldin Feb 18, 2026
51c0e8b
[IMP] estate: add kanban view with grouping and conditional fields
karimgamaleldin Feb 18, 2026
10f2794
[ADD] awesome_owl: implement owl framework components and todo list
karimgamaleldin Feb 19, 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
1 change: 1 addition & 0 deletions awesome_owl/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap_backend'),
('include', 'web._assets_bootstrap'),
('include', 'web._assets_core'),
'web/static/src/libs/fontawesome/css/font-awesome.css',
Expand Down
23 changes: 23 additions & 0 deletions awesome_owl/static/src/components/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Component, useState } from "@odoo/owl";

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

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

toggleContent(){
this.state.renderContent = !this.state.renderContent;
}

}
14 changes: 14 additions & 0 deletions awesome_owl/static/src/components/card/card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?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">
<span t-att-class="state.renderContent ? 'fa fa-eye-slash' : 'fa fa-eye' " t-on-click="toggleContent"> </span>
<h5 class="card-title"><t t-out="props.title"/></h5>
<t t-slot="default" t-if="state.renderContent"/>
</div>
</div>
</t>

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

export class Counter extends Component {
static template = "awesome_owl.counter";
static props = {
onChange: {type: Function, optional: true},
};

static defaultProps = {
onChange: () => {},
};

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

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

<t t-name="awesome_owl.counter">
<div class="m-2 p-2 border d-inline-block">
<span class="me-2">Counter: <t t-esc="state.value"/></span>
<button class="btn btn-primary" t-on-click="increment">Increment</button>
</div>
</t>

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

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

static props = {
todo: {
type: Object,
shape: {
id: Number,
title: String,
isCompleted: Boolean,
}
},
toggleTodo: Function,
deleteTodo: Function
};

onToggle() {
this.props.toggleTodo(this.props.todo.id);
}

onDelete() {
this.props.deleteTodo(this.props.todo.id);
}

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

<t t-name="awesome_owl.TodoItem">
<div>
<label t-att-class="props.todo.isCompleted ? 'text-decoration-line-through text-muted' : '' ">
<input class="form-check-input" type="checkbox" t-on-change="onToggle"/>
<t t-esc="props.todo.id" />
.
<t t-esc="props.todo.title" />
<span class="fa fa-remove" t-on-click="onDelete"/>
</label>
</div>
</t>

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

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

static components = { TodoItem };

setup () {
this.todos = useState([]);
this.ids = useState({"last_id": 0});
useAutoFocus("input");
}

addTodo (ev) {
if (ev.keyCode === 13 && ev.target.value) {
this.ids.last_id += 1
this.todos.push({
id: this.ids.last_id,
title: ev.target.value,
isCompleted: false,
});
ev.target.value = "";
}
}

toggleTodo(todo_id) {
const index = this.todos.findIndex(t => t.id === todo_id);
if (index !== -1) {
this.todos[index].isCompleted = !this.todos[index].isCompleted;
}
}

deleteTodo(todo_id){
const index = this.todos.findIndex(t => t.id === todo_id);
if (index !== -1) {
this.todos.splice(index, 1);
}
}

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

<t t-name="awesome_owl.TodoList">
<div class="m-2 p-2 border">
<h1>Todo List</h1>
<input class="form-control mb-3" type="text" placeholder="Enter a new task." t-on-keyup="addTodo" t-ref="input"/>
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<TodoItem todo="todo" toggleTodo.bind="toggleTodo" deleteTodo.bind="deleteTodo"/>
</t>
</div>
</t>

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

export function useAutoFocus (refName) {
const ref = useRef(refName);
onMounted(() => {
ref.el.focus();
});
}
1 change: 0 additions & 1 deletion awesome_owl/static/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ const config = {

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

18 changes: 17 additions & 1 deletion awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import { Component } from "@odoo/owl";
import { Component, markup, useState } from "@odoo/owl";
import { Counter } from "./components/counter/counter";
import { Card } from "./components/card/card";
import { TodoList } from "./components/todo_list/todo_list";

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

static components = { Counter, Card, TodoList };

value1 = "<b>Some HTML content</b>";
value2 = markup("<b>Some other HTML content</b>");

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

onCounterChange () {
this.state.sum += 1;
}
}
13 changes: 12 additions & 1 deletion awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@

<t t-name="awesome_owl.playground">
<div class="p-3">
hello world
<div>
<Counter onChange.bind="onCounterChange"/>
<Counter onChange.bind="onCounterChange"/>
<p>The sum is: <t t-esc="state.sum"/></p>
</div>
<Card title="'Card 1'">
Description for Card 1
</Card>
<Card title="'Card 2'">
<Counter />
</Card>
<TodoList/>
</div>
</t>

Expand Down
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
21 changes: 21 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

{
'name': 'estate',
'depends': [
'base',
],
"author": "Karim Gamaleldin",
"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_offers_views.xml",
"views/res_users_views.xml",
"views/estate_property_menus.xml",
"views/estate_property_type_menus.xml",
"views/estate_property_tag_menus.xml",
],
'application': True,
}
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
122 changes: 122 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare, float_is_zero


def get_date_in_3_months() -> fields.Date:
'''
This function calculates the date that is three months from the current date.

returns:
A date object representing the date that is three months from today.
'''
today_data = fields.Date.today()
three_months_later = fields.Date.add(today_data, months=3)
return three_months_later


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

name = fields.Char(required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(copy=False, default=lambda self: get_date_in_3_months())
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([
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West'),
])
state = fields.Selection([
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('canceled', 'Canceled'),
], default="new", required=True, copy=False)
active = fields.Boolean(default=True)

total_area = fields.Integer(compute="_compute_total_area")
best_offer = fields.Float(compute="_compute_best_offer", string="Best Offer")

property_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", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")

_check_expected_price_pos = models.Constraint(
'CHECK(expected_price > 0)',
'The expected price must be strictly positive.'
)

_check_selling_price_pos = models.Constraint(
'CHECK(selling_price >= 0)',
'The selling price cannot be negative.'
)

@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:
# If the property has offers, the best offer is the maximum of the expected prices of the offers. Otherwise, it is 0.
if record.offer_ids:
record.best_offer = max(record.offer_ids.mapped("price"))
else:
record.best_offer = 0.0

@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 = False

@api.constrains("selling_price", "expected_price")
def _check_selling_price_vs_expected_price(self):
for record in self:
if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0:
raise UserError("The selling price cannot be less than 90% of the expected price.")

def action_set_sold(self):
for record in self:
if record.state == "canceled":
raise UserError("Canceled properties cannot be sold.")
record.state = "sold"
return True

def action_set_cancel(self):
for record in self:
if record.state == "sold":
raise UserError("Sold properties cannot be canceled.")
record.state = "canceled"
return True

@api.ondelete(at_uninstall=False)
def _unlink_if_new_or_canceled(self):
for record in self:
if record.state not in ("new", "canceled"):
raise UserError("Only properties in 'new' or 'canceled' state can be deleted.")

def check_has_higher_offer(self, price):
for offer in self.offer_ids:
if float_compare(offer.price, price, precision_digits=2) > 0:
return True
return False
Loading