-
Notifications
You must be signed in to change notification settings - Fork 3.3k
onboarding server framework #1383
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 13 commits
e1d7982
cb84f6e
99af35b
a969dc2
031009b
fa0cc1c
b4428f5
e63118d
1ac00c6
c3c4378
0d07749
721c3c7
873b465
65c6202
c1476ea
20f44e2
d5e25b3
ef40a4d
e734266
cf99a0c
83db6b4
502b3c8
6d5c63e
356cd8e
fdf84cf
a22a4ec
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 |
|---|---|---|
|
|
@@ -127,3 +127,5 @@ dmypy.json | |
|
|
||
| # Pyre type checker | ||
| .pyre/ | ||
|
|
||
| .vscode | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| from . import models | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| 'name': 'estate-kehey', | ||
| 'depends': [ | ||
| 'base', | ||
| ], | ||
| 'data': [ | ||
| 'data/ir.model.access.csv', | ||
| 'views/estate_property_views.xml', | ||
| 'views/estate_property_settings_views.xml', | ||
| 'views/users_extra_views.xml', | ||
| 'data/estate_menus.xml' | ||
| ], | ||
| 'application':True | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <menuitem id="test_menu_root" name="Real Estate"> | ||
| <menuitem id="test_first_level_menu" name="Advertisements"> | ||
| <menuitem id="test_model_menu_action" action="estate_property_action"/> | ||
| </menuitem> | ||
| <menuitem id="settings" name="Settings"> | ||
| <menuitem id="property_type_menu_action" action="estate_property_type_action"/> | ||
| <menuitem id="property_tag_menu_action" action="estate_property_tag_action"/> | ||
| </menuitem> | ||
| </menuitem> | ||
| </odoo> | ||
|
|
| 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_user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1 | ||
| access_estate_property_type_user,access_estate_property_user,model_estate_property_type,base.group_user,1,1,1,1 | ||
| access_estate_property_tag_user,access_estate_property_user,model_estate_property_tag,base.group_user,1,1,1,1 | ||
| access_estate_property_offer_user,access_estate_property_user,model_estate_property_offer,base.group_user,1,1,1,1 | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| from . import estate_property | ||
| from . import estate_property_type | ||
| from . import estate_property_tag | ||
| from . import estate_property_offer | ||
| from . import inhereted_users | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,101 @@ | ||||||
| from dateutil.relativedelta import relativedelta | ||||||
|
|
||||||
| from odoo import api, exceptions, fields, models | ||||||
|
|
||||||
|
|
||||||
| class EstateProperty(models.Model): | ||||||
| _name = "estate.property" | ||||||
| _description = "estate model" | ||||||
| _order = "id desc" | ||||||
|
|
||||||
| name = fields.Char(required=True) | ||||||
| salesman_id = fields.Many2one("res.partner", string="Salesman") | ||||||
| buyer_id = fields.Many2one("res.users", default=lambda self: self.env.user, string="Buyer") | ||||||
| type_id = fields.Many2one("estate.property.type") | ||||||
| tags_id = fields.Many2many("estate.property.tag") | ||||||
| offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") | ||||||
| active = fields.Boolean(default=True) | ||||||
| state = fields.Selection( | ||||||
| required=True, | ||||||
| copy=False, | ||||||
| default="new", | ||||||
| selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")], | ||||||
| ) | ||||||
| description = fields.Text() | ||||||
| postcode = fields.Char() | ||||||
| date_availability = fields.Datetime(copy=False, default=fields.Datetime.today() + (relativedelta(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='type', | ||||||
| selection=[('north', 'North'), ('south', 'South'), ('East', 'east'), ('West', 'west')], | ||||||
| ) | ||||||
| total_area = fields.Integer(string="Total Area", compute="_compute_total_surface") | ||||||
| best_offer = fields.Float(string="Best Offer", compute="_compute_best_offer") | ||||||
|
|
||||||
| # ==========constraints=================== | ||||||
| _check_positive_expected_price = models.Constraint("CHECK (expected_price > 0)", "expected price should be bigger than 0") | ||||||
| _check_positive_selling_price = models.Constraint("CHECK (selling_price > 0)", "expected price should be bigger than 0") | ||||||
|
|
||||||
| @api.constrains("selling_price", "expected_price") | ||||||
| def _check_enough_selling_price(self): | ||||||
| for record in self: | ||||||
| offer_made = "accepted" in record.offer_ids.mapped("status") | ||||||
| price_good_enough = record.selling_price > 0.9 * record.expected_price | ||||||
| if not price_good_enough and offer_made: | ||||||
| to_low_user_error = "selling price is too low for the expected price" | ||||||
| raise exceptions.ValidationError(to_low_user_error) | ||||||
|
|
||||||
| # ==========computed fields=============== | ||||||
| @api.depends('garden_area', 'living_area') | ||||||
| def _compute_total_surface(self): | ||||||
| for record in self: | ||||||
| record.total_area = record.garden_area + record.living_area | ||||||
|
|
||||||
| @api.depends('offer_ids') | ||||||
| def _compute_best_offer(self): | ||||||
| for record in self: | ||||||
| if not record.offer_ids: | ||||||
| record.best_offer = 0 | ||||||
| else: | ||||||
| record.best_offer = max(record.offer_ids.mapped("price")) | ||||||
|
|
||||||
| # ============onchage fields============== | ||||||
| @api.onchange("garden") | ||||||
| def _onchange_garden(self): | ||||||
| if self.garden: | ||||||
| self.garden_area = 10 | ||||||
| self.garden_orientation = "north" | ||||||
| else: | ||||||
| self.garden_area = False | ||||||
| self.garden_orientation = False | ||||||
|
|
||||||
| # ==========button functions============== | ||||||
| def action_property_sold(self): | ||||||
| for record in self: | ||||||
| if record.state == "cancelled": | ||||||
| no_sell_cancelled_error = "Can't sell a cancelled property" | ||||||
| raise exceptions.UserError(no_sell_cancelled_error) | ||||||
| record.state = "sold" | ||||||
| return True | ||||||
|
|
||||||
| def action_property_cancelled(self): | ||||||
| for record in self: | ||||||
| if record.state == "sold": | ||||||
| no_sell_a_sold_property = "Can't cancel a sold property" | ||||||
| raise exceptions.UserError(no_sell_a_sold_property) | ||||||
| record.state = "cancelled" | ||||||
| return True | ||||||
|
|
||||||
| @api.ondelete(at_uninstall=False) | ||||||
| def ondelete(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. It is kinda a nitpick but we usually name those method as
Suggested change
|
||||||
| for property in self: | ||||||
| if property.state in ("new", "cancelled"): | ||||||
| no_delete_new_or_cancelled_record = "cannot delete new or cancelled record" | ||||||
| raise exceptions.UserError(no_delete_new_or_cancelled_record) | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,66 @@ | ||||||||||||||||||
| from dateutil.relativedelta import relativedelta | ||||||||||||||||||
|
|
||||||||||||||||||
| from odoo import api, exceptions, fields, models | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class EstatePropertyOffer(models.Model): | ||||||||||||||||||
| _name = "estate.property.offer" | ||||||||||||||||||
| _description = "estate offer model" | ||||||||||||||||||
| _order = "price desc" | ||||||||||||||||||
|
|
||||||||||||||||||
| name = fields.Char(required=True) | ||||||||||||||||||
| price = fields.Float() | ||||||||||||||||||
| status = fields.Selection( | ||||||||||||||||||
| string='status', | ||||||||||||||||||
| copy=False, | ||||||||||||||||||
| selection=[('accepted', 'Accepted'), ('refused', 'Refused')], | ||||||||||||||||||
| ) | ||||||||||||||||||
| partner_id = fields.Many2one("res.users", required=True) | ||||||||||||||||||
| property_id = fields.Many2one("estate.property", required=True, ondelete="cascade") | ||||||||||||||||||
| date_deadline = fields.Datetime(string="Deadline", compute="compute_deadline", inverse="_inverse_deadline") | ||||||||||||||||||
| validity = fields.Integer(string="validity", default=7) | ||||||||||||||||||
| property_type_id = fields.Many2one("estate.property.type", related="property_id.type_id", string="Property Type", store=True) | ||||||||||||||||||
|
|
||||||||||||||||||
| # =========contraints============ | ||||||||||||||||||
| _check_positive_offer_price = models.Constraint("CHECK (price > 0)", "expected price should be bigger than 0") | ||||||||||||||||||
|
|
||||||||||||||||||
| @api.depends('validity', "create_date") | ||||||||||||||||||
| def compute_deadline(self): | ||||||||||||||||||
| for record in self: | ||||||||||||||||||
| if record.create_date: | ||||||||||||||||||
| record.date_deadline = record.create_date + relativedelta(days=record.validity) | ||||||||||||||||||
| else: | ||||||||||||||||||
| record.date_deadline = fields.Datetime.today() + relativedelta(days=record.validity) | ||||||||||||||||||
|
Comment on lines
+29
to
+33
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.
Suggested change
To avoid duplicating some of the code we can do this, wdyt? |
||||||||||||||||||
|
|
||||||||||||||||||
| def _inverse_deadline(self): | ||||||||||||||||||
| for record in self: | ||||||||||||||||||
| record.validity = (record.date_deadline - record.create_date).days | ||||||||||||||||||
|
|
||||||||||||||||||
| # ===========button actions=========== | ||||||||||||||||||
| def action_accept(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. Nitpick 😇 not a needed line |
||||||||||||||||||
| for record in self: | ||||||||||||||||||
| if "accepted" in record.property_id.offer_ids.mapped("status"): | ||||||||||||||||||
| already_accepted_exception = "already accepted an offer!" | ||||||||||||||||||
| raise exceptions.UserError(already_accepted_exception) | ||||||||||||||||||
| record.property_id.buyer_id = record.partner_id | ||||||||||||||||||
| record.property_id.state = "offer_accepted" | ||||||||||||||||||
| record.status = "accepted" | ||||||||||||||||||
| record.property_id.selling_price = record.price | ||||||||||||||||||
|
|
||||||||||||||||||
| def action_refuse(self): | ||||||||||||||||||
| for record in self: | ||||||||||||||||||
| for property in record.property_id: | ||||||||||||||||||
| record.status = "refused" | ||||||||||||||||||
|
|
||||||||||||||||||
| @api.model | ||||||||||||||||||
| def create(self, vals): | ||||||||||||||||||
| for to_create in vals: | ||||||||||||||||||
| property = self.env["estate.property"].browse(to_create["property_id"]) | ||||||||||||||||||
| new_bid = to_create["price"] | ||||||||||||||||||
| for offer in property.offer_ids: | ||||||||||||||||||
| if offer.price > new_bid: | ||||||||||||||||||
|
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. since you introduced an order for the offers here https://github.com/odoo/tutorials/pull/1383/changes#diff-8000c23a2907f34f0cc387be9dc4178411fda81b674724854bd4d048f9e11b16R9 the recordset will be ordered upon price by default so normally |
||||||||||||||||||
| cant_bit_lower_exception = "can't bid lower than the highest bid" | ||||||||||||||||||
| raise exceptions.UserError(cant_bit_lower_exception) | ||||||||||||||||||
| property.state = "offer_received" | ||||||||||||||||||
| return super().create(vals) | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from odoo import fields, models | ||
|
|
||
| class EstatePropertyTag(models.Model): | ||
| _name = "estate.property.tag" | ||
| _description = "estate tag model" | ||
| _order = "name" | ||
|
|
||
| name = fields.Char(required=True) | ||
|
|
||
|
|
||
| _check_unique_tag = models.Constraint("UNIQUE(name)", "tags should be unique") | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from odoo import api, fields, models | ||
|
|
||
| class EstatePropertyType(models.Model): | ||
| _name = "estate.property.type" | ||
| _description = "estate type model" | ||
| _order = "sequence, name" | ||
|
|
||
| name = fields.Char() | ||
| property_ids = fields.One2many("estate.property", "type_id") | ||
| sequence = fields.Integer(default=0) | ||
| offer_ids = fields.One2many("estate.property.offer", "property_type_id") | ||
| offer_count = fields.Integer(compute="_compute_offer_count") | ||
|
|
||
| _check_unique_type = models.Constraint("UNIQUE(name)", "types should be unique") | ||
|
|
||
| @api.depends("offer_ids") | ||
| def _compute_offer_count(self): | ||
| for property_type_record in self: | ||
| property_type_record.offer_count = len(property_type_record.offer_ids) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| from odoo import fields, models | ||
|
|
||
| class InheritedModel(models.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. When inherit a model we name the class the same as the parent class |
||
| _inherit = "res.users" | ||
|
|
||
| property_ids = fields.One2many( | ||
| "estate.property", | ||
| "salesman_id", | ||
| domain=[("state", "in", ["new", "offer_received"])]) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| <?xml version="1.0"?> | ||
|
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 a better practice to have a XML view file for each model. |
||
| <odoo> | ||
|
|
||
| <record id="estate_property_type_view_list" model="ir.ui.view"> | ||
| <field name="name">estate.property.type.list</field> | ||
| <field name="model">estate.property.type</field> | ||
| <field name="arch" type="xml"> | ||
| <list string="Tests"> | ||
| <field name="sequence" string="sequence" widget="handle"/> | ||
| <field name="name"/> | ||
| </list> | ||
| </field> | ||
| </record> | ||
|
|
||
| <record id="estate_property_tag_view_list" model="ir.ui.view"> | ||
| <field name="name">estate.property.tag.list</field> | ||
| <field name="model">estate.property.tag</field> | ||
| <field name="arch" type="xml"> | ||
| <list string="Tests" editable="bottom"> | ||
| <field name="name"/> | ||
| </list> | ||
| </field> | ||
| </record> | ||
|
|
||
| <record id="estate_property_type_view_form" model="ir.ui.view"> | ||
| <field name="name">estate.property.type.form</field> | ||
| <field name="model">estate.property.type</field> | ||
| <field name="arch" type="xml"> | ||
| <form string="Properties"> | ||
| <header> | ||
| <button class="oe_stat_button" type="action" name="%(estate.estate_property_offers_action)d" icon="fa-money" string="Offers"/> | ||
| </header> | ||
| <sheet> | ||
| <notebook> | ||
| <page string="Properties"> | ||
| <field name="property_ids"> | ||
| <list> | ||
| <field name="name" string="Title"/> | ||
| <field name="expected_price" string="Expected Price"/> | ||
| <field name="state" string="Status"/> | ||
| </list> | ||
| </field> | ||
| </page> | ||
| </notebook> | ||
| </sheet> | ||
| </form> | ||
| </field> | ||
| </record> | ||
|
|
||
| <record id="estate_property_type_action" model="ir.actions.act_window"> | ||
| <field name="name">Property Types</field> | ||
| <field name="res_model">estate.property.type</field> | ||
| <field name="view_mode">list,form</field> | ||
| </record> | ||
|
|
||
| <record id="estate_property_tag_action" model="ir.actions.act_window"> | ||
| <field name="name">Property Tags</field> | ||
| <field name="res_model">estate.property.tag</field> | ||
| <field name="view_mode">list,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.
We normally tend to name the compute method after the exact name of field