-
Notifications
You must be signed in to change notification settings - Fork 3.1k
vibad - technical trainning #1239
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?
Changes from all commits
11a9e65
e8c4542
ebe26b3
760eba1
86dbba3
842b9fb
d3a2e67
0eeb0b0
45320c8
b2e44d5
7ab1515
c4f1032
0895297
e5985e4
bce8978
bf2cafe
867cbfe
9061548
8d99e23
5dd56af
a43be70
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 |
|---|---|---|
| @@ -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 }, | ||
| }; | ||
| } | ||
|
|
| 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> |
| 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(); | ||
| } | ||
| } |
| 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); | ||
| } | ||
| } | ||
| } |
| 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> |
| 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++; | ||
| } | ||
|
|
||
| } |
| 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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from . import models |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| 'name': 'estate', | ||
| 'depends': [ | ||
| 'base' | ||
| ], | ||
| 'installable': True, | ||
| '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', | ||
| ], | ||
|
|
||
|
|
||
| } | ||
| 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 |
| 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): | ||
|
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) | ||
|
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, | ||
| ) | ||
|
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): | ||
|
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. Tricky (?) question time! At first glance, this method could also be an Answer
It doesn't make much sense to set arbitrary default values when programmatically updating records, so |
||
| 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.") | ||
| 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): | ||
|
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. 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) | ||
| 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() | ||||||
|
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. American english is ugly, too bad it's the default most of the time 😢
Suggested change
(don't apply this, it's just me ranting)
Author
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. 🇺🇸🇺🇸🇺🇸 |
||||||
|
|
||||||
| _check_name = models.Constraint( | ||||||
| "UNIQUE(name)", | ||||||
| message="The name of the tag must be unique", | ||||||
| ) | ||||||
Uh oh!
There was an error while loading. Please reload this page.