diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b6e47617de1..00000000000 --- a/.gitignore +++ /dev/null @@ -1,129 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ 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..1f833dc484a --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': 'Real estate', + 'version': '0.1', + 'depends': ['base'], + 'author': 'odoo SA', + 'category': 'Finance', + 'description': """ + Empty real estate app for tutorial purposes + """, + 'application': 'True', + 'data': [ + 'security/ir.model.access.csv', + 'view/estate_property_views.xml', + 'view/estate_property_type_views.xml', + 'view/estate_property_tag_views.xml', + 'view/estate_property_offer_views.xml', + 'view/estate_menus.xml', + 'view/user_property_views.xml' + ], + 'license': 'LGPL-3', +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..c0ef8d7922c --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property, estate_property_tag, estate_property_type, estate_property_offer, user_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..f42fd7b7bdd --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,99 @@ +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class Estate(models.Model): + _name = 'estate.property' + _description = 'It allows to manage your properties' + _order = 'id desc' + + name = fields.Char(required=True, default='Unknown') + property_type_id = fields.Many2one('estate.property.type') + tag_ids = fields.Many2many('estate.property.tag') + last_seen = fields.Datetime('Last Seen', default=fields.Datetime.now) + description = fields.Char(required=True) + postcode = fields.Char() + date_availability = fields.Date(default=fields.Date.today() + relativedelta(months=3), copy=False) + active = fields.Boolean(default=True) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + best_offer = fields.Float(compute='_compute_best_offer') + sales_person_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user) + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offer') + buyer_id = fields.Many2one('res.partner', string='Customer') + bedrooms = fields.Integer(default=2) + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_orientation = fields.Selection( + string='Orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + ) + living_area = fields.Integer() + garden_area = fields.Integer() + total_area = fields.Float(compute='_compute_total_area') + state = fields.Selection( + string='State', + selection=[ + ('new', 'New'), + ('offer received', 'Offer Received'), + ('offer accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + readonly=True, + default='new', + ) + + _sql_constraints = [ + ('expected_price', 'CHECK(expected_price >= 0 )', 'A price should always be possitive'), + ('selling_price', 'CHECK(selling_price >= 0 )', 'A price should always be possitive'), + ] + + @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.onchange('offer_ids') + def _onchange_offer_ids(self): + if self.state == 'new': + if len(self.offer_ids): + self.state = 'offer received' + self._origin.state = 'offer received' # error, didn't found the source, fix, I'm a bugfixer + + @api.depends('offer_ids') + def _compute_best_offer(self): + for record in self: + all_price = record.offer_ids.mapped('price') + if all_price: + record.best_offer = max(all_price) + else: + record.best_offer = 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 = '' + + def action_property_sold(self): + for record in self: + if record.state == 'cancelled': + raise UserError(self.env._('property already sold')) + record.state = 'sold' + + def action_property_cancelled(self): + for record in self: + if record.state == 'sold': + raise UserError(self.env._('property already sold')) + record.state = 'cancelled' + + @api.ondelete(at_uninstall=False) + def _unlink_for_state(self): + if self.state not in ['new', 'cancelled']: + raise UserError('You cannot delete an property that as is not New or cancelled') diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..4415be2ec42 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,67 @@ +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstateOffer(models.Model): + _name = 'estate.property.offer' + _description = 'It allows to create a new property offer' + _order = 'price desc' + + price = fields.Float(required=True) + validity = fields.Integer() + date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date') + create_date = fields.Date(default=fields.Date.today(), required=True) + partner_id = fields.Many2one('res.partner', string='Customer') + property_id = fields.Many2one('estate.property', string='Property') + status = fields.Selection( + string='State', + selection=[ + ('pending', 'Pending'), + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ], + default='pending', + ) + property_type_id = fields.Many2one(related='property_id.property_type_id') + + _sql_constraints = [('price', 'CHECK(price >= 0 )', 'A price should always be possitive')] + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for record in self: + record.date_deadline = (record.create_date or fields.Date.today()) + relativedelta(days=record.validity) + + def _inverse_date(self): + for record in self: + record.validity = (record.date_deadline - fields.Date.today()).days + + def action_accept_offer(self): + for record in self: + if record.property_id.state not in ['sold', 'cancelled']: + for offer in record.property_id.offer_ids: + offer.status = 'refused' + record.status = 'accepted' + record.property_id.selling_price = record.price + record.property_id.state = 'offer accepted' + else: + raise UserError(self.env._('property already sold')) + return True + + def action_refuse_offer(self): + for record in self: + if record.property_id.state not in ['sold', 'cancelled']: + if record.status == 'accepted': + record.property_id.selling_price = 0 + record.property_id.state = 'offer received' + else: + raise UserError(self.env._('property already sold')) + record.status = 'refused' + return True + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals['price'] < self.env['estate.property'].browse(vals['property_id']).best_offer: + raise UserError('The price is lower than the best-offer') + return super().create(vals_list) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..668fcd63cf6 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstateTags(models.Model): + _name = 'estate.property.tag' + _description = 'It allows to create a new property tag' + _order = 'name desc' + + name = fields.Char(required=True) + color = fields.Integer(default=0) + + _sql_constraints = [('unique_tag_by_estate_property', 'UNIQUE(name)', 'Only one name by tag')] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..9898d6df066 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,18 @@ +from odoo import api, fields, models + + +class EstateType(models.Model): + _name = 'estate.property.type' + _description = 'It allows to create a new property type' + _order = 'sequence desc' + + name = fields.Char(required=True) + sequence = fields.Integer('Sequence', default=1, help='Used to order properties types. Lower is better.') + properties_ids = fields.One2many('estate.property', 'property_type_id') + offer_ids = fields.One2many('estate.property.offer', 'property_type_id') + offer_count = fields.Integer(compute='_compute_offer_stats') + + @api.depends('offer_ids') + def _compute_offer_stats(self): + for record in self: + record.offer_count = len(record.offer_ids) or 0 diff --git a/estate/models/user_property.py b/estate/models/user_property.py new file mode 100644 index 00000000000..ef81d7a4818 --- /dev/null +++ b/estate/models/user_property.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class UserProperty(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'estate.property', 'sales_person_id', domain=[('state', 'in', ['new', 'offer received'])] + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..c4e7d89e491 --- /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,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/view/estate_menus.xml b/estate/view/estate_menus.xml new file mode 100644 index 00000000000..87b39a249c7 --- /dev/null +++ b/estate/view/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/estate/view/estate_property_offer_views.xml b/estate/view/estate_property_offer_views.xml new file mode 100644 index 00000000000..f15407178cc --- /dev/null +++ b/estate/view/estate_property_offer_views.xml @@ -0,0 +1,49 @@ + + + + + estate.offer.main.list + estate.property.offer + + + + + + + +

