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
1 change: 1 addition & 0 deletions awesome_owl/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap'),
('include', 'web._assets_bootstrap_backend'),
('include', 'web._assets_core'),
'web/static/src/libs/fontawesome/css/font-awesome.css',
'awesome_owl/static/src/**/*',
Expand Down
15 changes: 15 additions & 0 deletions awesome_owl/static/src/components/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component, useState } from "@odoo/owl";

export class Card extends Component {
static template = "awesome_owl.Card";
static props = {
title: String,
slots: Object,
};
setup() {
this.state = useState({ isOpen: true });
}
toggleOpen() {
this.state.isOpen = !this.state.isOpen;
}
}
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">
<h5 class="card-title">
<t t-out="props.title"/>
<button t-on-click="toggleOpen" class="btn">Toggle</button>
</h5>
<t t-slot="default" t-if="this.state.isOpen"/>
</div>
</div>
</t>
</templates>
19 changes: 19 additions & 0 deletions awesome_owl/static/src/components/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();
}
}
}
12 changes: 12 additions & 0 deletions awesome_owl/static/src/components/counter/counter.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.Counter">
<div class="d-flex gap-2 border border-dark align-items-center p-2">
<span>Counter: <t t-out="state.value"/>
</span>
<button t-on-click="increment" class="btn btn-primary">Increment</button>
</div>
</t>

</templates>
17 changes: 17 additions & 0 deletions awesome_owl/static/src/components/todoo/todo_item/todo_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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,
};
}
13 changes: 13 additions & 0 deletions awesome_owl/static/src/components/todoo/todo_item/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="d-flex align-items-center gap-2">
<input type="checkbox" t-on-change="props.toggleState"/>
<t t-out="props.todo.id"/>
. <span t-att-class="{'text-decoration-line-through text-muted':props.todo.isCompleted}">
<t t-out="props.todo.description" />
</span>
<span role="button" class="fa fa-remove ms-3 text-danger" t-on-click="props.removeTodo"/>
</div>
</t>
</templates>
36 changes: 36 additions & 0 deletions awesome_owl/static/src/components/todoo/todo_list/todo_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Component, useState } from "@odoo/owl";
import { TodoItem } from "../todo_item/todo_item";
import { useAutofocus } from "../../../utils";
export class TodoList extends Component {
static template = "awesome_owl.TodoList";
static components = { TodoItem };
setup() {
this.todos = useState([]);
useAutofocus("todo_input");
}
addTodo(event) {
if (event.keyCode === 13 && event.target.value.length) {
this.todos.push({
id: this.todos.length
? this.todos[this.todos.length - 1].id + 1
: 1,
description: event.target.value,
isCompleted: false,
});
event.target.value = "";
}
}
toggleState(id) {
const todo = this.todos.find((todo) => todo.id === id);
if (todo) {
todo.isCompleted = !todo.isCompleted;
}
}

removeTodo(id) {
const index = this.todos.findIndex((todo) => todo.id === id);
if (index !== -1) {
this.todos.splice(index, 1);
}
}
}
12 changes: 12 additions & 0 deletions awesome_owl/static/src/components/todoo/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">
<div class="d-flex flex-column">
<input placeholder="Add a todo" t-on-keyup="addTodo" t-ref="todo_input"/>
<t t-foreach="this.todos" t-as="todo" t-key="todo.id">
<TodoItem todo="todo" toggleState.bind="()=>this.toggleState(todo.id)" removeTodo.bind="()=>this.removeTodo(todo.id)"/>
</t>

</div>
</t>
</templates>
16 changes: 13 additions & 3 deletions awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { Component } from "@odoo/owl";

