diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..ee541a17374 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,2 @@ +from . import models +from odoo import api, SUPERUSER_ID diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..d065888fe19 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,25 @@ +{ + 'name':"estate", + 'author':"Odoo S.A.", + 'licence':"none", + + 'summary': """ + Houses! + """, + + 'description': """ + Lots of Houses!!" + """, + + 'application': True, + 'installable': True, + 'data':[ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_types_views.xml', + 'views/estate_property_tags_views.xml', + 'views/estate_property_offers_views.xml', + 'views/estate_menus.xml', + 'views/estate_users.xml', + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..407879d58ce --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import est_property +from . import est_property_offers +from . import est_property_tags +from . import est_property_types +from . import inherited_model diff --git a/estate/models/est_property.py b/estate/models/est_property.py new file mode 100644 index 00000000000..acaa0345300 --- /dev/null +++ b/estate/models/est_property.py @@ -0,0 +1,110 @@ +from datetime import timedelta +from odoo import models, fields, api +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare, float_is_zero + + +class EstateProperty(models.Model): + _name = "est.property" + _description = "Real Estate" + _order = "id desc" + + _check_expected_price = models.Constraint( + 'CHECK (expected_price >= 0)', + 'The Expected Price should be positive!', + ) + + _check_selling_price = models.Constraint( + 'CHECK (selling_price >= 0)', + 'The Selling Price should be positive!', + ) + def _today_plus_90days(self): + return fields.Date.today(self) + timedelta(days=90) + + + name = fields.Char('Property Name', required=True) + description = fields.Text(required=True) + post_code = fields.Char(required=True) + date_availability = fields.Date(default=(_today_plus_90days)) #add 90 days which is about 3 months + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer(default=2) + living_area = fields.Integer("Living Area (sqm)") + 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("Active",default=True) + state = fields.Selection( + selection = [("new","New"),("offer_received","Offer Received"),("offer_accepted","Offer Accepted"),("sold","Sold"),("cancelled","Cancelled")], + default = "new" + ) + partner_id = fields.Many2one("res.partner", string="Buyer") + user_id = fields.Many2one("res.users", string="Salesman") + property_type_id = fields.Many2one("est.property.type", string="Property Type") + + tag_ids = fields.Many2many("est.property.tag",string="Tags") + + offers_ids = fields.One2many("est.property.offer","property_id",string="Offers") + + total_area = fields.Integer(compute="_compute_total", readonly=True) + max_offer = fields.Integer(compute="_compute_best_offer", readonly=True) + + @api.constrains("selling_price") + def _check_selling_price(self): + for property in self: + ## if different than 0 and selling_price < 90% of expected_price + if (not float_is_zero(self.selling_price,precision_digits=3) and (1 == float_compare(self.expected_price * .9, self.selling_price, precision_digits=3))): + raise ValidationError(r"Selling price can't be less than 90% expected price") + + + @api.depends("living_area","garden_area") + def _compute_total(self): + for property in self: + property.total_area = property.living_area + property.garden_area + + @api.depends("offers_ids") + def _compute_best_offer(self): + + for property in self: + + property.max_offer = max(property.offers_ids.mapped("price"),default=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 = None + + @api.ondelete(at_uninstall=False) + def _ondelete(self): + for property in self: + if not(property.state == "new" or property.state == "cancelled"): + raise UserError("You can only delete properties that are 'New' or 'Cancelled'.") + + + # ------------- Actions ------------------------------- + + def sell_action(self): + for property in self: + if self.state == "cancelled": + #Raise error + raise UserError("Can't change to Sold if current state is Cancelled!") + else: + self.state = "sold" + + def cancel_action(self): + for property in self: + if self.state =="sold": + #Raise error + raise UserError("Can't change to Cancel if current state is Sold!") + else: + self.state = "cancelled" + \ No newline at end of file diff --git a/estate/models/est_property_offers.py b/estate/models/est_property_offers.py new file mode 100644 index 00000000000..6f7dcc2cbe2 --- /dev/null +++ b/estate/models/est_property_offers.py @@ -0,0 +1,82 @@ +from datetime import timedelta +from odoo import models, fields, api +from odoo.exceptions import UserError + + +class EstateOffer(models.Model): + _name = "est.property.offer" + _description = "Property Offers" + _order = "price desc" + + _check_price = models.Constraint( + 'CHECK (price >= 0)', + 'The Price should be positive!', + ) + + + name = fields.Char("name") + price = fields.Integer("price",required=True) + status = fields.Selection( + string="status", + default="waiting", + selection = [ + ("accepted","Accepted"), + ("refused","Refused"), + ("waiting","Waiting"), + ], + ) + partner_id = fields.Many2one("res.partner") + property_id = fields.Many2one("est.property",required=True) + + property_type_id = fields.Many2one("est.property.type", related="property_id.property_type_id", stored=True, compute="_compute_property_type_id") + + + validity_date = fields.Date(compute="_compute_validity_date",inverse="_inverse_validity_duration") + validity_duration = fields.Integer(default=7) + + @api.depends("validity_duration") + def _compute_validity_date(self): + for offer in self: + offer.validity_date = fields.Date.today() + timedelta(days=offer.validity_duration) + + @api.depends("property_id") + def _compute_property_type_id(self): + for offer in self: + if offer.property_id.property_type_id: + offer.property_type_id = offer.property_id.property_type_id + else: + offer.property_type_id = False + + def _inverse_validity_duration(self): + for offer in self: + offer.validity_duration = (offer.validity_date - fields.Date.today()).days + + def action_confirm(self): + for offer in self: + if offer.property_id.state == "sold": + raise UserError(self.env._("Property was already sold!")) + elif offer.property_id.state == "cancelled": + raise UserError(self.env._("Property was already cancelled!")) + elif offer.status == "refused": + raise UserError(self.env._("Cannot accept a proposal that was already refused!")) + else: + offer.status = "accepted" + offer.property_id.selling_price = offer.price + offer.property_id.partner_id = offer.partner_id + offer.property_id.state = "sold" + + def action_reject(self): + self.status = "refused" + + @api.model + def create(self,vals_list): + for vals in vals_list: + property = self.env['est.property'].browse(vals.get('property_id')) + + if property.state == 'new': + property.state = 'offer_received' + + if property.max_offer > vals.get('price',0): + raise UserError(self.env._("Cannot create an offer with a lower value than a previous one!")) + + return super().create(vals_list) diff --git a/estate/models/est_property_tags.py b/estate/models/est_property_tags.py new file mode 100644 index 00000000000..a3f37423725 --- /dev/null +++ b/estate/models/est_property_tags.py @@ -0,0 +1,14 @@ +from odoo import models, fields + +class EstateTag(models.Model): + _name = "est.property.tag" + _description = "Property tags" + _order = "name asc" + + _check_name = models.Constraint( + 'unique(name)', + 'There is already a property tag with that name!', + ) + + name = fields.Char("name", required=True) + color = fields.Integer("color") diff --git a/estate/models/est_property_types.py b/estate/models/est_property_types.py new file mode 100644 index 00000000000..f05407ba19e --- /dev/null +++ b/estate/models/est_property_types.py @@ -0,0 +1,23 @@ +from odoo import models, fields, api + +class EstateType(models.Model): + _name = "est.property.type" + _description = "Property types" + _order = "sequence,name" + + _check_name = models.Constraint( + 'unique(name)', + 'There is already a property type with that name!', + ) + + name = fields.Char("name", required=True) + sequence = fields.Integer(default=1) + + + offer_ids = fields.One2many("est.property.offer","property_type_id") + offer_count = fields.Integer(compute="_compute_count") + + @api.depends("offer_ids") + def _compute_count(self): + for type in self: + type.offer_count = len(type.offer_ids) diff --git a/estate/models/inherited_model.py b/estate/models/inherited_model.py new file mode 100644 index 00000000000..27b89fea716 --- /dev/null +++ b/estate/models/inherited_model.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class User(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("est.property","user_id",string="Properties") \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..5441b5b72c2 --- /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_model,access_estate_model,model_est_property,base.group_user,1,1,1,1 +access_type,access_type,model_est_property_type,base.group_user,1,1,1,1 +access_tag,access_tag,model_est_property_tag,base.group_user,1,1,1,1 +access_offer,access_offer,model_est_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..95d9bc9b513 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 00000000000..f58546ddf67 --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,25 @@ + + + + + est_property_offer_view_list + est.property.offer + + + + + + + + + + + + + + Property offers + est.property.offer + list,form + + + diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml new file mode 100644 index 00000000000..b450455bbc6 --- /dev/null +++ b/estate/views/estate_property_tags_views.xml @@ -0,0 +1,10 @@ + + + + + Property tags + est.property.tag + list,form + + + diff --git a/estate/views/estate_property_types_views.xml b/estate/views/estate_property_types_views.xml new file mode 100644 index 00000000000..d97bac6db6a --- /dev/null +++ b/estate/views/estate_property_types_views.xml @@ -0,0 +1,58 @@ + + + + + + est.property.type.list + est.property.type + + + + + + + + + + + + + + est.property.offer + Property Types + list + [('property_type', '=', active_id)] + + + + + est.property.type.form + est.property.type + +
+
+ +
+ + + + + +
+
+
+ + + + Property types + est.property.type + list,form + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..b4c37ec2767 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,221 @@ + + + + + + est_test_view_list + est.property + + + + + + + + + + + + + + + + + + + + + est_test_view_kanban + est.property + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + +
+
+
+ +
+
+ + + est_test_view_form + est.property + +
+ +
+ +
+ + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +