Skip to content

[ADD] estate: first tutorial for a real estate #868

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

Open
wants to merge 12 commits into
base: 18.0
Choose a base branch
from
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
Binary file added awesome_owl/static/src/Owl.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions awesome_owl/static/src/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Component, useState } from "@odoo/owl";

export class Card extends Component {
static template = "awesome_owl.Card";
static props = {
heading: { type: String },
content: { type: String },
slots: {
type: Object,
shape: {
default: true
},
},
};

setup() {
this.state = useState({ isOpen: true });
}

toggleContent(){
this.state.isOpen = !this.state.isOpen;
}
}
17 changes: 17 additions & 0 deletions awesome_owl/static/src/card/card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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.heading"/>
<button class="btn" t-on-click="toggleContent">Toggle</button>
</h5>
<p class="card-text" t-if="state.isOpen">
<t t-out="props.content"/>
<t t-slot="default"/>
</p>
</div>
</div>
</t>
</templates>
22 changes: 22 additions & 0 deletions awesome_owl/static/src/counter/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/** @odoo-module **/

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();
}
}
}
11 changes: 11 additions & 0 deletions awesome_owl/static/src/counter/counter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_owl.Counter">
<div class="m-2 p-2 border d-inline-block">
<span class="me-2">Counter: <t t-esc="state.value"/></span>
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</div>
</t>
</templates>
21 changes: 18 additions & 3 deletions awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
/** @odoo-module **/
import { Card } from "./card/card";
import { Component, markup, useState } from "@odoo/owl";
import { Counter } from "./counter/counter";

import { Component } from "@odoo/owl";

export class Playground extends Component {
static template = "awesome_owl.playground";
static template = "awesome_owl.Playground";
static components = { Card, Counter };
static props = {
heading: { type: String, optional: true }
};

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

incrementSum() {
this.sum.value++;
}
}
32 changes: 28 additions & 4 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_owl.Playground">
<div class="p-5">
<div class="text-center">
<h2 class="flex">
Hello dear,
<img src="./awesome_owl/static/src/Owl.png" alt="Owl Picture" style="width: 50px; height: 80px;"/>
</h2>
<p class="italic">
"Wisdom doesn’t always hoot the loudest, but it listens the best."
</p>
</div>
<div class="flex-column">
<div class="mb-2">
<Counter onChange.bind="incrementSum"/>
<Counter onChange.bind="incrementSum" />
</div>

<t t-name="awesome_owl.playground">
<div class="p-3">
hello world
<div class="mb-40">
<p>Total Sum: <t t-esc="sum.value" /></p>
</div>
</div>
<div>
<Card heading="'Wisdom 1'" content="'Knowledge flies on quiet wings — not loud ones.'">
<t t-out="this.str2" />
</Card>
<Card heading="'Wisdom 2'" content="'True wisdom doesn’t flutter — it soars slowly and surely.'">
<Counter />
</Card>
</div>
</div>
</t>

</templates>
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': 'Estate',
'depends': [
'base_setup',
'mail'
],
'data': [
'security/ir.model.access.csv',
'views/ninja_turtles_estate_views.xml',
'views/ninja_turtles_estate_property_offer_views.xml',
'views/ninja_turtles_estate_property_type_views.xml',
'views/ninja_turtles_estate_property_tag_views.xml',
'views/ninja_turtles_estate_property_kanban.xml',
'views/ninja_turtles_estate_menus.xml',
'views/res_users_views.xml',
],
'license': 'LGPL-3',
'application': True
}
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 ninja_turtles_estate
from . import ninja_turtles_estate_property_offer
from . import ninja_turtles_estate_property_tag
from . import ninja_turtles_estate_property_type
from . import res_users
151 changes: 151 additions & 0 deletions estate/models/ninja_turtles_estate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from datetime import date, timedelta

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


class NinjaTurtlesEstate(models.Model):
_name = "ninja.turtles.estate"
_description = "For the fastest progress ever!"
_order = "id desc"
_inherit = ['mail.thread']

name = fields.Char(string="Name", required=True)
description = fields.Text(string="Description")
postcode = fields.Char(string="Postcode")
date_availability = fields.Date(
string="Available From",
copy=False,
default=lambda self: date.today() + timedelta(days=90),
)
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=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West'),
],
help="Garden Orientation is for choosing your specified area for your garden.",
)
status = fields.Selection(
string="Status",
selection=[
('new', 'New'),
('offer received', 'Offer Received'),
('offer accepted', 'Offer Accepted'),
('sold', "Sold"),
('cancelled', "Cancelled"),
],
required=True,
default="new",
copy=False,
tracking=True,
)
active = fields.Boolean(
string="Active",
default=True,
)
total_area = fields.Integer(
string="Total Area",
compute="_compute_total_area",
)
best_price = fields.Float(
string="Best Offer",
compute="_compute_best_price",
)
property_type_id = fields.Many2one(
"ninja.turtles.estate.property.type",
string="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(
"ninja.turtles.estate.property.tag",
string="Tags",
)
offer_ids = fields.One2many(
"ninja.turtles.estate.property.offer",
"property_id",
string="Offers",
tracking=True,
)

_sql_constraints = [
('check_expected_price_positive', 'CHECK(expected_price > 0)', 'Expected price must be strictly positive.'),
('check_selling_price_positive', 'CHECK(selling_price >= 0)', 'Selling price must be positive.'),
]

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

@api.depends("offer_ids.price")
def _compute_best_price(self):
self.best_price = 0
for record in self.filtered(lambda r: r.offer_ids and any(r.offer_ids.mapped('price'))):
record.best_price = max(record.offer_ids.mapped('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

def action_mark_sold(self):
for record in self:
if record.status in ('cancelled', 'new'):
raise UserError("Cancelled or new properties cannot be sold.")
elif record.status == 'offer received':
raise UserError("You cannot sell a property with a non-accepted offer.")
else:
record.status = 'sold'

def action_cancel(self):
for record in self:
if record.status == 'sold':
raise UserError("Sold properties cannot be cancelled.")
record.status = 'cancelled'

@api.constrains('selling_price', 'expected_price')
def _check_selling_price_margin(self):
for record in self.filtered(lambda p: not float_is_zero(p.selling_price, precision_digits=2)):
min_price = 0.9 * record.expected_price
if float_compare(record.selling_price, min_price, precision_digits=2) < 0:
raise ValidationError(
"Selling price cannot be lower than 90% of the expected price.\n"
f"Expected: {record.expected_price}, Minimum Allowed: {min_price}, Given: {record.selling_price}"
)

@api.ondelete(at_uninstall=False)
def _check_property_deletion(self):
if any(self.filtered(lambda p: p.status not in ['new', 'cancelled'])):
raise UserError("You can only delete properties in 'New' or 'Cancelled' status.")
Loading