import { Component, useState } from "@odoo/owl";
import { Counter } from "./components/counter/counter";
import { Card } from "./components/card/card";
import { TodoList } from "./components/todoo/todo_list/todo_list";
export class Playground extends Component {
static template = "awesome_owl.playground";
static template = "awesome_owl.Playground";
static components = { Counter, Card, TodoList };

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

<t t-name="awesome_owl.playground">
<t t-name="awesome_owl.Playground">
<div class="d-flex gap-2">
<div class="p-3">
hello world
</div>
<Counter onChange.bind="incrementSum"/>
<Counter onChange.bind="incrementSum"/>

</div>
<div class="p-3">
hello world
Sum: <t t-out="this.sum.value"/>
</div>
<div class="d-flex gap-4">
<Card title="'Card 1'">
Card 1 content
</Card>
<Card title="'Card 2'">
<Counter />
</Card>
</div>
<h1>Todo List</h1>
<TodoList/>
</t>

</templates>
7 changes: 7 additions & 0 deletions awesome_owl/static/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useRef, onMounted } from "@odoo/owl";
export const useAutofocus = (ref) => {
const inputRef = useRef(ref);
onMounted(() => {
inputRef.el.focus();
});
};
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',
'version': '0.0',
'summary': 'Real Estate Management',
'depends': [
'base',
],
'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_views.xml'
],
'application': True,
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}
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
117 changes: 117 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
from odoo.tools.date_utils import relativedelta
from odoo.tools.float_utils import float_compare, float_is_zero


GARDEN_ORIENTATION = [("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")]
PROPERTY_STATE = [("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")]


class EstateProperty(models.Model):

# -------------------------------------------------------------------------
# Private attributes
# -------------------------------------------------------------------------
_name = "estate.property"
_description = "Real Estate Property"
_order = "id desc"

# -------------------------------------------------------------------------
# Field declarations
# -------------------------------------------------------------------------
name = fields.Char(string="Title", required=True)
description = fields.Text(string="Description")
postcode = fields.Char(string="Postcode")
date_availability = fields.Date(string="Date Availability", copy=False, default=lambda self: fields.Date.context_today(self) + relativedelta(months=3))
expected_price = fields.Float(string="Expected Price", required=True)
selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
bedrooms = fields.Integer(string="Bedrooms", default=2)
living_area = fields.Integer(string="Living Area (sqm)")
facades = fields.Integer(string="Facades")
garage = fields.Boolean(string="Garage")
garden = fields.Boolean(string="Garden")
garden_area = fields.Integer(string="Garden Area (sqm)")
garden_orientation = fields.Selection(string="Garden Orientation", selection=GARDEN_ORIENTATION)
active = fields.Boolean(string="Active", default=True)
state = fields.Selection(string="Status", selection=PROPERTY_STATE, required=True, copy=False, default="new")

property_type_id = fields.Many2one("estate.property.type", string="Property Type")
buyer_id = fields.Many2one("res.partner", string="Buyer")
salesman_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user)
property_tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")

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

# -------------------------------------------------------------------------
# SQL constraints
# -------------------------------------------------------------------------
_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
"The expected price must be strictly positive."
)
_check_selling_price = models.Constraint(
'CHECK(selling_price >= 0)',
"The selling price must be positive."
)

# -------------------------------------------------------------------------
# Compute methods
# -------------------------------------------------------------------------
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for property in self:
property.total_area = property.living_area + property.garden_area

@api.depends("offer_ids.price")
def _compute_best_offer(self):
for property in self:
property.best_offer = max(property.offer_ids.mapped("price"), default=0)

# -------------------------------------------------------------------------
# Constraints and onchange methods
# -------------------------------------------------------------------------
@api.constrains("selling_price")
def _check_selling_price_minimum_ratio(self):
for property in self:
if float_is_zero(property.selling_price, precision_digits=2):
continue
if float_compare(property.selling_price, property.expected_price * 0.9, precision_digits=2) < 0:
raise ValidationError("The selling price must be at least 90% of the expected price.")

@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

# -------------------------------------------------------------------------
# ORM methods
# -------------------------------------------------------------------------
@api.ondelete(at_uninstall=False)
def _unlink_new_cancelled_property(self):
for property in self:
if property.state not in ["new", "cancelled"]:
raise UserError("Only new and canceled properties can be deleted")

# -------------------------------------------------------------------------
# Action methods
# -------------------------------------------------------------------------
def action_cancel_property(self):
for property in self:
if property.state == "sold":
raise UserError("Sold properties cannot be cancelled")
property.state = "cancelled"
return True

def action_sold_property(self):
for property in self:
if property.state == "cancelled":
raise UserError("Cancelled properties cannot be sold")
property.state = "sold"
return True
Loading