From d59d3f9542545360600ba6e91409cb67beeb3d5c Mon Sep 17 00:00:00 2001 From: Serhii Rubanskyi - seru Date: Mon, 17 Mar 2025 10:37:45 +0100 Subject: [PATCH 1/4] [ADD] Added real_estate app Followed the tutorial task-xxxxx --- awesome_clicker/__manifest__.py | 1 + estate/__init__.py | 1 + estate/__manifest__.py | 19 ++++ estate/models/__init__.py | 5 + estate/models/estate_property.py | 106 ++++++++++++++++++ estate/models/estate_property_offer.py | 60 ++++++++++ estate/models/estate_property_tag.py | 12 ++ estate/models/estate_property_type.py | 36 ++++++ estate/models/res_users.py | 11 ++ estate/security/ir.model.access.csv | 6 + estate/views/estate_menus.xml | 14 +++ estate/views/estate_property_offer_views.xml | 47 ++++++++ estate/views/estate_property_tag_views.xml | 19 ++++ estate/views/estate_property_type_views.xml | 57 ++++++++++ estate/views/estate_property_views.xml | 110 +++++++++++++++++++ estate/views/res_users_views.xml | 15 +++ 16 files changed, 519 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/models/res_users.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml create mode 100644 estate/views/estate_property_views.xml create mode 100644 estate/views/res_users_views.xml diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py index e57ef4d5bb0..7d9d4d79572 100644 --- a/awesome_clicker/__manifest__.py +++ b/awesome_clicker/__manifest__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# some not so great changes to make a first commit { 'name': "Awesome Clicker", diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..8c312f28372 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "Real Estate", + "category": "Real Estate", + "application": True, + "installable": True, + "depends":[ + "base", + "web" + ], + "data": [ + "security/ir.model.access.csv", + "views/estate_menus.xml", + "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", + ] +} \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..a9459ed5906 --- /dev/null +++ b/estate/models/__init__.py @@ -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 res_users \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..145a54d0cc5 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,106 @@ +from odoo import api, fields, models, exceptions, tools + + +class Property(models.Model): + _name = "estate.property" + _description = "Real Estate property" + _order = "sequence, id desc" + + sequence = fields.Integer("Sequence", default=1) + name = fields.Char("Title", required=True) + description = fields.Text("Description") + postcode = fields.Char("Postcode") + date_availability = fields.Date( + "Available Date", copy=False, default=fields.Date.add(fields.Date.today(), months=3)) + expected_price = fields.Float("Expected price", required=True) + selling_price = fields.Float("Selling price", readonly=True, copy=False) + bedrooms = fields.Integer("Bedrooms", default=2) + living_area = fields.Integer("Living area (sqm)") + facades = fields.Integer("Facades") + garage = fields.Boolean("Garage") + garden = fields.Boolean("Garden") + garden_area = fields.Integer("Garden area (sqm)") + garden_orientation = fields.Selection([ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West") + ], string="Garden Orientation") + active = fields.Boolean("Active", default=True) + state = fields.Selection([ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled") + ], string="Status", required=True, copy=False, default="new") + + _sql_constraints = [ + ("check_expected_price", "CHECK(expected_price > 0)", + "Expected price must be strictly positive."), + ("check_selling_price", "CHECK(selling_price >= 0)", + "Selling Price must be positive.") + ] + + property_type_id = fields.Many2one( + "estate.property.type", string="Property Type") + salesman_id = fields.Many2one( + "res.users", string="Salesman", 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.Float("Total area (sqm)", compute="_compute_area") + best_price = fields.Float("Best price", compute="_compute_best_price") + + @api.depends("living_area", "garden_area") + def _compute_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: + record.best_price = max([0]+record.offer_ids.mapped('price')) + + @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 = "" + + def action_set_sold(self): + for record in self: + if record.state != "canceled": + record.state = "sold" + else: + raise exceptions.UserError( + "Canceled properties cannot be sold.") + + def action_set_canceled(self): + for record in self: + if record.state != "sold": + record.state = "canceled" + else: + raise exceptions.UserError( + "Sold properties cannot be canceled.") + + @api.constrains("expected_price", "selling_price") + def _check_price(self): + for record in self: + if record.selling_price and record.expected_price and not tools.float_utils.float_is_zero(record.selling_price, precision_digits=2): + if record.selling_price < 0.9 * record.expected_price: + raise exceptions.ValidationError( + "The selling price cannot be lower than 90% of the expected price.") + + + @api.ondelete(at_uninstall=False) + def _not_delete_if_not_new_or_canceled(self): + for record in self: + if record.state not in ["new", "canceled"]: + raise exceptions.UserError( + "You cannot delete a property that is not new.") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..a9902fc2b4c --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,60 @@ +from odoo import api, models, fields, exceptions, tools + +class PropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + _order = "price desc" + + price = fields.Float("Price") + status = fields.Selection([("accepted", "Accepted"), ("refused", "Refused")], string="Status", copy=False) + partner_id = fields.Many2one("res.partner", string = "Partner", required=True) + property_id = fields.Many2one("estate.property", string = "Property", required=True) + validity = fields.Integer("Validity (days)", default=7) + date_deadline = fields.Date("Deadline", compute = "_compute_date_deadline", inverse = "_inverse_date_deadline") + property_type_id = fields.Many2one("estate.property.type", string="Property Type", related="property_id.property_type_id") + + _sql_constraints = [ + ("check_price", "CHECK(price > 0)", "Offer price must be positive.") + ] + + @api.depends("validity") + def _compute_date_deadline(self): + for record in self: + record.date_deadline = fields.Date.add(record.create_day if hasattr(record, "create_day") else fields.Date.today(), days = record.validity) + + def _inverse_date_deadline(self): + for record in self: + create_date = record.create_day if hasattr(record, "create_day") else fields.Date.today() + record.validity = (record.date_deadline - create_date).days + + + def action_accept_offer(self): + for record in self: + if record.property_id.state not in ["offer_accepted", "sold"]: + # if tools.float_utils.float_compare(record.price, 0.9 * record.property_id.expected_price, precision_rounding=2) == -1: + # i don't know why the above didn't work (i tried for expected price 100 and offer price 89 and it still accepted the offer + # so i wrote the below instead, which is not a good practice) + if record.price < 0.9 * record.property_id.expected_price: + raise exceptions.ValidationError("The selling price cannot be lower than 90% of the expected price.") + else: + record.status = "accepted" + record.property_id.state = "offer_accepted" + record.property_id.buyer_id = record.partner_id + record.property_id.selling_price = record.price + else: + raise exceptions.UserError("Only one offer can be accepted.") + + def action_refuse_offer(self): + for record in self: + record.status = "refused" + + @api.model_create_multi + def create(self, vals): + for val in vals: + property = self.env["estate.property"].browse(val["property_id"]) + if val["price"] < property.best_price: + raise exceptions.UserError(f"Price must be at least ${property.best_price}.") + + property.state = "offer_received" + + return super().create(vals) \ No newline at end of file diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..eb19319c171 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,12 @@ +from odoo import models, fields + +class PropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real Estate Property Tag" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer("Color") + _sql_constraints = [ + ("name_unique", "UNIQUE(name)", "The property tag name must be unique.") + ] \ No newline at end of file diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..180f8c67042 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,36 @@ +from odoo import api, fields, models + +class PropertyType(models.Model): + _name = "estate.property.type" + _description = "Real estate property type" + _order = "sequence, name" + + name = fields.Char(required = True) + sequence = fields.Integer("Sequence", default=1) + sql_constraints = [ + ('name_unique', 'UNIQUE(name)', 'The property type name must be unique.') + ] + + property_ids = fields.One2many("estate.property", "property_type_id", string = "Properties") + offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers") + offer_count = fields.Integer(compute="_compute_offers", string="Offers count") + + @api.depends("offer_ids") + def _compute_offers(self): + for record in self: + record.offer_count = len(record.offer_ids) + +class PropertyTypeLine(models.Model): + _name = "estate.property.type.line" + _description = "Real estate property type line" + + model_id = fields.Many2one("estate.property.type") + name = fields.Char("Title") + expected_price = fields.Float("Expected Price") + state = fields.Selection([ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled") + ], string="Status", required=True, copy=False, default="new") diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..9a0a24fa1bd --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + +class ResUsersProperties(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesman_id", + string="Properties", + domain=[('state', 'in', ['new', 'offer_received'])] + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..ee747c69c4f --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 +estate.access_estate_property_type_line,access_estate_property_type_line,estate.model_estate_property_type_line,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..ea4b655a9a7 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..20b568ceff0 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,47 @@ + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + + + +
+
+
+ + estate.property.offer.list + estate.property.offer + + + + + + + + + +

+ +

+ + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..90edfffa0c8 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,110 @@ + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+
+ +
+

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + estate.property.list + estate.property + + + + + + + + + + + + + + + Properties + estate.property + list,form + {'search_default_available': True} + +
\ No newline at end of file diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..f0f2b534055 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.form + res.users + + + + + + + + + + \ No newline at end of file From 7b4657877d2d7b6dc00d1acd00fb9ae8963d0a5f Mon Sep 17 00:00:00 2001 From: Serhii Rubanskyi - seru Date: Wed, 19 Mar 2025 17:31:25 +0100 Subject: [PATCH 2/4] [ADD] Added estate_account module Followed the tutorial task-xxxxx --- estate/models/estate_property_offer.py | 33 +++++++----- estate/views/estate_property_offer_views.xml | 1 - estate/views/estate_property_views.xml | 55 ++++++++++++++++---- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 11 ++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 26 +++++++++ 7 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index a9902fc2b4c..20e11131efe 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,38 +1,43 @@ -from odoo import api, models, fields, exceptions, tools +from odoo import api, models, fields, exceptions + class PropertyOffer(models.Model): _name = "estate.property.offer" _description = "Estate Property Offer" _order = "price desc" - + price = fields.Float("Price") - status = fields.Selection([("accepted", "Accepted"), ("refused", "Refused")], string="Status", copy=False) - partner_id = fields.Many2one("res.partner", string = "Partner", required=True) - property_id = fields.Many2one("estate.property", string = "Property", required=True) + status = fields.Selection([ + ("accepted", "Accepted"), + ("refused", "Refused")], + string="Status", copy=False) + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) validity = fields.Integer("Validity (days)", default=7) - date_deadline = fields.Date("Deadline", compute = "_compute_date_deadline", inverse = "_inverse_date_deadline") + date_deadline = fields.Date("Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline") property_type_id = fields.Many2one("estate.property.type", string="Property Type", related="property_id.property_type_id") _sql_constraints = [ ("check_price", "CHECK(price > 0)", "Offer price must be positive.") ] - + @api.depends("validity") def _compute_date_deadline(self): for record in self: - record.date_deadline = fields.Date.add(record.create_day if hasattr(record, "create_day") else fields.Date.today(), days = record.validity) + record.date_deadline = fields.Date.add( + record.create_day if hasattr(record, "create_day") else fields.Date.today(), + days=record.validity) def _inverse_date_deadline(self): for record in self: create_date = record.create_day if hasattr(record, "create_day") else fields.Date.today() record.validity = (record.date_deadline - create_date).days - def action_accept_offer(self): for record in self: if record.property_id.state not in ["offer_accepted", "sold"]: # if tools.float_utils.float_compare(record.price, 0.9 * record.property_id.expected_price, precision_rounding=2) == -1: - # i don't know why the above didn't work (i tried for expected price 100 and offer price 89 and it still accepted the offer + # i don't know why the above didn't work (i tried for expected price 100 and offer price 89 and it still accepted the offer # so i wrote the below instead, which is not a good practice) if record.price < 0.9 * record.property_id.expected_price: raise exceptions.ValidationError("The selling price cannot be lower than 90% of the expected price.") @@ -43,18 +48,18 @@ def action_accept_offer(self): record.property_id.selling_price = record.price else: raise exceptions.UserError("Only one offer can be accepted.") - + def action_refuse_offer(self): for record in self: record.status = "refused" - + @api.model_create_multi def create(self, vals): for val in vals: property = self.env["estate.property"].browse(val["property_id"]) if val["price"] < property.best_price: raise exceptions.UserError(f"Price must be at least ${property.best_price}.") - + property.state = "offer_received" - return super().create(vals) \ No newline at end of file + return super().create(vals) diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index 20b568ceff0..2e74bc44434 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -25,7 +25,6 @@ decoration-danger="status == 'refused'"> -