-
Notifications
You must be signed in to change notification settings - Fork 3.3k
19.0 technical onboarding macau #1427
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
137d16b
23a4825
1c14732
03143cd
4bfd2b2
e57335a
35fd32d
888003d
5c88030
1d4ff80
356052a
f2df725
e085d2f
a786d99
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 @@ | ||
| from . import models |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| 'name': 'Real Estate', | ||
| 'author': 'Odoo S.A.', | ||
| 'license': 'LGPL-3', | ||
| "installable": True, | ||
| "application": True, | ||
| 'depends': [ | ||
| 'base', | ||
| ], | ||
| "data": [ | ||
| "security/ir.model.access.csv", | ||
|
|
||
| "views/estate_property_views.xml", | ||
| "views/estate_property_offer_views.xml", | ||
| "views/estate_property_tag_views.xml", | ||
| "views/estate_property_type_views.xml", | ||
| "views/estate_menus.xml", | ||
| "views/estate_inherited_users_views.xml", | ||
| ], | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from . import estate_inherited_users | ||
| from . import estate_property | ||
| from . import estate_property_offer | ||
| from . import estate_property_tag | ||
| from . import estate_property_type |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class EstateInheritedUsers(models.Model): | ||
| _inherit = 'res.users' | ||
|
|
||
| property_ids = fields.One2many( | ||
| comodel_name='estate.property', | ||
| inverse_name='salesperson', | ||
| string='Estate Properties', | ||
| domain=['|', ('state', '=', 'new'), ('state', '=', 'offer_received')], | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,108 @@ | ||||||
| from odoo import api, fields, models | ||||||
| from odoo.exceptions import UserError, ValidationError | ||||||
| from odoo.tools.float_utils import float_compare, float_is_zero | ||||||
|
|
||||||
|
|
||||||
| class EstateProperty(models.Model): | ||||||
| _name = 'estate.property' | ||||||
| _description = 'Estate Property' | ||||||
| _order = 'id desc' | ||||||
|
|
||||||
| active = fields.Boolean(string='Active', default=True) | ||||||
| bedrooms = fields.Integer(string='Bedrooms', default=2) | ||||||
| best_price = fields.Float(compute='_compute_best_price') | ||||||
| buyer = fields.Many2one(comodel_name='res.partner', readonly=True, string='Buyer', copy=False) | ||||||
| date_availability = fields.Date( | ||||||
| string='Date availability', | ||||||
| copy=False, | ||||||
| default=lambda self: fields.Date.add(fields.Date.today(), months=3), | ||||||
| ) | ||||||
| description = fields.Text(string='Description') | ||||||
| expected_price = fields.Float(string='Expected price', required=True) | ||||||
| facades = fields.Integer(string='Facades') | ||||||
| garage = fields.Boolean(string='Garage') | ||||||
| garden = fields.Boolean(string='Garden') | ||||||
| garden_area = fields.Integer(string='Garden area') | ||||||
| garden_orientation = fields.Selection( | ||||||
| string='Garden orientation', | ||||||
| selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], | ||||||
| ) | ||||||
| living_area = fields.Integer(string='Living area') | ||||||
| name = fields.Char(string='Name', required=True, default="Unknown") | ||||||
| offers = fields.One2many(comodel_name='estate.property.offer', inverse_name='property_id', string='Offers') | ||||||
| postcode = fields.Char(string='Postcode') | ||||||
| property_type = fields.Many2one(comodel_name='estate.property.type', string='Property type') | ||||||
| salesperson = fields.Many2one(comodel_name='res.users', string='Salesperson', default=lambda self: self.env.user) | ||||||
| selling_price = fields.Float(string='Selling price', readonly=True, copy=False) | ||||||
| state = fields.Selection( | ||||||
| string='State', | ||||||
| selection=[('new', 'New'), ('offer_received', 'Offer received'), ('offer_accepted', 'Offer accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], | ||||||
| required=True, | ||||||
| copy=True, | ||||||
| default='new', | ||||||
| ) | ||||||
| tags = fields.Many2many(comodel_name='estate.property.tag', string='Tags') | ||||||
| total_area = fields.Integer(compute='_compute_total_area') | ||||||
|
|
||||||
| _strictly_positive_expected_price = models.Constraint( | ||||||
| 'CHECK(expected_price > 0)', | ||||||
| 'The expected price of a property should be strictly positive.', | ||||||
| ) | ||||||
| _positive_selling_price = models.Constraint( | ||||||
| 'CHECK(selling_price >= 0)', | ||||||
| 'The selling price of a property should 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('offers.price') | ||||||
| def _compute_best_price(self): | ||||||
| for record in self: | ||||||
| record.best_price = max(record.offers.mapped('price')) if record.offers else 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 = None | ||||||
|
|
||||||
| def sold_property(self): | ||||||
| for record in self: | ||||||
| if record.state == 'cancelled': | ||||||
| raise UserError(self.env._('A cancelled property cannot be sold.')) | ||||||
| record.state = 'sold' | ||||||
| return True | ||||||
|
|
||||||
| def cancel_property(self): | ||||||
| for record in self: | ||||||
| if record.state == 'sold': | ||||||
| raise UserError(self.env._('A sold property cannot be cancelled.')) | ||||||
| record.state = 'cancelled' | ||||||
| return True | ||||||
|
|
||||||
| def action_view_offers(self): | ||||||
| self.ensure_one() | ||||||
| return { | ||||||
| 'type': 'ir.actions.act_window', | ||||||
| 'res_model': 'estate.property.offer', | ||||||
| 'name': self.env._('Offers'), | ||||||
| 'views': [[False, 'list'], [False, 'form']], | ||||||
| 'domain': [('property_id', '=', self.id)], | ||||||
| } | ||||||
|
|
||||||
| @api.constrains('expected_price', 'selling_price') | ||||||
| def _check_selling_price_relation_to_expected_price(self): | ||||||
| for record in self: | ||||||
| if float_compare(record.expected_price * 0.9, record.selling_price, 2) == 1 and not float_is_zero(record.selling_price, 2): | ||||||
| raise ValidationError(self.env._(r'The selling price cannot be lower than 90% of the expected price.')) | ||||||
|
|
||||||
| @api.ondelete(at_uninstall=False) | ||||||
| def _unlink_if_status_is_new_or_cancelled(self): | ||||||
| if any(record.state not in ['new', 'cancelled'] for record in 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. When using
Suggested change
|
||||||
| raise UserError(self.env._('Can\'t delete a property that is not new or cancelled.')) | ||||||
|
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. nitpick
Suggested change
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||
| from datetime import datetime, timedelta | ||||||
|
|
||||||
| from odoo import api, fields, models | ||||||
| from odoo.exceptions import UserError, ValidationError | ||||||
| from odoo.tools.float_utils import float_compare | ||||||
|
|
||||||
|
|
||||||
| class EstatePropertyOffer(models.Model): | ||||||
| _name = 'estate.property.offer' | ||||||
| _description = 'Estate Property Offer' | ||||||
| _order = 'price desc' | ||||||
|
|
||||||
| date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline') | ||||||
| partner_id = fields.Many2one(comodel_name='res.partner', string='Partner', required=True) | ||||||
| price = fields.Float(string='Price') | ||||||
| property_id = fields.Many2one(comodel_name='estate.property', string='Property', required=True) | ||||||
| property_type = fields.Many2one(related='property_id.property_type', string='Property type') | ||||||
| status = fields.Selection( | ||||||
| string='Status', | ||||||
| readonly=True, | ||||||
| selection=[('accepted', 'Accepted'), ('refused', 'Refused')], | ||||||
| copy=False, | ||||||
| ) | ||||||
| validity = fields.Integer(string='Validity', default=7) | ||||||
|
|
||||||
| _strictly_positive_price = models.Constraint( | ||||||
| 'CHECK(price > 0)', | ||||||
| 'The price of an offer should be strictly positive', | ||||||
| ) | ||||||
|
|
||||||
| def _compute_display_name(self): | ||||||
| for record in self: | ||||||
| record.display_name = self.env._("Estate Property Offer %s", record.id) | ||||||
|
|
||||||
| @api.depends('create_date', 'validity') | ||||||
| def _compute_date_deadline(self): | ||||||
| for record in self: | ||||||
| if record.create_date: | ||||||
| record.date_deadline = record.create_date + timedelta(days=record.validity) | ||||||
| else: | ||||||
| record.date_deadline = datetime.now() + timedelta(days=record.validity) | ||||||
|
|
||||||
| def _inverse_date_deadline(self): | ||||||
| for record in self: | ||||||
| record.validity = (record.date_deadline - record.create_date.date()).days | ||||||
|
|
||||||
| def accept_offer(self): | ||||||
| for record in self: | ||||||
| if any(offer.status == 'accepted' for offer in record.property_id.offers): | ||||||
| raise UserError(self.env._('Only one offer can be accepted by property.')) | ||||||
| record.status = 'accepted' | ||||||
| record.property_id.buyer = record.partner_id | ||||||
| record.property_id.selling_price = record.price | ||||||
| record.property_id.state = 'offer_accepted' | ||||||
| return True | ||||||
|
|
||||||
| def refuse_offer(self): | ||||||
| for record in self: | ||||||
| if record.status == 'accepted': | ||||||
| record.property_id.buyer = None | ||||||
| record.property_id.selling_price = None | ||||||
| record.property_id.state = 'offer_received' | ||||||
| record.status = 'refused' | ||||||
| return True | ||||||
|
|
||||||
| def action_view_property(self): | ||||||
| self.ensure_one() | ||||||
| return { | ||||||
| 'type': 'ir.actions.act_window', | ||||||
| 'res_model': 'estate.property', | ||||||
| 'name': self.env._('Properties'), | ||||||
| 'views': [[False, 'list'], [False, 'form']], | ||||||
| 'domain': [('id', '=', self.property_id.id)], | ||||||
| } | ||||||
|
|
||||||
| @api.constrains('price') | ||||||
| def _check_price(self): | ||||||
| for record in self: | ||||||
| if float_compare(record.property_id.expected_price * 0.9, record.price, 2) == 1: | ||||||
| raise ValidationError(self.env._(r'The price of an offer cannot be lower than 90% of the expected price of the property.')) | ||||||
|
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. your IDE probably doesn't like it but python accept it fine
Suggested change
|
||||||
|
|
||||||
| @api.model | ||||||
| def create(self, vals_list): | ||||||
| for vals in vals_list: | ||||||
| property = self.env['estate.property'].browse(vals['property_id']).with_prefetch(self.ids) | ||||||
| if any(offer.price > vals['price'] for offer in property.offers): | ||||||
|
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. price is not a required field so it may be missing (probably blocked by the constraint tho)
Suggested change
|
||||||
| raise UserError(self.env._('An offer cannot have a lower price than an existing offer.')) | ||||||
| if property.state == 'new': | ||||||
| property.state = 'offer_received' | ||||||
| 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 EstatePropertyTag(models.Model): | ||||||
| _name = 'estate.property.tag' | ||||||
| _description = 'Estate Property Tag' | ||||||
| _order = 'name' | ||||||
|
|
||||||
| name = fields.Char(string='Name', required=True, default="Unknown") | ||||||
|
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. nitpick, by default Odoo using the technical name of the field with
Suggested change
|
||||||
| color = fields.Integer(string='Color') | ||||||
|
|
||||||
| _unique_tag_name = models.Constraint( | ||||||
| 'unique (name)', | ||||||
| 'The name of a property tag should be unique.', | ||||||
| ) | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,23 @@ | ||||||||||||||||||||||
| from odoo import api, fields, models | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class EstatePropertyType(models.Model): | ||||||||||||||||||||||
| _name = 'estate.property.type' | ||||||||||||||||||||||
| _description = 'Estate Property Type' | ||||||||||||||||||||||
| _order = 'sequence, name' | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| name = fields.Char(string='Name', required=True, default="Unknown") | ||||||||||||||||||||||
| offers = fields.One2many(comodel_name='estate.property.offer', inverse_name='property_type', string="Offers") | ||||||||||||||||||||||
| offers_count = fields.Integer(string='Offers count', compute='_compute_offers_count') | ||||||||||||||||||||||
| properties = fields.One2many(comodel_name='estate.property', inverse_name='property_type', string='Properties') | ||||||||||||||||||||||
| sequence = fields.Integer(string='Sequence', default=1, help='Used to order stages. Lower is better.') | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| _unique_type_name = models.Constraint( | ||||||||||||||||||||||
| 'unique (name)', | ||||||||||||||||||||||
| 'The name of a property type should be unique.', | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @api.depends('offers') | ||||||||||||||||||||||
| def _compute_offers_count(self): | ||||||||||||||||||||||
| for record in self: | ||||||||||||||||||||||
| record.offers_count = len(record.offers) | ||||||||||||||||||||||
|
Comment on lines
+22
to
+23
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. nitpick, this is probably too much for now, but here's what this would look like instead.
Suggested change
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink | ||
| estate.access.estate.property,access.estate.property,model_estate_property,base.group_user,1,1,1,1 | ||
| estate.access.estate.property.offer,access.estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1 | ||
| estate.access.estate.property.tag,access.estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1 | ||
| estate.access.estate.property.type,access.estate.property.type,model_estate_property_type,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <record id="estate_view_inherited_users_form" model="ir.ui.view"> | ||
| <field name="name">estate.inherited.users.form</field> | ||
| <field name="model">res.users</field> | ||
| <field name="inherit_id" ref="base.view_users_form"/> | ||
| <field name="arch" type="xml"> | ||
| <xpath expr="//notebook" position="inside"> | ||
| <page string="Properties"> | ||
| <field name="property_ids" /> | ||
| </page> | ||
| </xpath> | ||
| </field> | ||
| </record> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <menuitem id="estate_menu_root" name="Real Estate"> | ||
| <menuitem id="estate_menu_advertisements" name="Advertisements"> | ||
| <menuitem id="estate_menu_properties" action="estate_action_to_properties" /> | ||
| </menuitem> | ||
| <menuitem id="estate_menu_settings" name="Settings"> | ||
| <menuitem id="estate_menu_property_types" action="estate_action_to_property_types" /> | ||
| <menuitem id="estate_menu_property_tags" action="estate_action_to_property_tags" /> | ||
| </menuitem> | ||
| </menuitem> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <record id="estate_action_to_property_offers" 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> | ||
| <field name="domain">[('property_type', '=', active_id)]</field> | ||
| </record> | ||
|
|
||
| <record id="estate_view_property_offers_list" model="ir.ui.view"> | ||
| <field name="name">estate.property.offer.list</field> | ||
| <field name="model">estate.property.offer</field> | ||
| <field name="arch" type="xml"> | ||
| <list string="Properties" editable="top" decoration-danger="status=='refused'" decoration-success="status=='accepted'"> | ||
| <field name="price" /> | ||
| <field name="partner_id" /> | ||
| <field name="property_id" /> | ||
| <field name="validity" /> | ||
| <field name="date_deadline" /> | ||
| <button name="accept_offer" type="object" string="Accept" icon="fa-check" invisible="status in ['accepted', 'refused']" /> | ||
| <button name="refuse_offer" type="object" string="Refuse" icon="fa-times" invisible="status in ['accepted', 'refused']" /> | ||
| </list> | ||
| </field> | ||
| </record> | ||
|
|
||
| <record id="estate_view_property_offers_form" model="ir.ui.view"> | ||
| <field name="name">estate.property.offer.form</field> | ||
| <field name="model">estate.property.offer</field> | ||
| <field name="arch" type="xml"> | ||
| <form string="Property Offer"> | ||
| <header> | ||
| <button name="accept_offer" type="object" string="Accept" /> | ||
| <button name="refuse_offer" type="object" string="Refuse" /> | ||
| </header> | ||
| <sheet> | ||
| <div name="button_box" class="oe_button_box"> | ||
| <button name="action_view_property" type="object" class="oe_stat_button" icon="fa-home"> | ||
| <field name="property_id" widget="statinfo" string="Property" /> | ||
| </button> | ||
| </div> | ||
| <group> | ||
| <field name="price" /> | ||
| <field name="status" /> | ||
| <field name="partner_id" /> | ||
| <field name="property_id" /> | ||
| <field name="validity" /> | ||
| <field name="date_deadline" /> | ||
| </group> | ||
| </sheet> | ||
| </form> | ||
| </field> | ||
| </record> | ||
| </odoo> |
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.
nitpick, both are valid