- hello world
+
+
The sum of the Counters :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 00000000000..f374a9dd3c8
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,9 @@
+import {onMounted, useRef} from "@odoo/owl";
+
+export function useAutoFocus(name) {
+ const input = useRef(name);
+
+ onMounted(() => {
+ input.el.focus();
+ });
+}
diff --git a/awesome_owl/views/templates.xml b/awesome_owl/views/templates.xml
index aa54c1a7241..0a0f1046305 100644
--- a/awesome_owl/views/templates.xml
+++ b/awesome_owl/views/templates.xml
@@ -1,15 +1,16 @@
-
+
+
-
+
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..fd36d9347bc
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,28 @@
+{
+ 'name': 'Real Estate',
+ 'description': 'Real Estate - Management',
+ 'category': 'Sales/CRM',
+ 'version': '1.0',
+ 'depends': ['base', 'mail'],
+ 'author': 'Odoo S.A.',
+ 'license': 'LGPL-3',
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/estate_property_offer_views.xml',
+ 'views/estate_property_views.xml',
+ 'views/estate_property_type_views.xml',
+ 'views/estate_property_tag_views.xml',
+ 'views/res_users_views.xml',
+ 'views/estate_menu_views.xml',
+ 'data/estate_property_type_data.xml',
+ ],
+ 'demo': [
+ 'demo/estate_property_type_demo.xml',
+ 'demo/estate_property_demo.xml',
+ 'demo/estate_property_offer_demo.xml',
+ 'demo/estate_workflow_demo.xml',
+ ],
+ 'assets': {},
+ 'application': True,
+ 'installable': True,
+}
diff --git a/estate/data/estate_property_type_data.xml b/estate/data/estate_property_type_data.xml
new file mode 100644
index 00000000000..45165faad99
--- /dev/null
+++ b/estate/data/estate_property_type_data.xml
@@ -0,0 +1,19 @@
+
+
+
+ 1
+ Residential
+
+
+ 2
+ Commercial
+
+
+ 3
+ Industrial
+
+
+ 4
+ Land
+
+
\ No newline at end of file
diff --git a/estate/demo/estate_property_demo.xml b/estate/demo/estate_property_demo.xml
new file mode 100644
index 00000000000..c812e3e079c
--- /dev/null
+++ b/estate/demo/estate_property_demo.xml
@@ -0,0 +1,58 @@
+
+
+
+ Big Villa
+ new
+ A nice and big villa
+ 12345
+ 2020-02-02
+ 1600000
+ 0
+ 6
+ 100
+ 4
+ True
+ True
+ 100000
+ south
+
+
+
+ Trailer home
+ canceled
+ Home in a trailer park
+ 54321
+ 1970-01-01
+ 100000
+ 120000
+ 1
+ 10
+ 4
+ False
+ False
+
+
+
+ The White House
+ new
+ A spot in the oval office
+ 99999
+ 2027-01-01
+ 100000000
+ 42
+ 10000
+ 12
+ True
+ True
+ 1000000
+ south
+
+
+
+
\ No newline at end of file
diff --git a/estate/demo/estate_property_offer_demo.xml b/estate/demo/estate_property_offer_demo.xml
new file mode 100644
index 00000000000..29f22377180
--- /dev/null
+++ b/estate/demo/estate_property_offer_demo.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ 10000
+
+
+
+
+
+ 1500000
+
+
+
+
+
+ 1500001
+
+
+
\ No newline at end of file
diff --git a/estate/demo/estate_property_type_demo.xml b/estate/demo/estate_property_type_demo.xml
new file mode 100644
index 00000000000..5220dbc5cb6
--- /dev/null
+++ b/estate/demo/estate_property_type_demo.xml
@@ -0,0 +1,11 @@
+
+
+
+ 10
+ House
+
+
+ 11
+ Apartment
+
+
\ No newline at end of file
diff --git a/estate/demo/estate_workflow_demo.xml b/estate/demo/estate_workflow_demo.xml
new file mode 100644
index 00000000000..e4b8f9a6a7f
--- /dev/null
+++ b/estate/demo/estate_workflow_demo.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..fea9f441d6d
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,5 @@
+from . import estate_property
+from . import estate_property_offer
+from . import estate_property_tag
+from . import estate_property_type
+from . import res_users
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
new file mode 100644
index 00000000000..3494a4009e9
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,128 @@
+from dateutil.relativedelta import relativedelta
+
+from odoo import fields, models, api
+from odoo.exceptions import UserError
+from odoo.tools import float_compare
+
+
+class EstateProperty(models.Model):
+ _name = 'estate.property'
+ _description = "Estate property"
+
+ _order = 'id desc'
+
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+
+ name = fields.Char(required=True, string="Title", tracking=True)
+ description = fields.Text(string="Description", tracking=True)
+ postcode = fields.Char(required=True, string="Postcode", tracking=True)
+ date_availability = fields.Date(required=True, default=lambda self: fields.Date.today() + relativedelta(months=+3), copy=False, string="Available From")
+ expected_price = fields.Float(required=True, string="Expected Price", tracking=True)
+ selling_price = fields.Float(readonly=True, copy=False, string="Selling Price")
+ bedrooms = fields.Integer(required=True, default=2, string="Bedrooms")
+ living_area = fields.Integer(required=True, string="Living Area (sqm)", help="Living area with a ceiling height of minimum 4 feet")
+ facades = fields.Integer(required=True, string="Facades")
+ garage = fields.Boolean(string="Garage")
+ garden = fields.Boolean(string="Garden", inverse='_inverse_garden')
+ garden_area = fields.Integer(string="Garden Area (sqm)")
+ garden_orientation = fields.Selection([
+ ('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")
+ ], string="Garden Orientation")
+ total_area = fields.Integer(store=True, compute='_compute_total_area', string="Total Area (sqm)")
+ state = fields.Selection([
+ ('new', "New"), ('offer_received', "Offer Received"), ('offer_accepted', "Offer Accepted"), ('sold', "Sold"), ('canceled', "Canceled")
+ ], string="State", default='new', required=True, readonly=True, tracking=True)
+ active = fields.Boolean(default=True)
+ property_type_id = fields.Many2one('estate.property.type', string="Property Type")
+ buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False)
+ seller_id = fields.Many2one('res.users', string="Salesman", 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")
+ best_price = fields.Float(required=True, string="Best Price", readonly=True, compute='_compute_best_price')
+ available_for_offers = fields.Boolean(store=False, compute='_compute_available_for_offers')
+ color = fields.Integer(string="Color Index", default=0)
+
+ _check_expected_price = models.Constraint(
+ 'CHECK (expected_price > 0)',
+ "The expected price must be greater than 0."
+ )
+ _check_selling_price = models.Constraint(
+ 'CHECK (selling_price >= 0)',
+ "The selling price must be greater or equal than 0."
+ )
+
+ @api.depends('living_area', 'garden_area', 'garden')
+ def _compute_total_area(self):
+ for record in self:
+ record.total_area = record.living_area + record.garden_area
+
+ @api.onchange('garden')
+ def _on_change_garden(self):
+ for record in self:
+ if record.garden:
+ record.garden_area = 10
+ record.garden_orientation = 'north'
+ else:
+ record.garden_area = 0
+ record.garden_orientation = None
+
+ def _inverse_garden(self):
+ for record in self:
+ if not record.garden:
+ record.garden_area = 0
+ record.garden_orientation = None
+
+ @api.depends('offer_ids.price')
+ def _compute_best_price(self):
+ for record in self:
+ record.best_price = max(record.offer_ids.mapped('price')) if len(record.offer_ids) > 0 else 0
+
+ def action_mark_as_sold(self):
+ for record in self:
+ if record.state != 'offer_accepted':
+ raise UserError(self.env._("Cannot mark a non offer_accepted property as sold"))
+
+ if len(record.offer_ids.filtered(lambda offer: offer.status == 'accepted')) != 1:
+ raise UserError(self.env._("Cannot mark a property as sold without an accepted offer"))
+
+ record.state = 'sold'
+
+ def action_mark_as_canceled(self):
+ for record in self:
+ if record.state == 'sold':
+ raise UserError(self.env._("Cannot mark a sold property as canceled"))
+ record.state = 'canceled'
+
+ def compute_accepted_offer(self, offer):
+ for record in self:
+ if not record.available_for_offers:
+ raise UserError(self.env._("Cannot add or update an offer on a property that is not available for offers"))
+
+ record.selling_price = offer.price
+ record.buyer_id = offer.partner_id
+ record.state = 'offer_accepted'
+
+ def compute_new_offer(self):
+ for record in self:
+ if not record.available_for_offers:
+ raise UserError(self.env._("Cannot add or update an offer on a property that is not available for offers"))
+
+ if record.state == 'new':
+ record.state = 'offer_received'
+
+ @api.depends('state')
+ def _compute_available_for_offers(self):
+ for record in self:
+ record.available_for_offers = record.state in {'new', 'offer_received'}
+
+ @api.constrains('selling_price', 'expected_price')
+ def _check_prices(self):
+ for record in self:
+ if len(record.offer_ids.filtered(lambda x: x.status == 'accepted')) > 0 and float_compare(record.selling_price, record.expected_price * 0.9, 2) == -1:
+ raise UserError(self.env._("The selling price must be at least 90% of the expected price"))
+
+ @api.ondelete(at_uninstall=False)
+ def _check_ondelete(self):
+ for record in self:
+ if record.state not in {'new', 'canceled'}:
+ raise UserError(self.env._("Cannot delete a property that is not in the new or canceled state"))
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..11d1514e665
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,75 @@
+from dateutil.relativedelta import relativedelta
+
+from odoo import fields, models, api
+from odoo.exceptions import UserError
+
+
+class EstatePropertyOffer(models.Model):
+ _name = 'estate.property.offer'
+ _description = "Estate property offer"
+
+ _order = 'price desc'
+
+ price = fields.Float(required=True, string="Price")
+ status = fields.Selection([
+ ('accepted', "Accepted"), ('refused', "Refused"),
+ ], copy=False, string="Status", readonly=True)
+ partner_id = fields.Many2one('res.partner', string="Partner", required=True)
+ property_id = fields.Many2one('estate.property', string="Property", required=True)
+ validity = fields.Integer(required=True, string="Validity", default=7)
+ date_deadline = fields.Date(string="Deadline", compute='_compute_date_deadline', inverse='_inverse_date_deadline')
+ available_for_offers = fields.Boolean(related='property_id.available_for_offers')
+ property_type_id = fields.Many2one('estate.property.type', related='property_id.property_type_id', store=True)
+
+ _check_price = models.Constraint(
+ 'CHECK (price > 0)',
+ "The price must be greater than 0."
+ )
+
+ @api.depends('validity')
+ def _compute_date_deadline(self):
+ for record in self:
+ record.date_deadline = fields.Date.today() + relativedelta(days=record.validity)
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ record.validity = relativedelta(record.date_deadline, (record.create_date if record.create_date else fields.Date.today())).days
+
+ def action_mark_as_accepted(self):
+ for record in self:
+ if record.available_for_offers:
+ record.status = 'accepted'
+ record.property_id.compute_accepted_offer(record)
+ else:
+ raise UserError(self.env._("This offer cannot be accepted because the property is already sold or canceled or an offer has been accepted"))
+
+ return True
+
+ def action_mark_as_refused(self):
+ for record in self:
+ if record.available_for_offers:
+ record.status = 'refused'
+ else:
+ raise UserError(self.env._("This offer cannot be accepted because the property is already sold or canceled or an offer has been accepted"))
+
+ return True
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if 'property_id' not in vals:
+ raise UserError(self.env._("You must select a property for the offer."))
+ if 'price' not in vals:
+ raise UserError(self.env._("You must set a price for the offer."))
+
+ property = self.env['estate.property'].browse(vals['property_id'])
+
+ if len(property.offer_ids.filtered(lambda offer: offer.price > vals['price'])) > 0:
+ raise UserError(self.env._("Cannot create an offer with a lower amount than the other offers for this property."))
+
+ offers = super().create(vals_list)
+
+ for record in offers:
+ record.property_id.compute_new_offer()
+
+ return offers
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..1dde5294eaf
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,16 @@
+from odoo import fields, models
+
+
+class EstatePropertyTag(models.Model):
+ _name = 'estate.property.tag'
+ _description = "Estate property tag"
+
+ _order = 'name'
+
+ name = fields.Char(required=True, string="Name")
+ color = fields.Integer(string="Color")
+
+ _name_unique_idx = models.UniqueIndex(
+ '(name)',
+ "The name of the property tag must be unique."
+ )
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 00000000000..2d0c7448643
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,37 @@
+from odoo import fields, models, api
+
+
+class EstatePropertyType(models.Model):
+ _name = 'estate.property.type'
+ _description = "Estate property type"
+
+ _order = 'sequence asc'
+
+ name = fields.Char(required=True, string="Title")
+ property_ids = fields.One2many('estate.property', 'property_type_id', string="Properties")
+ sequence = fields.Integer()
+ offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers")
+ offer_count = fields.Integer(compute='_compute_offer_count')
+
+ _name_unique_idx = models.UniqueIndex(
+ '(name)',
+ "The title of the property type must be unique."
+ )
+
+ @api.depends('offer_ids')
+ def _compute_offer_count(self):
+ for record in self:
+ record.offer_count = len(record.offer_ids)
+
+ def action_show_offers(self):
+ self.ensure_one()
+
+ return {
+ 'name': self.env._("Offers"),
+ 'view_mode': 'list,form',
+ 'res_model': 'estate.property.offer',
+ 'type': 'ir.actions.act_window',
+ 'context': {'create': False, 'delete': False, 'edit': False},
+ 'domain': [('property_type_id', 'in', self.id)],
+ 'target': 'current',
+ }
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 00000000000..472cc6c1f75
--- /dev/null
+++ b/estate/models/res_users.py
@@ -0,0 +1,9 @@
+from odoo import fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = 'res.users'
+
+ property_ids = fields.One2many('estate.property', 'seller_id', string="Available Properties",
+ 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..5d860c91af0
--- /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_user,model_estate_property,base.group_user,1,1,1,1
+access_estate_property_type,access_estate_property_type_user,model_estate_property_type,base.group_user,1,1,1,1
+access_estate_property_tag,access_estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1
+access_estate_property_offer,access_estate_property_offer_user,model_estate_property_offer,base.group_user,1,1,1,1
diff --git a/estate/static/description/icon.png b/estate/static/description/icon.png
new file mode 100644
index 00000000000..24887121b13
Binary files /dev/null and b/estate/static/description/icon.png differ
diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py
new file mode 100644
index 00000000000..755d413272d
--- /dev/null
+++ b/estate/tests/__init__.py
@@ -0,0 +1,3 @@
+from . import common
+from . import test_estate_property
+from . import test_estate_property_offer
diff --git a/estate/tests/common.py b/estate/tests/common.py
new file mode 100644
index 00000000000..9c79a8770e8
--- /dev/null
+++ b/estate/tests/common.py
@@ -0,0 +1,61 @@
+import datetime
+
+from odoo.addons.base.tests.common import BaseCommon
+
+
+class EstateTestCommon(BaseCommon):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.partner_a = cls.env['res.partner'].create({
+ 'name': 'Test Partner A',
+ })
+
+ cls.property_type_house = cls.env['estate.property.type'].create({
+ 'name': 'House',
+ })
+ cls.property_type_apartment = cls.env['estate.property.type'].create({
+ 'name': 'Apartment',
+ })
+
+ def get_create_property_kwargs(self, **kwargs):
+ return {
+ 'name': 'Property ' + str(datetime.datetime.now()),
+ 'postcode': '12345',
+ 'date_availability': datetime.datetime.now() + datetime.timedelta(days=7),
+ 'expected_price': 100000,
+ 'bedrooms': 2,
+ 'living_area': 120,
+ 'facades': 2,
+ 'property_type_id': self.property_type_house.id,
+ 'state': 'new',
+ **kwargs,
+ }
+
+ def create_property(self, state, **kwargs):
+ estate_property = self.env['estate.property'].create(self.get_create_property_kwargs(**kwargs))
+
+ if state in {'offer_received', 'offer_accepted', 'sold', 'canceled'}:
+ offer_a = self.create_offer(estate_property, 5000, partner_id=self.partner.id, validity=7)
+ offer_b = self.create_offer(estate_property, 10000, partner_id=self.partner_a.id, validity=14)
+
+ if state in {'offer_accepted', 'sold'}:
+ offer_a.action_mark_as_refused()
+ offer_b.action_mark_as_accepted()
+
+ if state == 'sold':
+ estate_property.action_mark_as_sold()
+ elif state == 'canceled':
+ estate_property.action_mark_as_canceled()
+
+ return estate_property
+
+ def create_offer(self, estate_property, price_increase=0, **kwargs):
+ return self.env['estate.property.offer'].create({
+ 'price': estate_property.expected_price + price_increase,
+ 'partner_id': self.partner.id,
+ 'property_id': estate_property.id,
+ 'validity': 7,
+ **kwargs,
+ })
diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py
new file mode 100644
index 00000000000..ca78dbc6c35
--- /dev/null
+++ b/estate/tests/test_estate_property.py
@@ -0,0 +1,53 @@
+from odoo import Command
+from odoo.addons.estate.tests.common import EstateTestCommon
+from odoo.exceptions import UserError
+from odoo.tests import tagged, Form
+
+
+@tagged('post_install', '-at_install')
+class EstatePropertyTestCase(EstateTestCommon):
+ def test_sell_property_with_no_offers(self):
+ estate_property = self.create_property('offer_received')
+
+ with self.assertRaises(UserError):
+ estate_property.action_mark_as_sold()
+
+ def test_sell_property_on_state_new(self):
+ estate_property = self.create_property('new')
+
+ with self.assertRaises(UserError):
+ estate_property.action_mark_as_sold()
+
+ def test_garden_reactivity(self):
+ estate_property = self.create_property('new')
+
+ with Form(estate_property) as form_estate_property:
+ form_estate_property.garden = True
+ form_estate_property.save()
+ self.assertEqual(estate_property.garden_area, 10)
+ self.assertEqual(estate_property.garden_orientation, 'north')
+
+ form_estate_property.garden = False
+ form_estate_property.save()
+ self.assertEqual(estate_property.garden_area, 0)
+ self.assertEqual(estate_property.garden_orientation, False)
+
+ form_estate_property.garden = True
+ form_estate_property.garden_area = 100
+ form_estate_property.garden_orientation = 'south'
+ form_estate_property.save()
+ self.assertEqual(estate_property.garden_area, 100)
+ self.assertEqual(estate_property.garden_orientation, 'south')
+
+ def test_create_property_with_offers(self):
+ vals = self.get_create_property_kwargs()
+
+ vals['offer_ids'] = [
+ Command.create({
+ 'price': vals['expected_price'] + 5000,
+ 'partner_id': self.partner.id,
+ 'validity': 7,
+ }),
+ ]
+
+ self.env['estate.property'].create(vals)
\ No newline at end of file
diff --git a/estate/tests/test_estate_property_offer.py b/estate/tests/test_estate_property_offer.py
new file mode 100644
index 00000000000..6ee041b16c9
--- /dev/null
+++ b/estate/tests/test_estate_property_offer.py
@@ -0,0 +1,26 @@
+from odoo.addons.estate.tests.common import EstateTestCommon
+from odoo.exceptions import UserError
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class EstatePropertyOfferTestCase(EstateTestCommon):
+ def test_stop_offer_creation_on_sold_property(self):
+ estate_property = self.create_property('sold')
+
+ with self.assertRaises(UserError):
+ self.create_offer(estate_property, 50000)
+
+ def test_offer_price_too_low_compared_to_other_offers(self):
+ estate_property = self.create_property('offer_received')
+
+ with self.assertRaises(UserError):
+ self.create_offer(estate_property, 0)
+
+ def test_offer_price_too_low_compared_to_expected_price(self):
+ estate_property = self.create_property('new')
+ offer = self.create_offer(estate_property, -estate_property.expected_price / 2)
+
+ with self.assertRaises(UserError):
+ offer.action_mark_as_accepted()
+ estate_property.action_mark_as_sold()
diff --git a/estate/views/estate_menu_views.xml b/estate/views/estate_menu_views.xml
new file mode 100644
index 00000000000..f38e53f4881
--- /dev/null
+++ b/estate/views/estate_menu_views.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
new file mode 100644
index 00000000000..612d80a0304
--- /dev/null
+++ b/estate/views/estate_property_offer_views.xml
@@ -0,0 +1,37 @@
+
+
+
+ estate.property.offer.view.list
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.view.form
+ estate.property.offer
+
+
+
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
new file mode 100644
index 00000000000..5669452106a
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,46 @@
+
+
+
+ estate.property.tag.view.list
+ estate.property.tag
+
+
+
+
+
+
+
+
+
+ estate.property.tag.search
+ estate.property.tag
+
+
+
+
+
+
+
+
+ estate.property.tag.view.form
+ estate.property.tag
+
+
+
+
+
+
+ Estate Property Tag
+ estate.property.tag
+ list,form
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..bcc5039884c
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,71 @@
+
+
+
+ estate.property.type.view.list
+ estate.property.type
+
+
+
+
+
+
+
+
+
+ estate.property.type.search
+ estate.property.type
+
+
+
+
+
+
+
+
+ estate.property.type.view.form
+ estate.property.type
+
+
+
+
+
+
+ Estate Property Type
+ estate.property.type
+ list,form
+
+
\ No newline at end of file
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
new file mode 100644
index 00000000000..43c3045d472
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,182 @@
+
+
+
+ estate.property.view.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.view.form
+ estate.property
+
+
+
+
+
+
+ estate.property.view.kanban
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+ Expected Price:
+
+
+ Best Price:
+
+
+ Selling Price:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Estate Property
+ estate.property
+ list,form,kanban
+ {'search_default_available': True}
+
+
\ No newline at end of file
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml
new file mode 100644
index 00000000000..e2643ebd2b6
--- /dev/null
+++ b/estate/views/res_users_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ res.users.form.inherit.properties
+ 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..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..99bc9c7560d
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,15 @@
+{
+ 'name': 'Real Estate Accounting',
+ 'description': 'Real Estate - Accounting',
+ 'category': 'Sales/CRM',
+ 'version': '1.0',
+ 'depends': ['estate', 'account'],
+ 'author': 'Odoo S.A.',
+ 'license': 'LGPL-3',
+ 'data': [
+ 'views/estate_property_views.xml'
+ ],
+ 'assets': {},
+ 'application': True,
+ 'installable': True,
+}
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
new file mode 100644
index 00000000000..49e424d7342
--- /dev/null
+++ b/estate_account/models/__init__.py
@@ -0,0 +1,2 @@
+from . import estate_property
+from . import estate_property_offer
diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py
new file mode 100644
index 00000000000..c555f217028
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,45 @@
+from odoo import models, Command, fields
+
+
+class EstateProperty(models.Model):
+ _inherit = 'estate.property'
+
+ invoice_count = fields.Integer(compute='_compute_invoice_count', string="Invoice Count")
+
+ def action_mark_as_sold(self):
+ # Keep it at the beginning to trigger the validation first
+ result = super().action_mark_as_sold()
+
+ for record in self:
+ # It's valid to assume that there is one accepted offer (validated by the inherited entity)
+ accepted_offer = record.offer_ids.filtered(lambda offer: offer.status == 'accepted')[0]
+
+ account_move = self.env['account.move'].create({
+ 'partner_id': record.buyer_id.id,
+ 'move_type': 'out_invoice',
+ 'invoice_line_ids': [
+ Command.create({
+ 'name': "6% of selling price",
+ 'quantity': 1,
+ 'price_unit': record.selling_price * 0.06,
+ }),
+ Command.create({
+ 'name': "Administrative fees",
+ 'quantity': 1,
+ 'price_unit': 100.0,
+ }),
+ ],
+ })
+
+ accepted_offer.account_move_id = account_move
+
+ return result
+
+ def _compute_invoice_count(self):
+ for record in self:
+ record.invoice_count = len(record.offer_ids.account_move_id)
+
+ def action_view_invoices(self):
+ self.ensure_one()
+ invoices = self.offer_ids.account_move_id
+ return invoices._get_records_action()
diff --git a/estate_account/models/estate_property_offer.py b/estate_account/models/estate_property_offer.py
new file mode 100644
index 00000000000..5e0e47b2b63
--- /dev/null
+++ b/estate_account/models/estate_property_offer.py
@@ -0,0 +1,8 @@
+from odoo import models, fields
+
+
+class EstatePropertyOffer(models.Model):
+ _inherit = ['estate.property.offer']
+ _name = 'estate.property.offer'
+
+ account_move_id = fields.Many2one('account.move', string="Invoice count")
diff --git a/estate_account/static/description/icon.png b/estate_account/static/description/icon.png
new file mode 100644
index 00000000000..f30bf4ac371
Binary files /dev/null and b/estate_account/static/description/icon.png differ
diff --git a/estate_account/views/estate_property_views.xml b/estate_account/views/estate_property_views.xml
new file mode 100644
index 00000000000..9eb163cbfbf
--- /dev/null
+++ b/estate_account/views/estate_property_views.xml
@@ -0,0 +1,31 @@
+
+
+
+ estate.property.view.form.inherit
+ estate.property
+
+
+
+
+
+
+
+
+
+ estate.property.view.kanban.inherit
+ estate.property
+
+
+
+
+ Invoices
+
+
+
+
\ No newline at end of file