diff --git a/.gitignore b/.gitignore index b6e47617de1..eded9e6c769 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,5 @@ dmypy.json # Pyre type checker .pyre/ + +.vscode/* \ No newline at end of file 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..e4efa83f70b --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,27 @@ +{ + 'name': "Real Estate", + + 'summary': "Cool Real Estate App", + 'description': """ +Cool Real Estate App +Wow This is a description omgg + """, + 'author': "kmhma", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials', + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['base'], + + 'data': [ + "security/ir.model.access.csv", + "views/property_type_views.xml", + "views/property_tag_views.xml", + "views/property_offer_views.xml", + "views/estate_property_views.xml", + "views/res_users_views.xml", + "views/estate_menus_views.xml", + ], + 'license': 'LGPL-3', +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9a2189b6382 --- /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 diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..5b4d1145d77 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,125 @@ +from odoo import models, fields, api, exceptions +from odoo.tools import float_compare + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate model" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Char() + postcode = fields.Char() + available_from = fields.Date( + copy=False, default=fields.Date.add(fields.Date.today(), 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(string="Living Area (sqm)") + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + active = fields.Boolean(default=True) + garden_area = fields.Integer(string="Garden Area (sqm)") + garden_orientation = fields.Selection( + string="Garden Orientation", + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + ) + state = fields.Selection( + string="State", + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + required=True, + copy=False, + default="new", + ) + property_type_id = fields.Many2one( + comodel_name="estate.property.type", string="Property Type" + ) + buyer_id = fields.Many2one(comodel_name="res.partner", copy=False) + salesman_id = fields.Many2one( + comodel_name="res.users", default=lambda self: self.env.uid + ) + tag_ids = fields.Many2many(comodel_name="estate.property.tag", string="Tags") + offer_ids = fields.One2many( + comodel_name="estate.property.offer", inverse_name="property_id" + ) + total_area = fields.Integer(compute="_compute_area", string="Total Area (sqm)") + best_price = fields.Float(compute="_compute_best_price") + + _postive_expected_price = models.Constraint( + "CHECK (expected_price > 0)", "The expected price must be strictly positive" + ) + + _postive_selling_price = models.Constraint( + "CHECK (selling_price > 0)", "The selling price must be strictly positive" + ) + + @api.depends("living_area", "garden_area") + def _compute_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: + property.best_price = max(property.offer_ids.mapped("price"), default=0.0) + + @api.onchange("offer_ids") + def _onchange_offer_receieved(self): + if len(self.offer_ids): + self.state = "offer_received" + + @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 sell_property(self): + for property in self: + if property.state == "cancelled": + raise exceptions.UserError("You cannot sell a cancelled property") + property.state = "sold" + return True + + def cancel_property(self): + for property in self: + if property.state == "sold": + raise exceptions.UserError("You cannot cancel a sold property") + else: + property.state = "cancelled" + return True + + @api.constrains("selling_price") + def _check_selling_price(self): + for property in self: + if ( + float_compare(property.selling_price, 0.9 * property.expected_price, 2) + == -1 + ): + raise exceptions.ValidationError( + "The selling price must be at least 90% of the expected price." + ) + + @api.ondelete(at_uninstall=False) + def _unlink_property_with_offer(self): + for property in self: + if property.state not in ["new", "cancelled"]: + raise exceptions.UserError( + "You cannot delete a property with existing offers!" + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..b25f99ba7fa --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,73 @@ +from odoo import models, fields, api, exceptions + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "property offer" + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + copy=False, + selection=[ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + ) + partner_id = fields.Many2one("res.partner", required=True, string="Partner") + property_id = fields.Many2one( + "estate.property", required=True, string="Property", ondelete="cascade" + ) + validity = fields.Integer(default=7) + deadline_date = fields.Date( + compute="_compute_deadline_date", inverse="_inverse_deadline_date" + ) + property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) + + _postive_price = models.Constraint( + "CHECK (price > 0)", + "The expected price must be strictly positive" + ) + + @api.depends("create_date", "validity") + def _compute_deadline_date(self): + for record in self: + create_date = record.create_date or fields.Date.today() + record.deadline_date = fields.Date.add(create_date, days=record.validity) + + def _inverse_deadline_date(self): + for record in self: + create_date = record.create_date or fields.Date.today() + record.validity = (record.deadline_date - create_date.date()).days + + def button_accept_offer(self): + for offer in self: + if offer.property_id.state == "sold": + raise exceptions.UserError( + f"Property '{offer.property_id.name}' is already sold" + ) + offer.property_id.write({ + "buyer_id": offer.partner_id, + "selling_price": offer.price, + "state": "offer_accepted" + }) + offer.status = "accepted" + return True + + def button_refuse_offer(self): + for offer in self: + if offer.property_id.state == "sold" and offer.status == "accepted": + raise exceptions.UserError( + f"Property '{offer.property_id.name}' is already sold to '{offer.partner_id.name}'" + ) + offer.status = "refused" + return True + + @api.model_create_multi + def create(self, vals_list): + for val in vals_list: + property_id = self.env['estate.property'].browse(val['property_id']) + if property_id.offer_ids and max(offer.price for offer in property_id.offer_ids) > val['price']: + raise exceptions.UserError("An offer with a higher price exists") + property_id.state = "offer_received" + return super().create(vals_list) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..5d62ae9ceb8 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "property tags" + _order = "name asc" + + name = fields.Char(required=True) + color = fields.Integer() + + _unique_tag = models.Constraint("unique(name)", "This tag already exists") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..ef9d173ef7b --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,23 @@ +from odoo import models, fields, api + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "property types" + _order = "sequence, name asc" + + name = fields.Char(required=True) + sequence = fields.Integer('Sequence', default=1) + + property_ids = fields.One2many( + comodel_name="estate.property", inverse_name="property_type_id" + ) + offer_ids = fields.One2many(comodel_name="estate.property.offer", inverse_name="property_type_id") + offer_count = fields.Integer(compute="_compute_offer_count", default=0, string="Offers") + + _unique_type = models.Constraint("unique(name)", "This type already exists") + + @api.depends("offer_ids") + def _compute_offer_count(self): + for type in self: + type.offer_count = len(type.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..9912be284b6 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class Users(models.Model): + _inherit = "res.users" + property_ids = fields.One2many(comodel_name="estate.property", inverse_name="salesman_id", + string="Properties") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..be6afd74af1 --- /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 +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,access.res.users,model_res_users,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus_views.xml b/estate/views/estate_menus_views.xml new file mode 100644 index 00000000000..412a4657d0b --- /dev/null +++ b/estate/views/estate_menus_views.xml @@ -0,0 +1,14 @@ + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..79ce14c26a9 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,155 @@ + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+
+ + +

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..ea14ea580f4 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,36 @@ + + + + + res.users.view.form.inherit + res.users + + + + + + + + + + + + + + + + + + + + + + + + + Users & Companies + res.users + list,form + + + diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..8806a6ccce8 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': "Real Estate Account", + + 'summary': "Cool Real Estate For Invoicing ", + 'description': """ +Cool Real Estate App +Wow This is a description omgg + """, + 'author': "kmhma", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials', + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['base', 'estate', 'account'], + + 'data': [ + ], + 'license': 'LGPL-3', +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..1c6611b61e2 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,32 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def sell_property(self): + partner_id = self.buyer_id.id + move_type = "out_invoice" + self.env["account.move"].create( + { + "partner_id": partner_id, + "move_type": move_type, + "invoice_line_ids": [ + Command.create( + { + "name": "Selling Price", + "quantity": 1, + "price_unit": 0.06 * self.selling_price, + } + ), + Command.create( + { + "name": "Administrative Fees", + "quantity": 1, + "price_unit": 100, + } + ), + ], + } + ) + return super().sell_property()