diff --git a/estate/__init__.py b/estate/__init__.py index 0650744f6bc..899bcc97f0f 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1,2 @@ from . import models + diff --git a/estate/__manifest__.py b/estate/__manifest__.py index d7cfa57d4c5..53b04d20e1c 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,11 +1,18 @@ { - "name": "Estate", # The name that will appear in the App list - "version": "16.0.0", # Version - "application": True, # This line says the module is an App, and not a module - "depends": ["base"], # dependencies + "name": "Estate", + "version": "16.0.0", + "application": True, + "depends": ["base"], "data": [ - + "security/ir.model.access.csv", + "views/estate_property_offer_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_views.xml", + "views/estate_menus.xml", + 'views/res_users_views.xml', ], "installable": True, + "application": True, 'license': 'LGPL-3', } diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9d34e4edf67 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import estate_property +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..13523eda5ec --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,123 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare, float_is_zero +from datetime import datetime, timedelta + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + _order = "id desc" + + # SQL Constraints + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', + 'The expected price must be strictly positive.'), + ('check_selling_price', 'CHECK(selling_price >= 0)', + 'The selling price must be positive.'), + ] + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + total_area = fields.Integer(compute="_compute_total_area", string="Total Area") + best_price = fields.Float(compute="_compute_best_price", string="Best Offer") + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ]) + active = fields.Boolean(default=True) + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], required=True, copy=False, default='new') + property_type_id = fields.Many2one( + "estate.property.type", + string="Property Type" + ) + buyer_id = fields.Many2one( + "res.partner", + string="Buyer", + copy=False + ) + salesperson_id = fields.Many2one( + "res.users", + string="Salesperson", + default=lambda self: self.env.user + ) + tag_ids = fields.Many2many( + "estate.property.tag", + string="Tags" + ) + offer_ids = fields.One2many( + "estate.property.offer", + "property_id", + string="Offers" + ) + + #api.depends area + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + if record.offer_ids: + record.best_price = max(record.offer_ids.mapped('price')) + else: + record.best_price = 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 + + #action area + def action_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError("Cancelled property cannot be sold.") + record.state = 'sold' + return True + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise UserError("Sold property cannot be cancelled.") + record.state = 'cancelled' + return True + + + #constrains area + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for record in self: + if not float_is_zero(record.selling_price, precision_digits=2): + if float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0: + raise ValidationError("The selling price cannot be lower than 90% of the expected price.") + + + @api.ondelete(at_uninstall=False) + def _check_property_state(self): + for record in self: + if record.state not in ['new', 'cancelled']: + raise UserError("Only new and cancelled properties can be deleted.") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..64e6dc8090d --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,70 @@ +from odoo import api, models, fields +from datetime import datetime, timedelta + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real Estate Property Offer" + _order = "price desc" + + # SQL Constraints + _sql_constraints = [ + ('check_price', 'CHECK(price > 0)', + 'The offer price must be strictly positive.'), + ] + + price = fields.Float(required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date() + status = fields.Selection( + [('accepted', 'Accepted'), ('refused', 'Refused')], + copy=False + ) + partner_id = fields.Many2one( + "res.partner", + string="Buyer", + required=True + ) + property_id = fields.Many2one( + "estate.property", + string="Property", + required=True, + ondelete="cascade" + ) + + property_type_id = fields.Many2one( + related="property_id.property_type_id", + string="Property Type", + store=True + ) + + def action_accept(self): + for record in self: + # Diğer tüm offer'ları refuse et + record.property_id.offer_ids.write({'status': 'refused'}) + # Bu offer'ı accept et + record.status = 'accepted' + # Property'nin buyer ve selling price'ını set et + record.property_id.buyer_id = record.partner_id + record.property_id.selling_price = record.price + record.property_id.state = 'offer_accepted' + return True + + def action_refuse(self): + for record in self: + record.status = 'refused' + return True + + @api.model + def create(self, vals_list): + property_obj = self.env['estate.property'].browse(vals_list['property_id']) + + existing_offers = self.search([('property_id', '=', vals_list['property_id'])]) + if existing_offers: + max_price = max(existing_offers.mapped('price')) + if vals_list['price'] <= max_price: + raise UserError("The offer must be higher than existing offers.") + + + property_obj.state = 'offer_received' + + return super().create(vals_list) \ 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..ebf13e6be20 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,15 @@ +from odoo import models, fields + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real Estate Property Tag" + _order = "name" + + # SQL Constraints + _sql_constraints = [ + ('check_name', 'UNIQUE(name)', + 'The property type name must be unique.'), + ] + + name = fields.Char(required=True) + color = fields.Integer() \ 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..93959916f2f --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,35 @@ +from odoo import api, models, fields + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + _order = "name" + + # SQL Constraints + _sql_constraints = [ + ('check_name', 'UNIQUE(name)', + 'The property type name must be unique.'), + ] + + name = fields.Char(required=True) + sequence = fields.Integer(default=1, help="Used to order types") + property_ids = fields.One2many( + "estate.property", + "property_type_id", + string="Properties" + ) + + offer_ids = fields.One2many( + "estate.property.offer", + "property_type_id", + string="Offers" + ) + offer_count = fields.Integer( + string="Offers Count", + compute="_compute_offer_count" + ) + + @api.depends("offer_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) \ 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..a409c05d614 --- /dev/null +++ b/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( + "estate.property", + "salesperson_id", + string="Properties", + domain=[('state', 'in', ['new', 'offer_received', 'offer_accepted'])] + ) \ 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..ee239f77e72 --- /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_user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type_user,access_estate_property_type_user,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag_user,access_estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer_user,access_estate_property_offer_user,model_estate_property_offer,base.group_user,1,1,1,1 \ 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..819397e809a --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + \ 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..b71a9651412 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,51 @@ + + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + + + + + + estate.property.offer.form + estate.property.offer + + + + + + + + + + + + + + + + + + + Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..b55d842bb4e --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,50 @@ + + + + Property Tags + estate.property.tag + list,form + + + + + + estate.property.tag.list + estate.property.tag + + + + + + + + + + estate.property.tag.form + estate.property.tag + + + + + + + + + + + + + + Property Tags + estate.property.tag + list,form + + + + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..97adb083fb1 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,57 @@ + + + + + Property Types + estate.property.type + list,form + + + + + + + + + estate.property.type.list + estate.property.type + + + + + + + + + + estate.property.type.form + estate.property.type + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..2b7e681e421 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,191 @@ + + + + Properties + estate.property + kanban,list,form + + {'search_default_available': True} + + + Create a property to advertise! + + + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + + + + + + + + + + + + + + + + + Expected Price: + + + + Best Offer: + + + + Selling Price: + + + + + + + + + + + + + + + diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..59e83774cfd --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,21 @@ + + + + res.users.form.inherit.estate + res.users + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..a759392c2cc --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': 'Real Estate Account', + 'version': '18.0', + 'depends': ['estate', 'account'], + 'author': 'Suleyman', + 'category': 'Real Estate', + 'description': """ + Link module between Real Estate and Accounting. + Creates invoices when properties are sold. + """, + 'data': [], + 'demo': [], + 'installable': True, + 'auto_install': False, +} \ No newline at end of file diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ No newline at end of file diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..2ef06f62df9 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,33 @@ +from odoo import models, Command + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold(self): + + result = super().action_sold() + + for record in self: + + invoice = self.env['account.move'].create({ + 'partner_id': record.buyer_id.id, + 'move_type': 'out_invoice', # Customer Invoice + 'invoice_line_ids': [ + + Command.create({ + 'name': f'Commission for {record.name}, (Selling Price: {record.selling_price:,.2f})', + 'quantity': 1, + 'price_unit': record.selling_price * 0.06, + }), + + Command.create({ + 'name': 'Administrative fees', + 'quantity': 1, + 'price_unit': 100.00, + }), + ], + }) + + print(f"Invoice created: {invoice.id} for property: {record.name}") + + return result \ No newline at end of file
+ Create a property to advertise! +