Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
987e6c2
[ADD] estate: initial module setup and ORM configuration
radet-odoo Dec 31, 2025
a94c3cd
[ADD] estate: add access rights for user groups
radet-odoo Dec 31, 2025
8a7b04c
[ADD] estate: add UI actions, menus and defaults
radet-odoo Jan 1, 2026
3981aa4
[FIX] runbot: fix checkpoints error
radet-odoo Jan 1, 2026
0bee4ea
[IMP] estate: add custom list, form, and search views
radet-odoo Jan 2, 2026
464369e
[IMP] Add many2one relation between two tables
radet-odoo Jan 5, 2026
359d3d5
[IMP] estate: add many2many and one2many relations to properties
radet-odoo Jan 6, 2026
9d74e9c
[IMP] estate: computed fields and onchanges
radet-odoo Jan 7, 2026
163c6d9
[IMP] estate: add action buttons for property and offer states
radet-odoo Jan 7, 2026
dff1b27
[IMP] estate: enforce business rules using constraints
radet-odoo Jan 8, 2026
6bef477
[IMP] estate: improve UI with inline views and widgets
radet-odoo Jan 9, 2026
5bb4895
[IMP] estate: enhance UI with views and visual cues
radet-odoo Jan 12, 2026
a8b4338
[IMP] estate: add inheritance and core business rules
radet-odoo Jan 15, 2026
dadd893
[ADD] estate_account: integrate invoicing on property sale
radet-odoo Jan 19, 2026
1f06294
[IMP] estate: add kanban view for properties
radet-odoo Jan 20, 2026
53bd7f4
[ADD] awesome_owl: add Owl component examples in playground
radet-odoo Jan 23, 2026
460982a
[ADD] awesome_owl: counters callbacks and todo list examples
radet-odoo Jan 29, 2026
0865c83
[ADD] awesome_owl: generic Card with slots and toggle
radet-odoo Feb 2, 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
8 changes: 8 additions & 0 deletions awesome_dashboard/static/src/dashboard.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { Layout } from "@web/search/layout";

class AwesomeDashboard extends Component {
static template = "awesome_dashboard.AwesomeDashboard";
static components = { Layout };

setup() {
this.display = {
controlPanel: {},
};
}
}

registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard);
3 changes: 3 additions & 0 deletions awesome_dashboard/static/src/dashboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.o_dashboard {
background-color: gray;
}
7 changes: 6 additions & 1 deletion awesome_dashboard/static/src/dashboard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
<templates xml:space="preserve">

<t t-name="awesome_dashboard.AwesomeDashboard">
hello dashboard
<Layout display="display" className="'o_dashboard h-100'">
<t t-set-slot="layout-buttons">
Hello dashboard
</t>
some content
</Layout>
</t>

</templates>
22 changes: 22 additions & 0 deletions awesome_owl/static/src/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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({ isOpen: true });
}

toggleContent() {
this.state.isOpen = !this.state.isOpen;
}
}
19 changes: 19 additions & 0 deletions awesome_owl/static/src/card/card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?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-out="props.title"/>
<button class="btn" t-on-click="toggleContent">
<t t-if="state.isOpen">Hide</t>
<t t-else="">Show</t>
</button>
</h5>
<p class="card-text" t-if="state.isOpen">
<t t-slot="default"/>
</p>
</div>
</div>
</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";
static props = {
onChange: { type: Function, optional: true }
};

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

increment() {
this.state.value = this.state.value + 1;
this.props.onChange && this.props.onChange()
}
}
10 changes: 10 additions & 0 deletions awesome_owl/static/src/counter/counter.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.Counter">
<div class="m-2 border d-inline-block">
<button class="btn btn-primary bg-success-subtle" t-on-click="increment">
Pressed <t t-esc="state.value"/> Times
</button>
</div>
</t>
</templates>
17 changes: 16 additions & 1 deletion awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { Component } from "@odoo/owl";
import { Component, markup, useState } from "@odoo/owl";
import { Counter } from "./counter/counter"
import { Card } from "./card/card";
import { TodoList } from "./todo_list/todo_list"

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

setup() {
this.str1 = "<div class='text-primary'>some content</div>";
this.str2 = markup("<div class='text-primary'>some content</div>");
this.sum = useState({ value: 0 })
this.incrementSum = this.incrementSum.bind(this);
}

incrementSum() {
this.sum.value += 1
}
}
12 changes: 12 additions & 0 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
<t t-name="awesome_owl.playground">
<div class="p-3">
hello world
<div>
<Counter onChange="incrementSum" />
<Counter onChange="incrementSum" />
<p>Sum is: <t t-esc="sum.value"/></p>
</div>
<div>
<Card title="'card 1'">content of card 1</Card>
<Card title="'card 2'">
Please Click ME!<Counter />
</Card>
</div>
<TodoList/>
</div>
</t>

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

