diff --git a/estate/__manifest__.py b/estate/__manifest__.py index d7cfa57d4c5..c2b55595c9e 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -4,7 +4,17 @@ "application": True, # This line says the module is an App, and not a module "depends": ["base"], # dependencies "data": [ - + 'security/ir.model.access.csv', + # estate property + 'views/estate_property_views.xml', + # estate property offer + 'views/estate_property_offer_views.xml', + # estate property type + 'views/estate_property_type_views.xml', + # estate property tag + 'views/estate_property_tag_views.xml', + 'views/res_users.xml', + 'views/estate_menus.xml', ], "installable": True, 'license': 'LGPL-3', diff --git a/estate/models.py b/estate/models.py deleted file mode 100644 index e69de29bb2d..00000000000 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..8f18620392c --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,151 @@ +from datetime import datetime, timedelta +from odoo import fields, models, Command +from odoo import api +from odoo import exceptions +from odoo.exceptions import ValidationError +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare, float_is_zero + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = "ESTATE Properties" + _order = "id desc" + + name = fields.Char(string="Title", required=True, help="Property name") + description = fields.Text(string="Description", help="Property description") + postcode = fields.Text(string="Postcode", help="Property Post code") + date_availability = fields.Date(string="Available From", + default=lambda self: (datetime.now() + timedelta(days=90)).strftime('%Y-%m-%d'), + copy=False, help="Property Date Availability") + + expected_price = fields.Float(string="Expected Price", required=True, help="Property Expected Price") + selling_price = fields.Float(string="Selling Price", readonly=True, copy=False, help="Property Selling Price") + + bedrooms = fields.Integer(string="Bedrooms", default=2, help="Property Bedrooms") + living_area = fields.Integer(string="Living Area (sqm)", help="Property Living Area") + facades = fields.Integer(string="Facades", help="Property Facades") + garage = fields.Boolean(string="Garage", help="Property Garage") + garden = fields.Boolean(string="Garden", help="Property Garden") + garden_area = fields.Integer(string="Garden Area", help="Property Garden Area") + garden_orientation = fields.Selection( + string="Orientation", + selection=[("North", "North"), ("South", "South"), ("East", "East"), ("West", "West")], + help="Orientation help to define the garden orientation") + + active = fields.Boolean(string="Active", default=True, help="Property is available") + + state = fields.Selection( + string="State", + selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), ("canceled", "Canceled")], + default="new", + help="State of the property advertisement") + + # links + property_type_id = fields.Many2one('estate.property.type', domain="[('id', '!=', False)]", string="Type") + tag_ids = fields.Many2many('estate.property.tag', string="Tag") + + salesperson_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user, + help="Salesperson") + buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False, help="Buyer Person") + + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers', help='List of offers') + + # campos computados + total_area = fields.Integer(string="Total Area (sqm)", compute='_compute_total_area') + + best_price = fields.Float(string="Best Offer", compute='_compute_best_offer') + + # campo compañia si se tienen 2 o más + company_id = fields.Many2one('res.company', string="Company", required=True, default=lambda self: self.env.company, + help="Company related to this property") + + # constraints + + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', 'A property expected price must be strictly positive'), + ('check_selling_price', 'CHECK(selling_price >= 0)', 'A property selling price must be positive'), + ] + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + """ + Compute and update total area. + """ + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends('offer_ids.price') + def _compute_best_offer(self): + """ + Compute and update best offer + """ + for record in self: + try: + record.best_price = max(record.offer_ids.mapped('price')) + except ValueError: + record.best_price = None + + @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_sold_property(self): + for record in self: + if record.state == 'canceled': + # cancelard propiedad + raise exceptions.UserError("canceled property cannot be sold") + elif record.state != 'offer_accepted': + # Ya se vendio joven + raise exceptions.UserError("You must accept an offer before make a property as sold") + else: + record.state = 'sold' + + return True + + def action_prueba(self): + for record in self: + if record.state == 'canceled': + # cancelada + raise exceptions.UserError("canceled property cannot be sold") + elif record.state != 'offer_accepted': + # not se acepta ofeertas + raise exceptions.UserError("You must accept an offer before make a property as sold") + else: + record.state = 'sold' + + return True + + def action_cancel_property(self): + for record in self: + if record.state == 'sold': + # venida + raise exceptions.UserError("sold property cannot be canceled") + else: + record.state = 'canceled' + return True + + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for property_record in self: + if not float_is_zero(property_record.selling_price, precision_digits=1): + if float_compare(property_record.selling_price, 0.9 * property_record.expected_price, + precision_digits=2) == -1: + raise ValidationError("Selling price cannot be lower than 90% of the expected price!") + else: + pass + else: + pass + + @api.ondelete(at_uninstall=False) + def _check_property_state(self): + for property_record in self: + if property_record.state not in ['new', 'canceled']: + raise UserError("Only New or Canceled property can be deleted!") + else: + pass diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..9269bd15b00 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,82 @@ +from datetime import datetime, timedelta +from datetime import date +from odoo import fields, models +from odoo import api +from odoo import exceptions +from odoo.exceptions import ValidationError + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = "Properties Offer" + _order = "price desc" + + price = fields.Float(string="Price", help="Buyer Offer") + validity = fields.Integer(string="validity (days)", default=7, help="validity") + date_deadline = fields.Date(string="Deadline", compute='_compute_date_deadline', inverse='_inverse_date_deadline', help='Validity date') + + status = fields.Selection( + string="Status", + selection=[("offer_accepted", "Accept"), ("offer_refused", "Refuse"),], + copy=False, + help="State of the property advertisement") + + partner_id = fields.Many2one('res.partner', string="Partner", required=True, help="Person who make the offer") + property_id = fields.Many2one('estate.property', string="Property", required=True, help="Property") + + property_type_id = fields.Many2one(related='property_id.property_type_id', store=True) + + # constraints + _sql_constraints = [ + ('check_price', 'CHECK(price >= 0)', 'An offer price must be strictly positive'), + ] + + + @api.depends('validity') + def _compute_date_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = record.create_date + timedelta(days=record.validity) + else: + record.date_deadline = date.today() + timedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + if record.create_date: + record.validity = (record.date_deadline - record.create_date.date()).days + else: + record.validity = record.date_deadline - datetime.now().date() + + def action_accept_offer(self): + for record in self: + # Reject all others offers + for offer in record.property_id.offer_ids: + offer.status = 'offer_refused' + + # acceptado + record.status = 'offer_accepted' + + # Actualizar campos de precios + record.property_id.state = record.status + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + + + + return True + + def action_refuse_offer(self): + for record in self: + record.status = 'Refused' + + return True + + @api.model + def create(self, vals): + # Check precio mas bajo que el permitido + existing_offers = self.env['estate.property.offer'].search([('property_id', '=', vals['property_id']), ('price', '>=', vals['price'])]) + if existing_offers: + raise ValidationError("The offer price cannot be lower than an existing offer.") + + # Oferta recibida al crear la oferta. + self.env['estate.property'].browse(vals['property_id']).write({'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..5ba7ac0fdec --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,15 @@ +from datetime import datetime, timedelta +from odoo import fields, models + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = "Properties Tag" + _order = "name" + + name = fields.Char(string="Name", required=True, help="Tag name") + color = fields.Integer(string="Color", help="Tag color") + + # constraints + _sql_constraints = [ + ('unique_property_tag_name', 'UNIQUE(name)', 'A property cannot have duplicate tags.'), + ] \ 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..a549e48a884 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta +from odoo import fields, models +from odoo import api + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = "Properties type" + _order = "name" + + name = fields.Char(string="Name", required=True, help="Property type") + + property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties', help='Type properties') + + sequence = fields.Integer('Sequence', default=1, help="Used to order stages. Lower is better.") + + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers") + + offer_count = fields.Integer(string="Offers Count", default=0, compute="_compute_offer_count") + + + # constraints + _sql_constraints = [ + ('unique_property_type_name', 'UNIQUE(name)', 'A property cannot have duplicate types.'), + ] + + @api.depends('offer_ids') + def _compute_offer_count(self): + for property_type_record in self: + property_type_record.offer_count = len(property_type_record.offer_ids) + + def get_offer_count(self): + # Recuperar valor + return self.offer_count \ No newline at end of file diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..0b8b5b56843 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from datetime import datetime, timedelta +from datetime import date +from odoo import fields, models +from odoo import api +from odoo import exceptions +from odoo.exceptions import ValidationError + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many('estate.property', 'salesperson_id', string='Properties', domain="[('active', '=', True), '|', ('state', '=', 'new'), ('state', '=', 'offer_received')]", help='List of properties') diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..38004a1e97e --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,12 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +1,Real Estate Base Access,model_estate_property,base.group_user,1,0,0,0 +2,Real Estate Base Access,model_estate_property_type,base.group_user,1,0,0,0 +3,Real Estate Base Access,model_estate_property_tag,base.group_user,1,0,0,0 +4,Real Estate Base Access,model_estate_property_offer,base.group_user,1,1,1,1 +5,Real Estate Manager Access,model_estate_property,base.group_system,1,1,1,1 +6,Real Estate Manager Access,model_estate_property_type,base.group_system,1,1,1,1 +7,Real Estate Manager Access,model_estate_property_tag,base.group_system,1,1,1,1 +8,Agent Access for Properties,model_estate_property,base.group_system,1,1,1,0 +9,Agent Access for Types and Tags,model_estate_property_type,base.group_system,1,0,0,0 +10,Agent Access for Tags,model_estate_property_tag,base.group_system,1,0,0,0 +11,Prevent Delete for Properties,model_estate_property,,0,0,0,0 \ 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..1f5afbc9391 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..0e76e68d8af --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,19 @@ + + + + + estate.property.offer.tree + estate.property.offer + + + + + + +