-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Sébastien Laurent commit tutorials Chapter 1 -> 4 #1175
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
01338d7
6bcf6d1
c41b9cd
519fdbd
99f8f4c
2a6ac3b
b410971
53f5643
ad54944
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,5 @@ | ||
| repos: | ||
| - repo: https://github.com/astral-sh/ruff-pre-commit | ||
| rev: v0.15.1 | ||
| hooks: | ||
| - id: ruff |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +0,0 @@ | ||
| # -*- coding: utf-8 -*- | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,2 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| from . import controllers |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,2 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| from . import controllers | ||
| from . import controllers |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1 @@ | ||
| # -*- coding: utf-8 -*- | ||
| from . import models |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,2 @@ | ||
| # -*- coding: utf-8 -*- | ||
| # import filename_python_file_within_folder_or_subfolder | ||
| from . import ir_action | ||
| from . import ir_ui_view | ||
| from . import ir_action, ir_ui_view |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,9 @@ | ||
| # -*- coding: utf-8 -*- | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class ActWindowView(models.Model): | ||
| _inherit = 'ir.actions.act_window.view' | ||
|
|
||
| view_mode = fields.Selection(selection_add=[ | ||
| ('gallery', "Awesome Gallery") | ||
| ], ondelete={'gallery': 'cascade'}) | ||
| ('gallery', "Awesome Gallery"), | ||
| ], ondelete={'gallery': 'cascade'}) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,3 @@ | ||
| # -*- coding: utf-8 -*- | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +0,0 @@ | ||
| # -*- coding: utf-8 -*- | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,2 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| from . import controllers | ||
| from . import controllers |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,2 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| from . import controllers | ||
| from . import controllers |
| 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,16 @@ | ||
| { | ||
| 'name': 'Estate', | ||
| 'author': 'Sébastien Laurent', | ||
| "license": 'LGPL-3', | ||
| 'depends': ['base'], | ||
| 'application': True, | ||
| 'data': [ | ||
| 'security/ir.model.access.csv', | ||
| 'views/estate_property_offer_views.xml', | ||
| 'views/estate_property_views.xml', | ||
| 'views/estate_property_type_views.xml', | ||
| 'views/estate_property_tag_views.xml', | ||
| 'views/estate_menus.xml', | ||
| 'views/res_users_views.xml', | ||
| ], | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| 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,119 @@ | ||
| from odoo import api, exceptions, fields, models, tools | ||
|
|
||
|
|
||
| class EstateProperty(models.Model): | ||
| _name = "estate.property" | ||
| _description = "This is my first model" | ||
| _order = "id desc" | ||
|
|
||
| @api.ondelete(at_uninstall=False) | ||
| def cannot_delete_new_cancelled_properties(self): | ||
| for property in self: | ||
| if not property.state in ['new', 'cancelled']: | ||
| error_message = "You can not delete a property in new or cancelled state!" | ||
| raise exceptions.ValidationError(error_message) | ||
|
|
||
| # Atomic fields | ||
| name = fields.Char(required=True) | ||
| description = fields.Text() | ||
|
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. It is not mandatory, but if you want you can add the |
||
| postcode = fields.Char() | ||
| date_availability = fields.Date(copy=False, default=fields.Date.add(fields.Date.today(), month=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='Garden Orientation', | ||
| selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], | ||
| help="Orientation of the estate", | ||
| ) | ||
| 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'), | ||
| ], | ||
| help='Status of the estate', | ||
| default='new', | ||
| ) | ||
|
|
||
| # Relational fields | ||
| property_type_id = fields.Many2one("estate.property.type", string="Property Type") | ||
| salesman_id = fields.Many2one("res.users", string="Salesman") | ||
| buyer_id = fields.Many2one("res.partner", string="Buyer", readonly=True) | ||
| accepted_price = fields.Integer(readonly=True) | ||
| tag_ids = fields.Many2many("estate.property.tag", string="Tags") | ||
| offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") | ||
|
|
||
| # Computed fields | ||
| total_area = fields.Float(compute="_compute_total_area") | ||
| best_price = fields.Float(compute="_compute_best_price") | ||
|
|
||
| # Constraints | ||
| _check_expected_price = models.Constraint( | ||
| 'CHECK(expected_price > 0)', | ||
| 'The expected price should be strictly greater than zero!', | ||
| ) | ||
|
|
||
| _check_seling_price = models.Constraint( | ||
| 'CHECK(selling_price >= 0)', | ||
| 'The seling price should be greater than zero!', | ||
| ) | ||
|
|
||
| @api.constrains('selling_price') | ||
| def _check_selling_price(self): | ||
| for record in self: | ||
| if tools.float_is_zero(record.selling_price, precision_digits=2) is True: | ||
| continue | ||
|
|
||
| if tools.float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=2) < 1: | ||
| error_message = "Selling price can not be lower than 90% of the expected price" | ||
| raise exceptions.ValidationError(error_message) | ||
|
|
||
| # Compute methods | ||
| @api.depends("living_area", "garden_area") | ||
| def _compute_total_area(self): | ||
| for record in self: | ||
| record.total_area = (record.garden_area or 0) + (record.living_area or 0) | ||
|
|
||
| @api.depends("offer_ids") | ||
| def _compute_best_price(self): | ||
| for record in self: | ||
| record.best_price = max(record.offer_ids.mapped("price"), default=0) or 0 | ||
|
|
||
| # Onchange | ||
| @api.onchange("garden") | ||
| def _onchange_garden(self): | ||
| if self.garden is True: | ||
| self.garden_orientation = 'north' | ||
| self.garden_area = 10 | ||
| else: | ||
| self.garden_orientation = False | ||
| self.garden_area = False | ||
|
|
||
| # Button logic | ||
| def sold_button(self): | ||
| for record in self: | ||
| if record.state == 'cancelled': | ||
| error_message = "This estate property is cancelled. You can not sell it!" | ||
| raise exceptions.UserError(error_message) | ||
| record.state = 'sold' | ||
|
|
||
| return True | ||
|
|
||
| def cancelled_button(self): | ||
| for record in self: | ||
| if record.state == 'sold': | ||
| error_message = "This estate property is sold. You can not cancel it!" | ||
| raise exceptions.UserError(error_message) | ||
| record.state = 'cancelled' | ||
|
|
||
| return True | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| from dateutil.relativedelta import relativedelta | ||
|
|
||
| from odoo import api, exceptions, fields, models | ||
| from odoo.tools.float_utils import float_compare | ||
|
|
||
|
|
||
| class EstatePropertyOffer(models.Model): | ||
| _name = "estate.property.offer" | ||
| _description = "This is my fourth model" | ||
|
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. Of course this is just a tutorial and it's ok to use whatever string you want. But for the future, consider that |
||
| _order = "price desc" | ||
|
|
||
| @api.model_create_multi | ||
AlessandroLupo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| def create(self, vals_list): | ||
| for vals in vals_list: | ||
| property = self.env['estate.property'].browse(vals['property_id']) | ||
| if float_compare(vals['price'] or -1, property.best_price, precision_digits=2) <= 0: | ||
| error_message = "You can not add offer with a price lower than the current best price!" | ||
| raise exceptions.UserError(error_message) | ||
|
|
||
| records = super().create(vals_list) | ||
|
|
||
| for record in records: | ||
| record.property_id.state = 'offer_received' | ||
|
|
||
| return records | ||
|
|
||
| # Atomic fields | ||
|
|
||
| price = fields.Float() | ||
| status = fields.Selection( | ||
| string="Status", | ||
| selection=[ | ||
| ("accepted", 'Accepted'), | ||
| ("refused", "Refused"), | ||
| ], | ||
| copy=False, | ||
| ) | ||
| validity = fields.Integer(default=7) | ||
| date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline") | ||
|
|
||
| # Relational fields | ||
|
|
||
| partner_id = fields.Many2one("res.partner", string="Partner", required=True) | ||
| property_id = fields.Many2one("estate.property", string="Property", required=True) | ||
| property_type_id = fields.Many2one( | ||
| "estate.property.type", | ||
| string="Property type", | ||
| related="property_id.property_type_id", | ||
| store=True, | ||
| ) | ||
|
|
||
| # Constraints | ||
| _check_offer_price = models.Constraint( | ||
| 'CHECK(price > 0)', | ||
| 'The price of the offer should be strictly greater than zero!', | ||
| ) | ||
|
|
||
| # Compute methods | ||
| @api.depends("validity") | ||
| def _compute_date_deadline(self): | ||
| for record in self: | ||
| start_date = record.create_date or fields.Date.today() | ||
|
|
||
| record.date_deadline = start_date + relativedelta(days=record.validity) | ||
|
|
||
| def _inverse_date_deadline(self): | ||
| for record in self: | ||
| start_date = record.create_date or fields.Date.today() | ||
|
|
||
| record.validity = (record.date_deadline - start_date.date()).days | ||
|
|
||
| # Button logic | ||
| def accept_button(self): | ||
| for record in self: | ||
| offer_recordset = record.property_id.offer_ids | ||
| for offer in offer_recordset: | ||
| if offer.status == 'accepted' and offer != record: | ||
| error_message = "You can not accept multiple offer!" | ||
| raise exceptions.UserError(error_message) | ||
|
|
||
| record.property_id.buyer_id = record.partner_id | ||
| record.property_id.selling_price = record.price | ||
| record.property_id.state = 'offer_accepted' | ||
| record.status = 'accepted' | ||
|
|
||
| def refuse_button(self): | ||
| for record in self: | ||
| record.status = 'refused' | ||
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.
If you don't specify a
license, you will get a warning in your console when you launch Odoo