-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Tutorials #1174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 19.0
Are you sure you want to change the base?
Tutorials #1174
Changes from all commits
34e5c4f
f6b71c7
ae8e6d9
04a9478
c2bb0ee
3d095b8
bca84cb
799b2e2
d31c05f
f6ffdb2
0338f3c
9dda914
f7ee509
626e597
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,9 @@ __pycache__/ | |
| *.py[cod] | ||
| *$py.class | ||
|
|
||
| # Ruff filter | ||
| *.toml | ||
|
|
||
| # C extensions | ||
| *.so | ||
|
|
||
|
|
||
| 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 }, | ||
| slots: { type: Object, shape: { default: Object } }, | ||
| }; | ||
|
|
||
| } | ||
| 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> |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here you could use optional chaining: |
||
| } | ||
| } | ||
| 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> |
| 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 }; | ||
| } |
| 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); | ||
| } | ||
| } |
| 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> |
| 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([]); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here I would do |
||
| this.counter = 0; | ||
| } | ||
|
|
||
| focusInput() { | ||
| this.myRef.el.focus(); | ||
| } | ||
|
|
||
| addTodo(ev) { | ||
| if (ev.keyCode === 13 && ev.target.value != "") { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could also use |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; | ||
| } | ||
| 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> |
| 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 |
| 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', | ||
| ], | ||
| } |
| 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, | ||
| ) |
| 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") | ||
AlessandroLupo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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) | ||
There was a problem hiding this comment.
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: Stringwas fine too (it is assumed implicitly that you are defining the type), but feel free to use the style you prefer 👍