+ +

+ + + + + + + + + + + + + +
+
+
diff --git a/estate/view/estate_property_views.xml b/estate/view/estate_property_views.xml new file mode 100644 index 00000000000..ca8c3afd433 --- /dev/null +++ b/estate/view/estate_property_views.xml @@ -0,0 +1,119 @@ + + + estate.main.list + estate.property + + + + + + + + + + + + + + + estate.main.kanban + estate.property + + + +
+ + + + + + +
+
+
+
+
+ + + estate.form + estate.property + +
+
+
+ +

+ +

+ +
+
+ + + + +
+
+ + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.view.search + estate.property + + + + + + + + + + + + + + + Property + estate.property + kanban,list,form + {'search_default_available': True} + +
diff --git a/estate/view/user_property_views.xml b/estate/view/user_property_views.xml new file mode 100644 index 00000000000..c970964a4ac --- /dev/null +++ b/estate/view/user_property_views.xml @@ -0,0 +1,16 @@ + + + res.users.view.form.inherit.property.offer + 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..54bed152adb --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,12 @@ +{ + 'name': 'EstateAccount', + 'version': '0.1', + 'depends': ['estate', 'account'], + 'author': 'odoo SA', + 'category': 'Finance', + 'description': """ + Link module to link estate and invoicing + """, + 'application': 'False', + 'license': 'LGPL-3', +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..6ccb0e0820c --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_sold_inherited diff --git a/estate_account/models/estate_sold_inherited.py b/estate_account/models/estate_sold_inherited.py new file mode 100644 index 00000000000..0174206974c --- /dev/null +++ b/estate_account/models/estate_sold_inherited.py @@ -0,0 +1,38 @@ +from odoo import Command, models + + +class EstateSoldInherited(models.Model): + _inherit = 'estate.property' + + def action_property_sold(self): + result = super().action_property_sold() + self.env['account.move'].create( + { + 'partner_id': self.buyer_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': [ + Command.create( + { + 'name': f'The property {self.name}', + 'quantity': 1, + 'price_unit': self.selling_price, + } + ), + Command.create( + { + 'name': f'Commission for property {self.name}', + 'quantity': 1, + 'price_unit': self.selling_price * 0.06, + } + ), + Command.create( + { + 'name': 'Administrative fees', + 'quantity': 1, + 'price_unit': 100.0, + } + ), + ], + } + ) + return result