diff --git a/.gitignore b/.gitignore index b6e47617de1..26e67f9cb76 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +#editor +*.swp diff --git a/estateaccountinator/__init__.py b/estateaccountinator/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estateaccountinator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estateaccountinator/__manifest__.py b/estateaccountinator/__manifest__.py new file mode 100644 index 00000000000..2c2bf67f6d5 --- /dev/null +++ b/estateaccountinator/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'estate-accountinator', + 'description': 'Inator that helps you do accounting on real estate', + 'category': 'Tutorials/Accountinator', + 'author': 'gato', + 'depends': [ + 'realestatinator', + 'account', + ], + 'installable': True, + 'application': True, + 'auto_install': False, + 'version': '0.1', +} diff --git a/estateaccountinator/models/__init__.py b/estateaccountinator/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estateaccountinator/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estateaccountinator/models/estate_property.py b/estateaccountinator/models/estate_property.py new file mode 100644 index 00000000000..75aa2e362dd --- /dev/null +++ b/estateaccountinator/models/estate_property.py @@ -0,0 +1,24 @@ +from odoo import api, Command, fields, models + + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + def mark_sold(self): + for record in self: + line_defs = [ + { + 'name': f'selling price (6% of {record.selling_price})', + 'quantity': 1, + 'price_unit': 0.06*record.selling_price + }, + { + 'name': 'administrative fees', + 'quantity': 1, + 'price_unit': 100 + }, + ] + lines = [Command.create(line) for line in line_defs] + values = {'partner_id': record.buyer.id, 'move_type': 'out_invoice', 'line_ids': lines} + moves = self.env['account.move'].create(values) + return super().mark_sold() diff --git a/realestatinator/__init__.py b/realestatinator/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/realestatinator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/realestatinator/__manifest__.py b/realestatinator/__manifest__.py new file mode 100644 index 00000000000..4beb62d3f2a --- /dev/null +++ b/realestatinator/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'name': 'real-estate-inator', + 'description': 'Inator that helps you find real estate.', + 'category': 'Tutorials/RealEstateInator', + 'author': 'gato', + 'depends': [ + 'base', + 'web', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tags_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_menus.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False, + 'version': '0.1', +} diff --git a/realestatinator/models/__init__.py b/realestatinator/models/__init__.py new file mode 100644 index 00000000000..006822a2a9c --- /dev/null +++ b/realestatinator/models/__init__.py @@ -0,0 +1,6 @@ +from . import res_users +from . import res_partner +from . import estate_property +from . import estate_property_type +from . import estate_property_tags +from . import estate_property_offer diff --git a/realestatinator/models/estate_property.py b/realestatinator/models/estate_property.py new file mode 100644 index 00000000000..8c7d2e8494f --- /dev/null +++ b/realestatinator/models/estate_property.py @@ -0,0 +1,112 @@ +from odoo import api, exceptions, fields, models + +class EstatePropery(models.Model): + _name = 'estate.property' + _description = 'real estate property' + _order = 'id desc' + _sql_constraints = [ + ('check_expected_price_positive', 'CHECK (0 < expected_price)', 'Check that the expected price is strictly positive'), + + ] + + + name = fields.Char('Title', required=True) + active = fields.Boolean('Active', default=False) + bedrooms = fields.Integer('Bedrooms', default=2) + best_price = fields.Float('Best Offer', compute='_compute_best_price') + buyer = fields.Many2one('res.partner', string='Buyer', copy=False) + description = fields.Text('Description') + date_availability = fields.Date('Available Date', copy=False, default=fields.Date.add(fields.Date.today(), months=+3)) + expected_price = fields.Float('Expected Price') + facades = fields.Integer('Facades') + garage = fields.Boolean('Garage') + garden = fields.Boolean('Garden') + garden_area = fields.Integer('Garden Area') + garden_orientation = fields.Selection(string='Garden Orientation', selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ]) + living_area = fields.Integer('Living Area') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offer') + postcode = fields.Char('Postcode') + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + sales_person = fields.Many2one('res.users', string='Sales Person', default=lambda self: self.env.user) + selling_price = fields.Float('Selling Price', readonly=True, copy=False) + sequence = fields.Integer('Sequence', default=0) + state = fields.Selection(string='State', selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], default='new') + tag_ids = fields.Many2many('estate.property.tags', string='Tags') + total_area = fields.Integer('Total Area', readonly=True, compute='_compute_total_area') + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for line in self: + line.total_area = line.living_area + line.garden_area + + @api.depends('offer_ids.price') + def _compute_best_price(self): + for record in self: + prices = [0] + record.offer_ids.mapped('price') + record.best_price = max(prices) + + @api.onchange('garden') + def _set_garden_properties(self): + for record in self: + # record.garden_orientation = 'N' if record.garden else '' + # record.garden_area = 10 if record.garden else 0 + + if record.garden: + if record.garden_orientation not in ['north', 'east', 'west', 'south']: + record.garden_orientation = 'north' + if record.garden_area == 0: + record.garden_area = 10 + else: + record.garden_orientation = '' + record.garden_area = 0 + + def mark_cancelled(self): + for record in self: + if record.state == 'cancelled': + raise exceptions.UserError('This property is already cancelled.') + + if record.state == 'sold': + raise exceptions.UserError('This property cannot be cancelled because it has already been sold.') + + record.state = 'cancelled' + record.active = False + + def mark_sold(self): + + for record in self: + if record.state == 'sold': + raise exceptions.UserError('This property is already sold.') + + + if record.state == 'cancelled': + raise exceptions.UserError('This property cannot be sold because it has already been cancelled.') + + record.state = 'sold' + record.active = False + + @api.constrains('selling_price') + def _check_selling_price(self): + for record in self: + if record.state not in ['offer_accepted', 'sold']: + return + if record.selling_price < 0.9 * record.expected_price: + raise exceptions.ValidationError('Selling price must be at least 90% of expected price.') + + @api.ondelete(at_uninstall=False) + def _unlink(self): + for record in self: + if record.state not in ['new', 'cancelled']: + raise exceptions.UserError('Property must be either new or cancelled to be deleted.') + + diff --git a/realestatinator/models/estate_property_offer.py b/realestatinator/models/estate_property_offer.py new file mode 100644 index 00000000000..36ac997d310 --- /dev/null +++ b/realestatinator/models/estate_property_offer.py @@ -0,0 +1,57 @@ +from odoo import api, exceptions, fields, models + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'estate property offer' + _order = 'price desc' + _sql_constraints = [ + ('check_offer_price_positive', 'CHECK (0 < price)', 'Check that the offer price is strictly positive.'), + ] + + creation_date = fields.Date('Creation Date', default=fields.Date.today()) + date_deadline = fields.Date('Deadline', compute='_compute_deadline', inverse='_inverse_deadline') + partner_id = fields.Many2one('res.partner', string='Partner') + price = fields.Float('Price') + property_id = fields.Many2one('estate.property', string='Property') + property_type_id = fields.Many2one(related='property_id.property_type_id', store=True) + status = fields.Selection(string='Status', selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused') + ], copy=False) + validity = fields.Integer('Validity', default=7) + + + @api.depends('validity') + def _compute_deadline(self): + for record in self: + record.date_deadline = fields.Date.add(record.creation_date, days=record.validity) + + def _inverse_deadline(self): + for record in self: + delta = record.date_deadline - record.creation_date + record.validity = delta.days + + def refuse_offer(self): + for record in self: + if record.status == 'accepted': + record.property_id.state = 'offer_received' + record.property_id.selling_price = 0 + record.property_id.buyer = None + record.status = 'refused' + + def accept_offer(self): + for record in self: + if record.property_id.selling_price != 0: + raise exceptions.UserError('An offer as already been accepted for this property.') + record.property_id.state = 'offer_accepted' + record.status = 'accepted' + record.property_id.selling_price = record.price + record.property_id.buyer = record.partner_id + @api.model + def create(self, vals): + estate_property = self.env['estate.property'].browse(vals["property_id"]) + if vals["price"] < estate_property.best_price: + raise exceptions.UserError(f'Offer must be higher than the current best offer({estate_property.best_price})') + if estate_property.state == 'new': + estate_property.state = 'offer_received' + return super().create(vals) diff --git a/realestatinator/models/estate_property_tags.py b/realestatinator/models/estate_property_tags.py new file mode 100644 index 00000000000..3b7286ac6b5 --- /dev/null +++ b/realestatinator/models/estate_property_tags.py @@ -0,0 +1,13 @@ +from odoo import fields, models + +class EstatePropertyTags(models.Model): + _name = 'estate.property.tags' + _description = 'estate property tag' + _order = 'name' + _sql_constraints = [ + ('name_unique', 'UNIQUE (name)', 'make sure tag name is unique.') + ] + + + color = fields.Integer('Colour') + name = fields.Char('Name', required=True) diff --git a/realestatinator/models/estate_property_type.py b/realestatinator/models/estate_property_type.py new file mode 100644 index 00000000000..65d025c16ed --- /dev/null +++ b/realestatinator/models/estate_property_type.py @@ -0,0 +1,22 @@ +from odoo import api, fields, models + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'real estate property type' + _order = 'name, sequence' + _sql_constraints = [ + ('name_unique', 'UNIQUE (name)', 'make sure type name is unique.') + ] + + name = fields.Char('Name', required=True) + offer_count = fields.Integer(string='Offer Count', compute='_count_offers') + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers') + property_ids = fields.One2many('estate.property', 'property_type_id', string='Property Type') + sequence = fields.Integer('sequence', default=1) + + + @api.depends('offer_ids') + def _count_offers(self): + for record in self: + record.offer_count = len(record.offer_ids) + diff --git a/realestatinator/models/res_partner.py b/realestatinator/models/res_partner.py new file mode 100644 index 00000000000..fa1d588b361 --- /dev/null +++ b/realestatinator/models/res_partner.py @@ -0,0 +1,8 @@ +from odoo import api, fields, models +from odoo.osv import expression + + +class Partner(models.Model): + _inherit = 'res.partner' + + diff --git a/realestatinator/models/res_users.py b/realestatinator/models/res_users.py new file mode 100644 index 00000000000..e5573766271 --- /dev/null +++ b/realestatinator/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class Users(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many('estate.property', 'sales_person', string='Properties') diff --git a/realestatinator/security/ir.model.access.csv b/realestatinator/security/ir.model.access.csv new file mode 100644 index 00000000000..417deb122c9 --- /dev/null +++ b/realestatinator/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 +realestatinator.access_estate_property,access_estate_property,realestatinator.model_estate_property,base.group_user,1,1,1,1 +realestatinator.access_estate_property_type,access_estate_property_type,realestatinator.model_estate_property_type,base.group_user,1,1,1,1 +realestatinator.access_estate_property_tags,access_estate_property_tags,realestatinator.model_estate_property_tags,base.group_user,1,1,1,1 +realestatinator.access_estate_property_offer,access_estate_property_offer,realestatinator.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/realestatinator/views/estate_menus.xml b/realestatinator/views/estate_menus.xml new file mode 100644 index 00000000000..50d12c2df2b --- /dev/null +++ b/realestatinator/views/estate_menus.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/realestatinator/views/estate_property_offer_views.xml b/realestatinator/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..4e5716552ad --- /dev/null +++ b/realestatinator/views/estate_property_offer_views.xml @@ -0,0 +1,27 @@ + + + + estate.property.offer.action + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + estate.property.offer.view.list + estate.property.offer + + + + +