-
Notifications
You must be signed in to change notification settings - Fork 2.8k
19.0 app traning raibr #1074
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?
19.0 app traning raibr #1074
Changes from all commits
9078a4d
b46dc7d
9d3e35d
afbf975
c79e559
27d7a39
fbb6c5e
06b0b67
e76dbf5
23dc969
7d28247
fcbae9f
90bef7f
806e103
f073fa7
8e85311
9259a91
35673d8
9c4e958
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,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,34 @@ | ||
| # Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
| { | ||
| "name": "Real Estate Advertisement", | ||
| "author": "Odoo", | ||
| "category": "Sales/Real Estate", | ||
| "sequence": 15, | ||
| "summary": "Manage property listings and real estate advertisements", | ||
| "description": """ | ||
| Real Estate Advertisement Management | ||
| ==================================== | ||
| This module allows you to manage real estate properties, including: | ||
| * Property listings with detailed information | ||
| * Property types and tags | ||
| * Property offers and negotiations | ||
| * Sales tracking | ||
| """, | ||
| "depends": ["base"], | ||
| "data": [ | ||
| "security/ir.model.access.csv", | ||
| "views/estate_property_views.xml", | ||
| "views/estate_property_offer_views.xml", | ||
| "views/estate_property_type_views.xml", | ||
| "views/estate_property_tag_views.xml", | ||
| "views/res_users_views.xml", | ||
| "views/estate_menus.xml", | ||
| ], | ||
| "assets": { | ||
| "web.assets_backend": [ | ||
| "estate/static/src/css/estate.css", | ||
| ], | ||
| }, | ||
| "application": True, | ||
| "license": "LGPL-3", | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
|
||
| from . import estate_property | ||
| from . import estate_property_type | ||
| from . import estate_property_tag | ||
| from . import estate_property_offer | ||
| from . import res_users |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| # 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 import float_compare, float_is_zero | ||
|
|
||
|
|
||
| class EstateProperty(models.Model): | ||
| _name = 'estate.property' | ||
| _description = "Real Estate Property" | ||
| _order = 'id desc' | ||
|
|
||
| _check_expected_price = models.Constraint('Check(expected_price > 0)', "The expected price must be strictly positive.") | ||
| _check_selling_price = models.Constraint('Check(selling_price >= 0)', "The selling price must be positive.") | ||
|
|
||
| name = fields.Char(required=True, string="Name") | ||
| description = fields.Text(string="Description") | ||
| postcode = fields.Char(string="Postcode") | ||
| date_availability = fields.Date(string="Available From") | ||
| expected_price = fields.Float(required=True, string="Expected Price") | ||
| selling_price = fields.Float(string="Selling Price") | ||
| bedrooms = fields.Integer(string="Bedrooms") | ||
| 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( | ||
| selection=[ | ||
| ('north', "North"), | ||
| ('south', "South"), | ||
| ('east', "East"), | ||
| ('west', "West"), | ||
| ], | ||
| string="Garden Orientation", | ||
| ) | ||
| state = fields.Selection( | ||
| selection=[ | ||
| ('new', "New"), | ||
| ('offer_received', "Offer Received"), | ||
| ('offer_accepted', "Offer Accepted"), | ||
| ('sold', "Sold"), | ||
| ('cancelled', "Cancelled"), | ||
| ], | ||
| default='new', | ||
| required=True, | ||
| string="Status", | ||
| ) | ||
| buyer_id = fields.Many2one(comodel_name='res.partner', string="Buyer") | ||
| salesperson_id = fields.Many2one(comodel_name='res.users', string="Salesperson", default=lambda self: self.env.user) | ||
| property_type_id = fields.Many2one(comodel_name='estate.property.type', string="Property Type") | ||
| tag_ids = fields.Many2many(comodel_name='estate.property.tag', string="Tags") | ||
| offer_ids = fields.One2many( | ||
| comodel_name='estate.property.offer', | ||
| inverse_name='property_id', | ||
| string="Offers", | ||
| ) | ||
| total_area = fields.Integer(compute='_compute_total_area', string="Total Area (sqm)") | ||
| best_price = fields.Float(compute='_compute_best_price', string="Best Offer Price") | ||
|
|
||
| @api.depends('living_area', 'garden_area') | ||
| def _compute_total_area(self): | ||
| for property in self: | ||
| property.total_area = property.living_area + property.garden_area | ||
|
|
||
| @api.depends('offer_ids.price') | ||
| def _compute_best_price(self): | ||
| for property in self: | ||
| if property.offer_ids: | ||
| property.best_price = max(property.offer_ids.mapped('price')) | ||
| else: | ||
| property.best_price = 0.0 | ||
|
|
||
| @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 | ||
|
|
||
| @api.constrains('selling_price', 'expected_price') | ||
| def _check_selling_price(self): | ||
| for property in self: | ||
| if not float_is_zero(property.selling_price, precision_digits=2): | ||
| if float_compare(property.selling_price, property.expected_price * 0.9, precision_digits=2) < 0: | ||
| raise ValidationError(self.env._("The selling price cannot be lower than 90% of the expected price.")) | ||
|
|
||
| def action_sold(self): | ||
| for property in self: | ||
| if property.state == 'cancelled': | ||
| raise UserError(self.env._("Cancelled property cannot be sold.")) | ||
| property.state = 'sold' | ||
| return True | ||
|
|
||
| def action_cancel(self): | ||
| for property in self: | ||
| if property.state == 'sold': | ||
| raise UserError(self.env._("Sold property cannot be cancelled.")) | ||
| property.state = 'cancelled' | ||
| return True | ||
|
|
||
| @api.ondelete(at_uninstall=True) | ||
| def delete(self): | ||
| for property in self: | ||
| if property.state not in ('new', 'cancelled'): | ||
| raise UserError(self.env._("Only new and cancelled properties can be deleted.")) | ||
| return super().unlink() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
|
||
| from datetime import timedelta | ||
| from odoo import api, fields, models | ||
| from odoo.exceptions import UserError | ||
|
|
||
|
|
||
| class EstatePropertyOffer(models.Model): | ||
| _name = 'estate.property.offer' | ||
| _description = "Real Estate Property Offer" | ||
| _order = 'price desc' | ||
|
|
||
| _check_price = models.Constraint('Check(price > 0)', "The offer price must be strictly positive.") | ||
ramezlahzy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| price = fields.Float(required=True, string="Price") | ||
| status = fields.Selection(selection=[('accepted', "Accepted"), ('refused', "Refused")], copy=False, string="Status") | ||
| partner_id = fields.Many2one(comodel_name='res.partner', string="Partner", required=True) | ||
| property_id = fields.Many2one(comodel_name='estate.property', string="Property", required=True, ondelete='cascade') | ||
| validity = fields.Integer(default=7, string="Validity (days)") | ||
| date_deadline = fields.Date( | ||
| compute='_compute_date_deadline', | ||
| inverse='_inverse_date_deadline', | ||
| store=True, | ||
| string="Deadline", | ||
| ) | ||
|
|
||
| @api.model_create_multi | ||
| def create(self, vals_list): | ||
| for vals in vals_list: | ||
| property_id = vals.get('property_id') | ||
| price = vals.get('price') | ||
|
|
||
| if property_id and price: | ||
| property = self.env['estate.property'].browse(property_id) | ||
| if property.offer_ids: | ||
| max_offer = max(property.offer_ids.mapped('price')) | ||
| if price < max_offer: | ||
| raise UserError(self.env._("The offer amount must be higher than %.2f") % max_offer) | ||
|
|
||
| offers = super().create(vals_list) | ||
| for offer in offers: | ||
| if offer.property_id.state == 'new': | ||
| offer.property_id.state = 'offer_received' | ||
| return offers | ||
|
|
||
| @api.depends('validity') | ||
| def _compute_date_deadline(self): | ||
| for offer in self: | ||
| offer.date_deadline = fields.Date.today() + timedelta(days=offer.validity) | ||
|
|
||
| def _inverse_date_deadline(self): | ||
| for offer in self: | ||
| if offer.date_deadline: | ||
| offer.validity = (offer.date_deadline - fields.Date.today()).days | ||
|
|
||
| def action_accept(self): | ||
| for offer in self: | ||
| if offer.property_id.offer_ids.filtered(lambda o: o.status == 'accepted' and o.id != offer.id): | ||
| raise UserError(self.env._("Only one offer can be accepted per property.")) | ||
|
Comment on lines
+58
to
+59
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. Can you not check the state of the property directly, instead of doing a filtering ? |
||
| offer.status = 'accepted' | ||
| offer.property_id.buyer_id = offer.partner_id | ||
| offer.property_id.selling_price = offer.price | ||
| offer.property_id.state = 'offer_accepted' | ||
| return True | ||
|
|
||
| def action_refuse(self): | ||
| for offer in self: | ||
| offer.status = 'refused' | ||
| return True | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
|
||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class EstatePropertyTag(models.Model): | ||
| _name = 'estate.property.tag' | ||
| _description = "Real Estate Property Tag" | ||
| _order = 'name' | ||
|
|
||
| _check_unique_name = models.Constraint('UNIQUE(name)', "The tag name must be unique.") | ||
|
|
||
| name = fields.Char(required=True, string="Name") | ||
| color = fields.Integer(string="Color") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
|
||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class EstatePropertyType(models.Model): | ||
| _name = 'estate.property.type' | ||
| _description = "Real Estate Property Type" | ||
| _order = 'name' | ||
|
|
||
| _check_unique_name = models.Constraint('UNIQUE(name)', "The type name must be unique.") | ||
|
|
||
| name = fields.Char(required=True, string="Name") | ||
| sequence = fields.Integer(default=1, string="Sequence") | ||
| property_ids = fields.One2many(comodel_name='estate.property', inverse_name='property_type_id', string="Properties") | ||
| offer_count = fields.Integer(compute='_compute_offer_count', string="Offer Count") | ||
|
|
||
| def _compute_offer_count(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. Is it normal that you don't have an
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. 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. Yeah but I think you still need to add a dependency as if property_ids change it is not recomputed. |
||
| for property_type in self: | ||
| property_type.offer_count = sum(property_type.property_ids.mapped(lambda p: len(p.offer_ids))) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
|
||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class ResUsers(models.Model): | ||
| _inherit = 'res.users' | ||
|
|
||
| property_ids = fields.One2many( | ||
| comodel_name='estate.property', | ||
| inverse_name='salesperson_id', | ||
| string="Estate Properties", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink | ||
| access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 | ||
| access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 | ||
| access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 | ||
| access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 | ||
| access_res_users_estate_salesperson,access_res_users_estate_salesperson,model_res_users,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /* Estate Module Kanban View Styles */ | ||
|
|
||
| .o_kanban_record_title { | ||
| font-size: 16px; | ||
| font-weight: bold; | ||
| margin-bottom: 8px; | ||
| color: #212529; | ||
| } | ||
|
|
||
| .o_kanban_record_subtitle { | ||
| font-size: 13px; | ||
| color: #6c757d; | ||
| margin-bottom: 4px; | ||
| } | ||
|
|
||
| .o_kanban_tags_section { | ||
| margin-top: 8px; | ||
| } | ||
|
Comment on lines
+1
to
+18
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. Try to at least put your module name to avoid collision with the rest of the css from odoo |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <odoo> | ||
| <!-- Main menu --> | ||
| <menuitem id="estate_menu_root" name="Real Estate"/> | ||
|
|
||
| <!-- Properties menu --> | ||
| <menuitem id="estate_property_menu" | ||
| name="Advertisements" | ||
| parent="estate_menu_root"/> | ||
|
|
||
| <menuitem id="estate_property_menu_action" | ||
| name="Properties" | ||
| parent="estate_property_menu" | ||
| action="estate_property_action"/> | ||
|
|
||
| <!-- Settings menu --> | ||
| <menuitem id="estate_settings_menu" | ||
| name="Settings" | ||
| parent="estate_menu_root"/> | ||
|
|
||
| <menuitem id="estate_property_type_menu_action" | ||
| name="Property Types" | ||
| parent="estate_settings_menu" | ||
| action="estate_property_type_action"/> | ||
|
|
||
| <menuitem id="estate_property_tag_menu_action" | ||
| name="Property Tags" | ||
| parent="estate_settings_menu" | ||
| action="estate_property_tag_action"/> | ||
|
|
||
| <menuitem id="estate_property_offer_menu_action" | ||
| name="Property Offers" | ||
| parent="estate_settings_menu" | ||
| action="estate_property_offer_action"/> | ||
|
|
||
| <menuitem id="estate_salesperson_menu_action" | ||
| name="Salespersons" | ||
| parent="estate_settings_menu" | ||
| action="estate_salesperson_action"/> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <odoo> | ||
| <!-- List View --> | ||
| <record id="estate_property_offer_view_list" model="ir.ui.view"> | ||
| <field name="name">estate.property.offer.view.list</field> | ||
| <field name="model">estate.property.offer</field> | ||
| <field name="arch" type="xml"> | ||
| <list editable="bottom" decoration-danger="status == 'refused'" | ||
| decoration-success="status == 'accepted'"> | ||
| <field name="price" /> | ||
| <field name="partner_id" /> | ||
| <field name="property_id" /> | ||
| <field name="property_id" /> | ||
| <field name="validity" /> | ||
| <field name="date_deadline" /> | ||
| <field name="status" /> | ||
| <button name="action_accept" type="object" icon="fa-check" title="Accept" /> | ||
| <button name="action_refuse" type="object" icon="fa-times" title="Refuse" /> | ||
|
|
||
ramezlahzy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| </list> | ||
| </field> | ||
| </record> | ||
|
|
||
| <!-- Form View --> | ||
| <record id="estate_property_offer_view_form" model="ir.ui.view"> | ||
| <field name="name">estate.property.offer.view.form</field> | ||
| <field name="model">estate.property.offer</field> | ||
| <field name="arch" type="xml"> | ||
| <form> | ||
| <sheet> | ||
| <group> | ||
| <field name="price" /> | ||
| <field name="partner_id" /> | ||
| <field name="property_id" /> | ||
| <field name="validity" /> | ||
| <field name="date_deadline" /> | ||
| <field name="status" /> | ||
| </group> | ||
| </sheet> | ||
| </form> | ||
| </field> | ||
| </record> | ||
|
|
||
| <!-- Action --> | ||
| <record id="estate_property_offer_action" model="ir.actions.act_window"> | ||
| <field name="name">Property Offers</field> | ||
| <field name="res_model">estate.property.offer</field> | ||
| <field name="view_mode">list,form</field> | ||
| </record> | ||
| </odoo> | ||
ramezlahzy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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.
Why doing two conditions ?