Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -3,6 +3,9 @@ __pycache__/
*.py[cod]
*$py.class

# Ruff filter
*.toml

# C extensions
*.so

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

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

Choose a reason for hiding this comment

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

Just to be clear, title: String was fine too (it is assumed implicitly that you are defining the type), but feel free to use the style you prefer 👍

slots: { type: Object, shape: { default: Object } },
};

}
15 changes: 15 additions & 0 deletions awesome_owl/static/src/card/card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?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>
<div class="card-text">
<t t-slot="default"/>
</div>
</div>
</div>
</t>

</templates>
19 changes: 19 additions & 0 deletions awesome_owl/static/src/counter/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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++;
if (this.props.onChange) {
this.props.onChange();
}
Comment on lines +15 to +17

Choose a reason for hiding this comment

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

Here you could use optional chaining: this.props.onChange?.().

}
}
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>
15 changes: 14 additions & 1 deletion awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
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/todo_list";

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

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

incrementSum() {
this.sum.value++;
}

static components = { Counter, Card, TodoList };
}
18 changes: 16 additions & 2 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@
<templates xml:space="preserve">

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

<div>
<Counter onChange.bind="incrementSum"/>
<Counter onChange.bind="incrementSum"/>
<p>Total: <t t-esc="sum.value"/></p>
</div>

<Card title="'Card with Counter'">
<Counter onChange.bind="incrementSum"/>
</Card>
<Card title="'Another Card'">
<Counter onChange.bind="incrementSum"/>
</Card>

<h2>Todo List</h2>
<TodoList/>
</t>

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

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

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

onRemove() {
this.props.removeTodo(this.props.todo.id);
}
}
12 changes: 12 additions & 0 deletions awesome_owl/static/src/todo/todo_item.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.todo_item">
<p t-att-class="props.todo.isCompleted ? 'text-decoration-line-through text-muted' : ''">
<input type="checkbox" t-att-checked="props.todo.isCompleted" t-on-change="onToggle"/>
<t t-esc="props.todo.id"/>. <t t-esc="props.todo.description"/>
<span class="fa fa-remove" t-on-click="onRemove"/>
</p>
</t>

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

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

setup() {
this.todos = useState([]);

Choose a reason for hiding this comment

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

Here I would do this.state = useState({todos: []}), and later access the todos array using this.state.todos, so it is clear that todos is in the component state.

this.counter = 0;
}

focusInput() {
this.myRef.el.focus();
}

addTodo(ev) {
if (ev.keyCode === 13 && ev.target.value != "") {

Choose a reason for hiding this comment

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

You could also use ev.key === "Enter" (see here). keyCode is deprecated

this.todos.push({
id: this.counter++,
description: ev.target.value,
isCompleted: false,
});
ev.target.value = "";
}
}

toggleTodoState = (todoId) => {
const todo = this.todos.find((t) => t.id === todoId);
if (todo) {
if (todo.isCompleted) {
todo.isCompleted = false;
} else {
todo.isCompleted = true;
}
}
}

removeTodo = (todoId) => {
const index = this.todos.findIndex((t) => t.id === todoId);
if (index > 0) {
this.todos.splice(index, 1);
}
}
Comment on lines +38 to +43

Choose a reason for hiding this comment

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

Will this work if we try to remove the first element in the todo list (index 0)?


static components = { TodoItem };
}
13 changes: 13 additions & 0 deletions awesome_owl/static/src/todo/todo_list.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.todo_list">
<div class="card d-inline-block m-2" style="width: 18rem;">
<input placeholder="Enter a new task" t-on-keyup="addTodo" />
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<TodoItem todo="todo" toggleState="toggleTodoState" removeTodo="removeTodo"/>
</t>
</div>
</t>

</templates>
3 changes: 3 additions & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
20 changes: 20 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
'name': "Real Estate",
'depends': ['base'],
'author': "Odoo",
'category': 'Category',
'license': 'LGPL-3',
'application': True,
'description': """
A app for real estate
""",
'data': [
'security/ir.model.access.csv',
'views/estate_views.xml',
'views/estate_list_views.xml',
'views/estate_form_views.xml',
'views/estate_search_views.xml',
'views/estate_menus.xml',
'views/estate_kanban_views.xml',
],
}
9 changes: 9 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import (
estate_property,
estate_property_offer,
estate_property_tag,
estate_property_type,
res_users,
)
108 changes: 108 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

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


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

name = fields.Char('Property Name', required=True)
description = fields.Text('Description')
postcode = fields.Char('Postcode')
date_availability = fields.Date('Available From', copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3))
expected_price = fields.Float('Expected Price', required=True)
selling_price = fields.Float('Selling Price', readonly=True, copy=False)
bedrooms = fields.Integer('Bedrooms', default=2)
living_area = fields.Integer('Living Area (sqm)')
facades = fields.Integer('Facades')
garage = fields.Boolean('Garage')
garden = fields.Boolean('Garden')
garden_area = fields.Integer('Garden Area (sqm)')
garden_orientation = fields.Selection(
string='Garden Orientation',
selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')],
help="Type is used to choose the orientation")
property_type_id = fields.Many2one('estate.property.type', string='Property Types')
seller_id = fields.Many2one('res.users', string='Salesman', 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')
active = fields.Boolean(default=True)
state = fields.Selection(
string='Status',
selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('canceled', 'Canceled')],
required=True, copy=False, default='new')
total_area = fields.Integer('Total Area (sqm)', compute="_compute_total_area")
best_price = fields.Float('Best Offer', compute='_compute_best_price')

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

@api.depends('offer_ids')
def _compute_best_price(self):
for line in self:
if line.offer_ids:
line.best_price = max(line.offer_ids.mapped('price'))
else:
line.best_price = 0.0

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

def action_sold(self):
for record in self:
if record.state != 'canceled':
record.state = 'sold'
else:
error_msg = "You cannot sell a canceled property."
raise UserError(error_msg)
return True

def action_cancel(self):
for record in self:
if record.state != 'sold':
record.state = 'canceled'
else:
error_msg = "You cannot cancel a sold property."
raise UserError(error_msg)
return True

_check_positive_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
'The expected price of a property should be strictly positive.',
)

_check_positive_selling_price = models.Constraint(
'CHECK(selling_price >= 0)',
'The selling price of a property should be positive or zero.',
)

@api.constrains('selling_price', 'expected_price')
def _check_price_offer(self):
for record in self:
if float_is_zero(record.selling_price, precision_digits=2):
continue
expected_price = record.expected_price
selling_price = record.selling_price
if float_compare(selling_price, expected_price * 0.9, precision_digits=2) < 0:
error_msg = "The selling price should be at least 90% of the expected price."
raise ValidationError(error_msg)

@api.ondelete(at_uninstall=False)
def _unlink_check_state(self):
for record in self:
if record.state != 'new' and record.state != 'canceled':
error_msg = "Only properties in 'New' or 'Canceled' status can be deleted."
raise UserError(error_msg)
Loading