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..59919aa8811 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "estate", + "depends": ["base"], + "category": "tutorials", + "installable": True, + "application": True, + "data": [ + "security/ir.model.access.csv", + "views/estate_property_view.xml", + "views/estate_property_offer_view.xml", + "views/estate_property_type_view.xml", + "views/estate_property_tag_view.xml", + "views/estate_menu.xml", + "views/user_res_view.xml", + ], + "author": "RADHR", + "license": "LGPL-3", +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..c0917a3d550 --- /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 inherited_model diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..952fa96a4d9 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,133 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError, ValidationError + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + date_availability = fields.Date( + string="Available From", + copy=False, + default=lambda self: fields.Date.add(fields.Date.today(), months=3), + ) + postcode = fields.Char(required=True) + expected_price = fields.Float() + selling_price = fields.Float(readonly=True) + 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( + selection=[ + ("north", "North"), + ("east", "East"), + ("west", "West"), + ("south", "South"), + ], + ) + active = fields.Boolean("Active", default=True) + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("accepted", "Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + default="new", + copy=False, + ) + property_type_id = fields.Many2one("estate.property.type", string="Property type") + buyer_id = fields.Many2one( + comodel_name="res.partner", + string="Buyer", + copy=False, + default=lambda self: self.env.user.partner_id, + ) + sales_person = fields.Many2one( + comodel_name="res.users", + string="Sales person", + index=True, + default=lambda self: self.env.user, + ) + property_tag = fields.Many2many( + comodel_name="estate.property.tag", + ) + offer_ids = fields.One2many( + comodel_name="estate.property.offer", + inverse_name="property_id", + ) + total_area = fields.Integer( + string="total_area", name="Total area", compute="_compute_total" + ) + best_price = fields.Integer( + compute="_compute_best_price", + store=True, + ) + + @api.depends("garden_area", "living_area") + def _compute_total(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + + for record in self: + prices = record.mapped("offer_ids.price") + record.best_price = max(prices) if prices 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 = False + + @api.ondelete(at_uninstall=False) + def _ondelete_property(self): + for record in self: + if record.state != "new" or record.state != "cancelled": + raise UserError( + "You can't delete the record which is not new or cancelled" + ) + + def action_cancel_offer(self): + for record in self: + if record.state == "sold": + raise UserError("Saved properties can't be cancelled") + else: + record.state = "cancelled" + + def action_sold_offer(self): + for record in self: + if record.state == "cancelled": + raise UserError("Cancelled properties can't be saved") + else: + record.state = "sold" + + def action_approve_best(self): + self.offer_ids[0].status = "accepted" + + _check_expected_price = models.Constraint( + "CHECK(expected_price > 0 and selling_price > 0)", + "Expected price and selling price must be positive", + ) + + @api.constrains("selling_price", "expected_price") + def _check_selling_expected_price(self): + for record in self: + base = record.expected_price * 0.9 + # value = str(base) + if record.selling_price and record.selling_price < base: + raise ValidationError( + f"Selling Price must be greater than 90 percent of expected price and atleast {base}" + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..ae2cd385ca6 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,80 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + _order = "price desc" + + name = fields.Char(string="Property Offer", required=True) + price = fields.Integer(string="Price", required=True) + status = fields.Selection( + string="Status", + selection=[("accepted", "Accepted"), ("refused", "Refused")], + 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(string="Validity", default=7) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_deadline", + inverse="_inverse_deadline", + readonly=False, + ) + property_type_id = fields.Many2one( + related="property_id.property_type_id", store=True + ) + + @api.model + def create(self, vals): + for val in vals: + property = self.env["estate.property"].browse(val["property_id"]) + if property.best_price > val.get("price", 0): + raise UserError("Better offer than this already exist") + property.state = "offer_received" + return super().create(vals) + + # It gets changed on each changes because it works based on cache + @api.depends("create_date", "validity") + def _compute_deadline(self): + for records in self: + default_date = ( + records.create_date.date() + if records.create_date + else fields.Date.today() + ) + records.date_deadline = fields.Date.add(default_date, days=records.validity) + + # Inverse is triggered when the computed field is written (usually during save),not during live editing. + def _inverse_deadline(self): + for records in self: + default_date = ( + records.create_date.date() + if records.create_date + else fields.Date.today() + ) + if records.date_deadline: + records.validity = (records.date_deadline - default_date).days + + def save_offer(self): + for record in self: + if record.status == "accepted": + raise UserError("Property is already accepted") + record.status = "accepted" + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + record.property_id.state = "accepted" + + def cancel_offer(self): + for record in self: + if record.status != "accepted" or record.status != "refused": + record.status = "refused" + if record.property_id.buyer_id == record.partner_id: + record.property_id.selling_price = False + record.property_id.state = False + + _check_price = models.Constraint( + "CHECK(price >= 0)", "Price filled must be positive" + ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..1933d52cf29 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,14 @@ +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) + color = fields.Integer(string="color") + + _check_name = models.Constraint( + "UNIQUE (name)", "Please give different tag as it is taken" + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..0f6d38d2b48 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,26 @@ +from odoo import fields, models, api + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + _order = "name" + + name = fields.Char(string="name", required=True) + property_ids = fields.One2many( + comodel_name="estate.property", + inverse_name="property_type_id", + string="Property Type", + ) + offer_ids = fields.One2many( + "estate.property.offer", inverse_name="property_type_id" + ) + offer_count = fields.Integer(string="Total Offer", compute="_compute_offer_count") + sequence = fields.Integer("Sequence", default=1) + + _name_check = models.Constraint("UNIQUE (name)", "Please add unique type") + + @api.depends("offer_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/inherited_model.py b/estate/models/inherited_model.py new file mode 100644 index 00000000000..ea446c6a7f1 --- /dev/null +++ b/estate/models/inherited_model.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", inverse_name="sales_person") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..ca940030189 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,acess_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 diff --git a/estate/views/estate_menu.xml b/estate/views/estate_menu.xml new file mode 100644 index 00000000000..c5644ec314e --- /dev/null +++ b/estate/views/estate_menu.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml new file mode 100644 index 00000000000..90e89ade8f7 --- /dev/null +++ b/estate/views/estate_property_offer_view.xml @@ -0,0 +1,39 @@ + + + Property Offers + estate.property.offer + list,form + + + estate.property.offer.list.view + estate.property.offer + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_view.xml b/estate/views/estate_property_view.xml new file mode 100644 index 00000000000..02be1453ab1 --- /dev/null +++ b/estate/views/estate_property_view.xml @@ -0,0 +1,138 @@ + + + estate.property.list.view + estate.property + + + + + + + + + + + + + + estate.property.form.view + estate.property + +
+
+
+ +
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + estate.property.search.view + estate.property + + + + + + + + + + + + + + + + estate.property.kanban.view + estate.property + + + + + +
+

+ +

+
+

+ Expected Price: + +

+
+
+

+ Best Price: + +

+
+
+

+ Selling Price: + +

+
+
+ +
+
+
+
+
+
+
+ + Properties + estate.property + list,form,kanban + {'search_default_available': True} + +
diff --git a/estate/views/user_res_view.xml b/estate/views/user_res_view.xml new file mode 100644 index 00000000000..34796b71eba --- /dev/null +++ b/estate/views/user_res_view.xml @@ -0,0 +1,14 @@ + + + res.users.view.form.inherit.estate.property + res.users + + + + + + + + + + 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..56e4469422f --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,7 @@ +{ + "name": "estate account", + "depends": ["account", "estate"], + "installable": True, + "author": "RADHR", + "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..da0ee9e5ee8 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,31 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold_offer(self): + + self.env["account.move"].create( + { + "partner_id": self.buyer_id.id, + "move_type": "out_invoice", + "line_ids": [ + Command.create( + { + "name": "Amount", + "quantity": 1, + "price_unit": self.selling_price * 0.06, + } + ), + Command.create( + { + "name": "Administration Fess", + "quantity": 1, + "price_unit": 100, + } + ), + ], + } + ) + return super().action_sold_offer()