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
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, required: true },
content: { type: String, required: true },
};
}

11 changes: 11 additions & 0 deletions awesome_owl/static/src/card/card.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.card">
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title"><t t-esc="props.title"/></h5>
<p class="card-text"><t t-out="props.content"/></p>
</div>
</div>
</t>
</templates>
14 changes: 14 additions & 0 deletions awesome_owl/static/src/counter/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Component, useState} from "@odoo/owl";

export class Counter extends Component {
static template = "awesome_owl.counter";

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

increment() {
this.state.value++;
this.props.onChange();
}
}
37 changes: 37 additions & 0 deletions awesome_owl/static/src/counter/counter.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* awesome_owl/static/src/counter/counter.scss */
.o_awesome_owl_counter {
border: 1px solid #ccc;
padding: 10px;
border-radius: 8px;
background-color: #f9f9f9;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);

p {
font-weight: bold;
color: #714B67; /* La couleur mauve d'Odoo */
}

.btn-primary {
margin-top: 8px;
padding: 8px 16px;
border: none;
border-radius: 999px;
background: linear-gradient(135deg, #875A7B 0%, #714B67 100%);
color: #fff;
font-weight: 600;
box-shadow: 0 4px 10px rgba(113, 75, 103, 0.18);
transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;

&:hover {
transform: translateY(-1px);
box-shadow: 0 6px 14px rgba(113, 75, 103, 0.24);
opacity: 0.98;
}

&:active {
transform: translateY(0);
box-shadow: 0 3px 8px rgba(113, 75, 103, 0.18);
}
}
}
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">
<div class="o_awesome_owl_counter d-flex align-items-center gap-2">
<span>Counter: <t t-esc="state.value"/></span>
<button class="btn btn-primary" t-on-click="increment">Increment</button>
</div>
</t>
</templates>
1 change: 1 addition & 0 deletions awesome_owl/static/src/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { whenReady } from "@odoo/owl";
import { mountComponent } from "@web/env";
import { Playground } from "./playground";
import { Counter } from "./counter/counter";

const config = {
dev: true,
Expand Down
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";

export class Playground extends Component {
static template = "awesome_owl.playground";
static components = { Counter, Card };
html = markup("<p>This is some <strong>HTML</strong> content.</p>");

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

onCounterIncremented() {
this.state.incrementSum++;
}

}
20 changes: 15 additions & 5 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_owl.playground">
<div class="p-3">
hello world
<t t-name="awesome_owl.playground">
<div class="container">
<h1>Playground</h1>
<Counter onChange="() => this.onCounterIncremented()"/>
<Counter onChange="() => this.onCounterIncremented()"/>
<p>Total increments: <t t-esc="state.incrementSum"/></p>
<Card
title="'Card Title'"
content="'je suis un card'"
/>
<Card
title="'Card Title 2'"
content="this.html"
/>
</div>
</t>
</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
22 changes: 22 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
'name': 'estate',
'depends': [
'base'
],
'installable': True,
Comment thread
nausicaa73 marked this conversation as resolved.
'application': True,
'author': 'vibad',
'license': 'LGPL-3',
'version': '1.0',
'data': [
'security/ir.model.access.csv',
'view/estate_property_offer_views.xml',
'view/estate_property_views.xml',
'view/estate_property_type_views.xml',
'view/estate_property_tag_views.xml',
'view/estate_inherit_view.xml',
'view/estate_action.xml',
],


}
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 inherited_model
122 changes: 122 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError


class Estate_property(models.Model):
Comment thread
nausicaa73 marked this conversation as resolved.
_name = "estate.property"
_description = "APP super mega trop bien"
_order = "id desc"
_check_expected_price = models.Constraint(
"CHECK(expected_price > 0)",
message="The expected price must be strictly positive",
)

_check_selling_price = models.Constraint(
"CHECK(selling_price >= 0)",
message="The selling price cannot be negative",
)

name = fields.Char(required=True)
Comment thread
nausicaa73 marked this conversation as resolved.
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3))
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer()
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
garden_orientation = fields.Selection(
string="Orientation",
selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")],
help="The garden orientation",
)
active = fields.Boolean(default=True)
state = fields.Selection(
string="Status",
selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")],
required=True,
copy=False,
default="new",
compute="_compute_state",
store=True,
)
Comment thread
nausicaa73 marked this conversation as resolved.
property_type_id = fields.Many2one("estate.property.type", string="Property Type")
salesperson_id = fields.Many2one("res.users", string="Salesperson", 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", string="Offers")
total_area = fields.Integer(compute="_compute_total_area")
best_price = fields.Float(compute="_compute_best_price")

@api.depends("offer_ids", "offer_ids.state")
def _compute_state(self):
for record in self:
if record.state in ["sold", "cancelled"]:
return
if record.offer_ids:
for offer in record.offer_ids:
if offer.state == "accepted":
record.state = "offer_accepted"
return
else:
record.state = "new"

@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):
for record in self:
if record.offer_ids:
record.best_price = max(record.offer_ids.mapped("price"))
else:
record.best_price = 0

