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
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 } from '@odoo/owl';

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

setup() {
this.state = useState({ isOpen: true });
}
}
16 changes: 16 additions & 0 deletions awesome_owl/static/src/card/card.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.card'>
<div class='card d-inline-block m-2' style='width: 18rem;'>
<div class='card-body'>
<div class='d-flex justify-content-between'>
<h5 class='card-title' t-out='props.title' />
<button class='btn btn-primary' t-on-click='() => state.isOpen = !state.isOpen'>Toggle</button>
</div>
<t t-if='state.isOpen' t-slot='default' />
</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({ counter: 0 });
}

increment() {
this.state.counter++;
if (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">
<button class="btn btn-primary" t-on-click='increment'>
Counter: <t t-esc='state.counter'/>
</button>
</t>

</templates>
2 changes: 1 addition & 1 deletion awesome_owl/static/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Playground } from "./playground";

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

// Mount the Playground component when the document.body is ready
Expand Down
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/todo_list";

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

html = markup('<a href="https://www.google.com" target="_blank" rel="noopener noreferrer">Some content</a>');

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

incrementSum() {
this.state.sum++;
}
}
21 changes: 17 additions & 4 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
<?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>
<group class='row'>
<group class='d-flex'>
<Card title="'Counters'">
<Counter onChange.bind="incrementSum" />
<Counter onChange.bind="incrementSum" />
</Card>
<Card title="'Total'"><p t-esc="`The sum is: ${state.sum}`" /></Card>
</group>
<group>
<Card title="'Injection'"><t t-out="html"/></Card>
<Card title="'Title'">Content</Card>
</group>
<group>
<TodoList />
</group>
</group>
</t>

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

export class TodoItem extends Component {
static template = 'awesome_owl.todo_item';
static props = {
todo: {
type: {
id: { type: Number },
description: { type: String },
isCompleted: { type: Boolean },
},
},
toggleState: { type: Function },
removeTodo: { type: Function },
};
}
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">
<div t-att-class='{"text-muted text-decoration-line-through": props.todo.isCompleted}'>
<input type='checkbox' t-att='props.todo.isCompleted' t-on-click='props.toggleState' />
<t t-esc='props.todo.id' />. <t t-esc='props.todo.description'/>
<span class='fa fa-remove' t-on-click='() => props.removeTodo(props.todo.id)' />
</div>
</t>

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

export class TodoList extends Component {
static template = 'awesome_owl.todo_list';
static components = { TodoItem };
static props = {};

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

useAutoFocus('input');
}

addTodo(ev) {
if (ev.keyCode === 13 && ev.target.value) {
const lastTodo = this.todos.slice(-1)[0];
this.todos.push({
id: lastTodo === undefined ? 0 : lastTodo.id + 1,
description: ev.target.value,
isCompleted: false,
});

ev.target.value = '';
}
}

removeTodo(elemId) {
const index = this.todos.findIndex((elem) => elem.id === elemId);
if (index >= 0) {
this.todos.splice(index, 1);
}
}
}
15 changes: 15 additions & 0 deletions awesome_owl/static/src/todo/todo_list.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.todo_list">
<div class='card d-inline-block m-2' style='width: 18rem;'>
<div class='card-body'>
<input t-on-keyup="addTodo" placeholder="Enter a new task" t-ref="input" />
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<TodoItem todo="todo" toggleState.bind="() => todo.isCompleted = !todo.isCompleted" removeTodo.bind="removeTodo"/>
</t>
</div>
</div>
</t>

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

function useAutoFocus(refName) {
const inputRef = useRef(refName);
onMounted(() => {
inputRef.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
20 changes: 20 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
'name': "Estate",
'description': """
Track real estate properties
""",
'version': '1.0',
'author': "Odoo S.A.",
'license': "LGPL-3",
'depends': ['base'],
'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/estate_menus.xml',
'views/res_users.xml',
],
}
7 changes: 7 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from . import (
estate_property,
estate_property_type,
estate_property_tag,
estate_property_offer,
res_users,
)
126 changes: 126 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare


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

name = fields.Char('Title', required=True)
description = fields.Text('Description')
postcode = fields.Char('Postcode')
date_availability = fields.Date(
'Available From',
copy=False,
default=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'),
],
default='north',
)
active = fields.Boolean('Active', default=True)
state = fields.Selection(
string='State',
selection=[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('canceled', 'Canceled'),
],
required=True,
copy=False,
default='new',
)
property_type_id = fields.Many2one('estate.property.type', string='Property Types')
property_tag_ids = fields.Many2many('estate.property.tag', string='Property Tags')
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
salesperson_id = fields.Many2one(
'res.users', string='Salesperson', default=lambda self: self.env.user
)
offer_ids = fields.One2many(
'estate.property.offer', 'property_id', string='Property Offers'
)
total_area = fields.Integer('Total Area (sqm)', compute='_compute_total_area')
best_price = fields.Float('Best Offer', compute='_compute_best_price')

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

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

@api.depends('offer_ids.price')
def _compute_best_price(self):
for property in self:
property.best_price = max(property.offer_ids.mapped('price'), default=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(self):
for property in self:
if property.state != 'offer_accepted':
continue

if (
property.state == 'offer_accepted'
and float_compare(property.selling_price, 0, 2) == -1
):
raise ValidationError('The selling price must be positive.')

if (
float_compare(property.selling_price, property.expected_price * 0.9, 2)
== -1
):
raise ValidationError(
'Selling cannot be less than 90% of the expected price.'
)

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

def action_set_property_as_sold(self):
for property in self:
if property.state == 'canceled':
raise UserError('Canceled properties cannot be sold.')
property.state = 'sold'
return True

def action_set_property_as_canceled(self):
for property in self:
if property.state == 'sold':
raise UserError('Sold properties cannot be canceled.')
property.state = 'canceled'
return True
Loading