export class TodoItem extends Component {
static template = "awesome_owl.TodoItem";
static props = {
todo: {
type: Object,
shape: { id: Number, description: String, isCompleted: Boolean }
},
toggleState: Function,
removeTodo: Function
};

onChange() {
this.props.toggleState(this.props.todo.id)
}

onDelete() {
this.props.removeTodo(this.props.todo.id);
}
}
13 changes: 13 additions & 0 deletions awesome_owl/static/src/todo_list/todo_item.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.TodoItem">
<div class="form-check">
<input class="form-check-input" type="checkbox" t-att-id="props.todo.id" t-att-checked="props.todo.isCompleted" t-on-change="onChange"/>
<label t-att-for="props.todo.id" t-att-class="props.todo.isCompleted ? 'text-decoration-line-through text-muted' : '' ">
<t t-esc="props.todo.id"/>.
<t t-esc="props.todo.description"/>
</label>
<span class="fa fa-remove ms-3 text-danger" t-on-click="onDelete"/>
</div>
</t>
</templates>
36 changes: 36 additions & 0 deletions awesome_owl/static/src/todo_list/todo_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Component, useState, useRef } 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.id = 1
this.inputRef = useRef("input");
useAutofocus(this.inputRef);
this.removeTodo = this.removeTodo.bind(this)
}

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

toggleState(id) {
const todo = this.todos.find(todo => todo.id === id)
todo.isCompleted = !todo.isCompleted
}

removeTodo(id) {
this.todos.splice(this.todos.findIndex((todo) => todo.id === id), 1)
}
}
12 changes: 12 additions & 0 deletions awesome_owl/static/src/todo_list/todo_list.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.TodoList">
<h2>Todo List</h2>
<div class="d-inline-block border p-2 m-2">
<input 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" toggleState.bind="toggleState" removeTodo="removeTodo"/>
</t>
</div>
</t>
</templates>
9 changes: 9 additions & 0 deletions awesome_owl/static/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { onMounted } from "@odoo/owl";

export function useAutofocus(ref) {
onMounted(() => {
if (ref.el) {
ref.el.focus();
}
});
}
1 change: 1 addition & 0 deletions estate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
i18n/
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
19 changes: 19 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
'name': 'Real Estate',
'category': 'Real Estate',
'version': '1.0',
'author': 'Radhey Detroja(RADET)',
'license': 'LGPL-3',
'summary': 'Manage real estate properties',
'depends': ['base', 'mail'],
'application': True,
'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_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 estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import 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 dateutil.relativedelta import relativedelta

from odoo import api, exceptions, fields, models, _
from odoo.tools.float_utils import float_compare, float_is_zero


class EstateProperty(models.Model):
_name = "estate.property"
_description = "Estate Property Model"
_order = "id desc"
_inherit = ["mail.thread", "mail.activity.mixin"]

name = fields.Char(required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
copy=False,
default=lambda self: fields.Date.today() + relativedelta(months=3),
)
expected_price = fields.Float(required=True)
selling_price = fields.Float(
readonly=True,
copy=False,
)
best_price = fields.Float(compute="_compute_best_price", string="Best Offer")
bedrooms = fields.Integer(default=2)
living_area = fields.Integer()
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
active = fields.Boolean(default=True)
garden_orientation = fields.Selection(
[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]
)
total_area = fields.Integer(compute="_compute_total_area", string="Total Area (sqm)")
state = fields.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")
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user)
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")

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

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

@api.depends("offer_ids.price")
def _compute_best_price(self):
for record in self:
prices = record.offer_ids.mapped("price")
record.best_price = max(prices) if prices else 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 = None

def action_sold(self):
if self.filtered(lambda r: r.state == 'cancelled'):
raise exceptions.UserError(_("Cancelled properties cannot be sold."))
self.write({'state': 'sold'})
return True

def action_cancel(self):
if self.filtered(lambda x: x.state == 'sold'):
raise exceptions.UserError(_("Sold properties cannot be cancelled."))
self.write({'state': 'cancelled'})
return True

@api.constrains("selling_price", "expected_price", "buyer_id")
def _check_selling_price(self):
for record in self:
if float_is_zero(record.selling_price, precision_digits=2):
if record.buyer_id:
raise exceptions.ValidationError("The selling price must be greater than 0.")
continue
if float_compare(record.selling_price, 0.0, precision_digits=2) <= 0:
raise exceptions.ValidationError("The selling price must be positive.")
min_selling_price = record.expected_price * 0.9
if float_compare(record.selling_price, min_selling_price, precision_digits=2) < 0:
raise exceptions.ValidationError(
f"Minimum selling price: {min_selling_price:.2f}"
)

@api.ondelete(at_uninstall=False)
def _unlink_if_not_new_or_cancelled(self):
forbidden = self.filtered(lambda r: r.state not in ("new", "cancelled"))
if forbidden:
raise exceptions.UserError(_("Only properties in 'New' or 'Cancelled' state can be deleted."))
Loading