@api.onchange("garden")
def _onchange_garden(self):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tricky (?) question time!

At first glance, this method could also be an inverse. Can you tell why it makes more sense for it to be an onchange?

Answer

inverse are triggered whenever the field is written. onchange are called by form views.

It doesn't make much sense to set arbitrary default values when programmatically updating records, so onchange is the correct approach here.

for record in self:
if record.garden:
record.write({
"garden_area": 10,
"garden_orientation": "north",
})
else:
record.write({
"garden_area": 0,
"garden_orientation": False,
})

@api.constrains("selling_price", "expected_price")
def _check_enough_selling_price(self):
for record in self:
if record.selling_price and record.selling_price < record.expected_price * 0.9:
raise ValidationError("The selling price cannot be less than 90% of the expected price.")

@api.ondelete(at_uninstall=False)
def _ondelete_cancel_new(self):
for record in self:
if record.state not in ["new", "cancelled"]:
raise UserError("You can only delete offers that are new or cancelled.")

def action_sold(self):
for record in self:
if record.state != "cancelled" and record.state != "sold":
record.write({
"state": "sold",
})
else:
raise UserError("A property that is cancelled or already sold cannot be sold.")

def action_cancel(self):
for record in self:
if record.state != "sold" and record.state != "cancelled":
record.write({
"state": "cancelled",
})
else:
raise UserError("A property that is sold or already cancelled cannot be cancelled.")
76 changes: 76 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from odoo import api, fields, models
from odoo.exceptions import UserError


class Estate_property_offer(models.Model):
_name = "estate.property.offer"
_description = "Offer for estate properties"
_order = "price desc"

price = fields.Float(required=True)
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
property_id = fields.Many2one("estate.property", string="Property", required=True)
state = fields.Selection([
("new", "New"),
("accepted", "Accepted"),
("refused", "Refused"),
], default="new", string="State", copy=False)
validaty = fields.Integer(string="Offer Validity (days)", default=7)
date_deadline = fields.Date(string="Offer Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline")
property_type_id = fields.Many2one(related="property_id.property_type_id", store=True)

_check_price = models.Constraint(
"CHECK(price > 0)",
message="The price must be strictly positive",
)

@api.depends("validaty", "create_date")
def _compute_date_deadline(self):
for record in self:
date = record.create_date.date() if record.create_date else fields.Date.today()
record.date_deadline = fields.Date.add(date, days=record.validaty)

def _inverse_date_deadline(self):
for record in self:
if record.date_deadline and record.create_date:
create_date = fields.Date.to_date(record.create_date)
record.validaty = (record.date_deadline - create_date).days
else:
record.validaty = 7

def accept_offer(self):
for record in self:
if record.state == "accepted" or record.state == "refused":
raise UserError("This offer has already been accepted or refused.")
if "accepted" in record.mapped("property_id.offer_ids.state"):
raise UserError("Another offer has already been accepted for this property.")
if record.property_id.garden_orientation == "south" and record.price <= record.property_id.expected_price:
raise UserError("The price must be more than the expected price for properties with a south-facing garden.")
record.write({
"state": "accepted",
"property_id": {
"selling_price": record.price,
"buyer_id": record.partner_id.id,
},
})
return True

def refuse_offer(self):
for record in self:
if record.state != "new":
raise UserError("This offer has already been accepted or refused.")
record.write({"state": "refused"})
return True

@api.model_create_multi
def create(self, vals_list):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same remark for order of methods

max_price_list = 0
for vals in vals_list:
if self.env["estate.property"].browse(vals["property_id"]).state == "new":
self.env["estate.property"].browse(vals["property_id"]).state = "offer_received"
max_price = max(self.env["estate.property.offer"].search([("property_id", "=", vals["property_id"])]).mapped("price") or [0])
max_price_list = max(max_price_list, max_price)
if vals["price"] <= max_price:
raise UserError("The price must be higher than the current highest offer.") # Error for one offer blocks all offers in the list
max_price_list = max(max_price_list, vals["price"])
return super().create(vals_list)
15 changes: 15 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import fields, models


class Estate_property_tag(models.Model):
_name = "estate.property.tag"
_description = "tag super mega trop bien"
_order = "name"

name = fields.Char(required=True)
color = fields.Integer()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

American english is ugly, too bad it's the default most of the time 😢

Suggested change
color = fields.Integer()
colour = fields.Integer()

(don't apply this, it's just me ranting)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🇺🇸🇺🇸🇺🇸


_check_name = models.Constraint(
"UNIQUE(name)",
message="The name of the tag must be unique",
)
Loading