diff --git a/.gitignore b/.gitignore index b6e47617de1..242b7777db4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ downloads/ eggs/ .eggs/ lib/ +.idea/ lib64/ parts/ sdist/ diff --git a/real_estate/__init__.py b/real_estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/real_estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/real_estate/__manifest__.py b/real_estate/__manifest__.py new file mode 100644 index 00000000000..bc5f7e9a33c --- /dev/null +++ b/real_estate/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'name': "real.estate", + 'summary': "summary of the real estate", + 'description': """Rugot first module""", + 'author': "Ruchita Gothi (Rugot)", + 'category': 'Real Estate', + 'version': '0.1', + 'depends': ['base', 'web'], + 'data': [ + 'security/ir.model.access.csv', + 'views/real_estate_property_offer_views.xml', + 'views/real_estate_property_views.xml', + 'views/real_estate_property_type_views.xml', + 'views/real_estate_tag_views.xml', + 'views/real_estate_property_maintenance_request_views.xml', + 'views/res_users_views.xml', + 'views/real_estate_menus.xml', + ], + 'application': True, + 'installable': True, + 'license': 'LGPL-3', +} diff --git a/real_estate/models/__init__.py b/real_estate/models/__init__.py new file mode 100644 index 00000000000..25960519ea3 --- /dev/null +++ b/real_estate/models/__init__.py @@ -0,0 +1,6 @@ +from . import real_estate_property +from . import real_estate_tag +from . import real_estate_property_offer +from . import real_estate_property_type +from . import real_estate_property_maintenance_request +from . import res_users diff --git a/real_estate/models/real_estate_property.py b/real_estate/models/real_estate_property.py new file mode 100644 index 00000000000..aa322e0111c --- /dev/null +++ b/real_estate/models/real_estate_property.py @@ -0,0 +1,150 @@ +from datetime import timedelta + +from odoo import models, fields, api +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_is_zero, float_compare + + +class real_estate(models.Model): + _name = 'real.estate' + _description = 'Real Estate Property' + _order = "id desc" + + name = fields.Char(required=True) + property_type_id = fields.Many2one( + "real.estate.property.type", string="Property Type") + street_address = fields.Char() + description = fields.Text() + postcode = fields.Integer() + date_availability = fields.Datetime(default=lambda self: fields.Datetime.now() + timedelta(days=90)) + expected_price = fields.Float() + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + bathrooms = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection(selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ]) + active = fields.Boolean(default=True) + tag_ids = fields.Many2many( + "real.estate.tag", string="Tags", ondelete='cascade') + offer_ids = fields.One2many( + "real.estate.property.offer", "property_id", string="Offers") + total_area = fields.Float(compute="_compute_total", store=True) + best_price = fields.Float( + string="Best Offer", + compute="_compute_best_price", + store=True) + # ist_time = fields.Char( + # string="Created On (IST)", + # compute="_compute_create_date_ist", + # store=True) + stage = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], default='new') + buyer_id = fields.Many2one( + 'res.partner', + string='Buyer', + copy=False) + selling_price = fields.Float() + maintenance_request_ids = fields.One2many( + "real.estate.property.maintenance.request", "property_id", string="Maintenance Requests") + total_maintenance_cost = fields.Float(compute="_compute_total_maintenance_cost", store=True, string="Total Cost") + salesperson_id = fields.Many2one( + 'res.users', + string='Salesperson' + ) + _check_expected_price_positive = models.Constraint( + 'CHECK(expected_price > 0)', + 'The expected price must be strictly positive.', + ) + + @api.constrains('expected_price', 'selling_price') + def _check_selling_price(self): + for rec in self: + if float_is_zero(rec.selling_price, precision_rounding=0.01): + continue + if float_compare( + rec.selling_price, + rec.expected_price * 0.9, + precision_rounding=0.01) < 0: + raise ValidationError( + 'The selling price cannot be lower than 90% of the expected price.') + + @api.depends('offer_ids.price') + def _compute_best_price(self): + for record in self: + prices = [] + for offer in record.offer_ids: + prices.append(offer.price) + record.best_price = max(prices) if prices else 0.0 + + @api.depends('maintenance_request_ids.cost') + def _compute_total_maintenance_cost(self): + for record in self: + costs = record.maintenance_request_ids.mapped('cost') + record.total_maintenance_cost = sum(costs) if costs else 0.0 + + @api.depends('living_area', 'garden_area') + def _compute_total(self): + for record in self: + record.total_area = (record.living_area or 0) + (record.garden_area or 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.ondelete(at_uninstall=False) + def _check_property_delete(self): + for record in self: + if record.stage not in ('new', 'cancelled'): + raise UserError( + "You can only delete properties in New or Cancelled state." + ) + + # @api.depends('create_date') + # def _compute_create_date_ist(self): + # for rec in self: + # if rec.create_date: + # ist_dt = fields.Datetime.context_timestamp( + # rec, rec.create_date + # ) + # rec.ist_time = ist_dt.strftime("%Y-%m-%d %H:%M:%S") + # else: + # rec.ist_time = False + + def action_cancel(self): + if self.stage == 'sold': + raise UserError("A sold property cannot be cancelled.") + self.stage = 'cancelled' + + def action_sold(self): + if self.stage == 'cancelled': + raise UserError("A cancelled property cannot be sold.") + maintenace_request = self.maintenance_request_ids.filtered_domain([('status', '!=', 'done')]) + if maintenace_request: + raise UserError("CProperty cannot be sold , there is any maintenance request not done") + self.stage = 'sold' + + @api.constrains('expected_price') + def _check_expected_price(self): + for rec in self: + if rec.expected_price < 0: + raise ValidationError( + 'The selling price cannot be lower than 90% of the expected price.' + ) diff --git a/real_estate/models/real_estate_property_maintenance_request.py b/real_estate/models/real_estate_property_maintenance_request.py new file mode 100644 index 00000000000..88263461747 --- /dev/null +++ b/real_estate/models/real_estate_property_maintenance_request.py @@ -0,0 +1,28 @@ +from odoo.exceptions import UserError + +from odoo import fields, models, api + + +class real_estate_properties_maintenance_request(models.Model): + _name = 'real.estate.property.maintenance.request' + _description = 'Real Estate Property Maintenance Request' + + name = fields.Char() + cost = fields.Integer() + status = fields.Selection([ + ('new', 'New'), + ('approved', 'Approved'), + ('done', 'Done'), + ], string="Status", copy=False, default='new') + property_id = fields.Many2one('real.estate', string='Property', ondelete='restrict') + + @api.onchange('status') + def _check_cost_on_accepted_status(self): + if self.status == 'approved' and self.cost <= 0: + raise UserError("Approved cost must be greater than 0") + + @api.ondelete(at_uninstall=False) + def _unlink_if_maintenance_request_not_done(self): + maintenace_request = self.filtered_domain([('status', '!=', 'done')]) + if maintenace_request: + raise UserError("Can't delete an active Maintenance Request Record!") diff --git a/real_estate/models/real_estate_property_offer.py b/real_estate/models/real_estate_property_offer.py new file mode 100644 index 00000000000..2db29d468f2 --- /dev/null +++ b/real_estate/models/real_estate_property_offer.py @@ -0,0 +1,149 @@ +from datetime import timedelta +# from collections import defaultdict + +from odoo import models, fields, api +from odoo.exceptions import UserError +from odoo.tools import float_compare + + +class real_estate_property_offer(models.Model): + _name = 'real.estate.property.offer' + _description = 'Real Estate Property Offer' + _order = "price desc" + + price = fields.Float(required=True) + property_id = fields.Many2one('real.estate', string='Property', ondelete='restrict') + property_type_id = fields.Many2one( + related="property_id.property_type_id", + store=True + ) + status = fields.Selection([ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ], string="Status", copy=False) + partner_id = fields.Many2one('res.partner', string="Buyer", required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + store=True + ) + _check_offer_price_positive = models.Constraint( + 'CHECK(price > 0)', + 'The offer price must be strictly positive.', + ) + + # @api.model + # def create(self, vals): + # grouped = defaultdict(list) + # for val in vals: + # property_id = val.get('property_id') + # price = val.get('price') + # if property_id and price: + # grouped[property_id].append(price) + # + # for property_id, new_prices in grouped.items(): + # property_rec = self.env['real.estate'].browse(property_id) + # existing_prices = property_rec.best_price + # new_max = max(new_prices) + # max_price = max(existing_prices, new_max) + # for price in new_prices: + # if price < max_price: + # raise UserError( + # "Only the highest offer is allowed" + # ) + # offers = super().create(vals) + # for offer in offers: + # if offer.property_id and offer.property_id.stage == 'new': + # offer.property_id.stage = 'offer_received' + # return offers + + @api.model + def create(self, vals): + for val in vals: + price = val.get('price') + property_id = val.get('property_id') + property = self.env['real.estate'].browse(property_id) + if property.stage == "new": + property.best_price = price + elif float_compare(price, property.best_price, precision_rounding=0.01) < 0: + raise UserError( + f"Price should be greater than {property.best_price}") + else: + property.best_price = price + if property and property.stage == 'new': + property.stage = 'offer_received' + + return super().create(vals) + + # @api.model + # def create(self, vals): + # for property_id, n + # for val in vals: + # property_id = val.get('property_id') + # price = val.get('price') + # if property_id and price: + # property_rec = self.env['real.estate'].browse(property_id) + # if property_rec.offer_ids: + # # max_offer = max(property_rec.offer_ids.mapped('price')) + # if price < property_rec.best_price: + # raise UserError( + # "The offer must be higher than existing offers." + # ) + # offer = super().create(vals) + # if offer.property_id and offer.property_id.stage == 'new': + # offer.property_id.stage = 'offer_received' + # return offer + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for offer in self: + if offer.create_date: + offer.date_deadline = offer.create_date.date() + timedelta(days=offer.validity) + else: + offer.date_deadline = fields.Date.today() + timedelta(days=offer.validity) + + def _inverse_date_deadline(self): + for offer in self: + if offer.create_date and offer.date_deadline: + offer.validity = (offer.date_deadline - offer.create_date.date()).days + + @api.ondelete(at_uninstall=False) + def _unlink_if_accepted_offer(self): + accepted_offer = self.filtered_domain([('status', '=', 'accepted')]) + if accepted_offer: + raise UserError("Can't delete an active record!") + + def action_accept(self): + # accepted_offer = self.search([ + # ('property_id', '=', self.property_id.id), + # ('status', '=', 'accepted') + # ], limit=1) + # accepted_offer = self.property_id.offer_ids.filtered( + # lambda o: o.status == 'accepted' + # ) + accepted_offer = self.property_id.offer_ids.filtered_domain([ + ('status', '=', 'accepted') + ]) + maintenace_request = self.property_id.maintenance_request_ids.filtered_domain([('status', '!=', 'done')]) + if accepted_offer: + raise UserError( + "Only one offer can be accepted for a property.") + if maintenace_request: + raise UserError("Property cannot be sold , there is any maintenance request not done") + self.status = 'accepted' + self.property_id.write({ + 'selling_price': self.price, + 'stage': 'offer_accepted', + 'buyer_id': self.partner_id.id, + }) + refused_offer = self.property_id.offer_ids.filtered_domain([ + ('id', '!=', 'self.id'), + ('status', '!=', 'accepted') + ]) + for refuse in refused_offer: + refuse.status = 'refused' + + def action_refuse(self): + self.status = 'refused' diff --git a/real_estate/models/real_estate_property_type.py b/real_estate/models/real_estate_property_type.py new file mode 100644 index 00000000000..bac5ceccd8f --- /dev/null +++ b/real_estate/models/real_estate_property_type.py @@ -0,0 +1,31 @@ +from odoo import fields, models + + +class RealEstateTag(models.Model): + _name = 'real.estate.property.type' + _description = 'Real Estate Property Type' + _order = "sequence, name desc" + + name = fields.Char(required=True) + sequence = fields.Integer(string="Sequence", default=10) + property_ids = fields.One2many( + 'real.estate', + 'property_type_id', + string='Properties' + ) + offer_ids = fields.One2many( + "real.estate.property.offer", + "property_type_id", + string="Offers" + ) + _unique_type_name = models.Constraint( + 'UNIQUE(name)', + 'The property type name must be unique.', + ) + offer_count = fields.Integer( + compute="_compute_offer_count" + ) + + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/real_estate/models/real_estate_tag.py b/real_estate/models/real_estate_tag.py new file mode 100644 index 00000000000..60f9a5c06b0 --- /dev/null +++ b/real_estate/models/real_estate_tag.py @@ -0,0 +1,19 @@ +from random import randint + +from odoo import fields, models + + +class RealEstateTag(models.Model): + _name = 'real.estate.tag' + _description = 'Real Estate Tag' + _order = "name desc" + + def _get_default_color(self): + return randint(1, 11) + + name = fields.Char(required=True) + color = fields.Integer(default=_get_default_color) + _unique_tag_name = models.Constraint( + 'UNIQUE(name)', + 'The property tag name must be unique.', + ) diff --git a/real_estate/models/res_users.py b/real_estate/models/res_users.py new file mode 100644 index 00000000000..572dd1ecac3 --- /dev/null +++ b/real_estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'real.estate', + 'salesperson_id', + string='Assigned Properties' + ) diff --git a/real_estate/security/ir.model.access.csv b/real_estate/security/ir.model.access.csv new file mode 100644 index 00000000000..970d60f19f0 --- /dev/null +++ b/real_estate/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +real_estate,real_estate,model_real_estate,base.group_user,1,1,1,1 +real_estate_tag,real_estate_tag,model_real_estate_tag,base.group_user,1,1,1,1 +real_estate_property_offer,real_estate_property_offer,model_real_estate_property_offer,base.group_user,1,1,1,1 +real_estate_property_type,real_estate_property_type,model_real_estate_property_type,base.group_user,1,1,1,1 +real_estate_property_maintenance_request,real_estate_property_maintenance_request,model_real_estate_property_maintenance_request,base.group_user,1,1,1,1 +,,,,,,, \ No newline at end of file diff --git a/real_estate/views/real_estate_menus.xml b/real_estate/views/real_estate_menus.xml new file mode 100644 index 00000000000..f35786b7999 --- /dev/null +++ b/real_estate/views/real_estate_menus.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + diff --git a/real_estate/views/real_estate_property_maintenance_request_views.xml b/real_estate/views/real_estate_property_maintenance_request_views.xml new file mode 100644 index 00000000000..8f82d5f7f77 --- /dev/null +++ b/real_estate/views/real_estate_property_maintenance_request_views.xml @@ -0,0 +1,43 @@ + + + + real.estate.property.maintenance.request.view.tree + real.estate.property.maintenance.request + + + + + + + + + + + + real.estate.property.maintenance.request.view.form + real.estate.property.maintenance.request + +
+
+ +
+ + + + + + + +
+
+
+ + + Maintenance request + real.estate.property.maintenance.request + list,form + + +
diff --git a/real_estate/views/real_estate_property_offer_views.xml b/real_estate/views/real_estate_property_offer_views.xml new file mode 100644 index 00000000000..4a69e6bd2a2 --- /dev/null +++ b/real_estate/views/real_estate_property_offer_views.xml @@ -0,0 +1,65 @@ + + + + real.estate.property.offer.view.tree + real.estate.property.offer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Property Type + real.estate.property.type + list,form + + + diff --git a/real_estate/views/real_estate_property_views.xml b/real_estate/views/real_estate_property_views.xml new file mode 100644 index 00000000000..733a63704d3 --- /dev/null +++ b/real_estate/views/real_estate_property_views.xml @@ -0,0 +1,167 @@ + + + + real.estate.view.list + real.estate + + + + + + + + + + + + + + + + real.estate.view.form + real.estate + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + real.estate.view.search + real.estate + + + + + + + + + + + + + + + + + + + Real Estate + real.estate + list,form + + { + 'search_default_active_record': 1, + } + + + +
diff --git a/real_estate/views/real_estate_tag_views.xml b/real_estate/views/real_estate_tag_views.xml new file mode 100644 index 00000000000..aab44a3ce64 --- /dev/null +++ b/real_estate/views/real_estate_tag_views.xml @@ -0,0 +1,35 @@ + + + + real.estate.tag.view.tree + real.estate.tag + + + + + + + + + + real.estate.tag.view.form + real.estate.tag + +
+ + + + + + +
+
+
+ + + Property Tag + real.estate.tag + list,form + + +
diff --git a/real_estate/views/res_users_views.xml b/real_estate/views/res_users_views.xml new file mode 100644 index 00000000000..56364a561be --- /dev/null +++ b/real_estate/views/res_users_views.xml @@ -0,0 +1,33 @@ + + + + res.users.view.form.inherit.real.estate + res.users + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/real_estate_account/__init__.py b/real_estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/real_estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/real_estate_account/__manifest__.py b/real_estate_account/__manifest__.py new file mode 100644 index 00000000000..8721133bb59 --- /dev/null +++ b/real_estate_account/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': "real.estate.account", + 'summary': "summary of the real estate account", + 'description': """Link between Estate and Accounting""", + 'author': "Ruchita Gothi (Rugot)", + 'category': 'Real Estate', + 'version': '0.1', + 'depends': ['real_estate', 'account'], + 'data': [ + # 'security/ir.model.access.csv', + ], + 'application': True, + 'installable': True, + 'license': 'LGPL-3', +} diff --git a/real_estate_account/models/__init__.py b/real_estate_account/models/__init__.py new file mode 100644 index 00000000000..c97e681daec --- /dev/null +++ b/real_estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import real_estate_property diff --git a/real_estate_account/models/real_estate_property.py b/real_estate_account/models/real_estate_property.py new file mode 100644 index 00000000000..e37f7e244cb --- /dev/null +++ b/real_estate_account/models/real_estate_property.py @@ -0,0 +1,26 @@ +from odoo import models + + +class real_estate(models.Model): + _inherit = 'real.estate' + + def action_sold(self): + res = super().action_sold() + for property in self: + self.env['account.move'].create({ + 'partner_id': property.buyer_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': [ + ({ + 'name': '6% of selling price', + 'quantity': 1, + 'price_unit': property.selling_price * 0.06, + }), + ({ + 'name': 'Administrative fees', + 'quantity': 1, + 'price_unit': 100.0, + }), + ], + }) + return res diff --git a/real_estate_account/security/ir.model.access.csv b/real_estate_account/security/ir.model.access.csv new file mode 100644 index 00000000000..9581ebac755 --- /dev/null +++ b/real_estate_account/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +,,,,,,, \ No newline at end of file