diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
diff --git a/README.md b/README.md
index 2e6514bff10..29e1a840775 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,4 @@
-# Odoo 12.0 - Technical Training
+# Odoo 16.0 - Technical Training
+
+The Technical Training of Odoo 16.0 is available on the
+[Tutorial](https://www.odoo.com/documentation/master/developer/howtos/rdtraining.html)
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/__manifest__.py b/__manifest__.py
new file mode 100644
index 00000000000..4fd3bca3358
--- /dev/null
+++ b/__manifest__.py
@@ -0,0 +1,16 @@
+{
+ 'name': 'Estate',
+ 'depends': ['base'],
+ 'installable': True,
+ 'application': True,
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/estate_property_views.xml',
+ 'views/estate_property_tag_views.xml',
+ 'views/estate_property_offer_views.xml',
+ 'views/estate_property_type_views.xml',
+ 'views/estate_menus.xml',
+ 'views/res_users_views.xml'
+ ],
+ 'license': 'LGPL-3'
+}
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..1f345fe6a75
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,11 @@
+{
+ "name": "Estate", # The name that will appear in the App list
+ "version": "16.0", # Version
+ "application": True, # This line says the module is an App, and not a module
+ "depends": ["base"], # dependencies
+ "data": [
+
+ ],
+ "installable": True,
+ 'license': 'LGPL-3',
+}
diff --git a/estate/models.py b/estate/models.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..9a2189b6382
--- /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
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
new file mode 100644
index 00000000000..bd144cc7aef
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,130 @@
+from odoo import models, fields, api
+from dateutil.relativedelta import relativedelta
+from odoo.exceptions import UserError
+from odoo.exceptions import ValidationError
+from odoo.tools.float_utils import float_compare, float_is_zero
+
+
+class EstateProperty(models.Model):
+ _name = "estate.property"
+ _description = "The real estate properties model"
+ _order = "id desc"
+
+ # Fields
+
+ name = fields.Char(required=True, string='Name')
+ description = fields.Text(
+ default='when duplicated, status and date are not copied')
+ postcode = fields.Char(help='this is the postcode')
+ date_availability = fields.Date(
+ copy=False, default=lambda self: fields.Datetime.now() + relativedelta(months=3))
+ expected_price = fields.Float(required=True)
+ selling_price = fields.Float(readonly=True, copy=False)
+ bedrooms = fields.Integer(default=2)
+ living_area = fields.Integer()
+ facades = fields.Integer()
+ garage = fields.Boolean()
+ garden = fields.Boolean()
+ garden_area = fields.Integer()
+ active = fields.Boolean(default=True)
+
+ # computed fields
+
+ total_area = fields.Float(compute="_compute_total_area")
+ best_price = fields.Float(
+ compute="_compute_best_offer", string='Best Offer', default=0)
+
+ # Selection fields
+
+ state = fields.Selection(
+ required=True,
+ copy=False,
+ default='new',
+ selection=[('new', 'New'), ('offer_received', 'Offer Received'),
+ ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('canceled', 'Canceled')],
+ )
+
+ garden_orientation = fields.Selection(
+ string='Garden Orientation',
+ selection=[('north', 'North'), ('south', 'South'),
+ ('east', 'East'), ('west', 'West')],
+ help="Garden Orientation: North, South, East and West."
+ )
+
+ # Relational Fields
+
+ property_type_id = fields.Many2one('estate.property.type')
+
+ # user_id = fields.Many2one('res.users')
+
+ property_tag_ids = fields.Many2many('estate.property.tag')
+
+ offer_ids = fields.One2many('estate.property.offer', 'property_id')
+
+ sales_person_id = fields.Many2one(
+ 'res.users', string='Salesperson', default=lambda self: self.env.user)
+
+ buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
+
+ # 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 strictly positive.'),
+ ]
+
+ @api.constrains('selling_price', 'expected_price')
+ def _check_selling_price(self):
+ for record in self:
+ if record.selling_price and record.selling_price < 0.9 * record.expected_price:
+ raise ValidationError("The selling price must be at least 90% of the expected price! You must reduce the epected price if you want to accept this offer.")
+
+ # compute methods
+
+ @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_offer(self):
+ for record in self:
+ record.best_price = max(
+ [offer.price for offer in record.offer_ids], default=0)
+
+ # onchange methods
+
+ @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 = ''
+
+ # Action Methods
+
+ def action_sold_property(self):
+ for record in self:
+ if record.state != 'canceled':
+ record.state = 'sold'
+ else:
+ raise UserError("Canceled properties cannot be sold.")
+
+ def action_cancel_property(self):
+ for record in self:
+ if record.state != 'sold':
+ record.state = 'canceled'
+ else:
+ raise UserError("Sold properties cannot be canceled.")
+
+ # CRUD Methods:
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_except_new_canceled(self):
+ if any(rec.state not in ('new', 'canceled') for rec in self):
+ raise UserError("only new and canceled 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..04964cafba9
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,86 @@
+from odoo import models, fields, api
+from dateutil.relativedelta import relativedelta
+from odoo.exceptions import UserError
+
+class EstatePropertyOffer(models.Model):
+ _name = 'estate.property.offer'
+ _description = "The property offer model"
+ _order = "price desc"
+
+
+ # Fields
+
+ price = fields.Float()
+ validity = fields.Integer(default=7)
+
+ # Selectoin Fields
+
+ status = fields.Selection(
+ copy=False,
+ selection=[('accepted', 'Accepted'), ('refused', 'Refused')]
+ )
+
+ # Relational Fields
+
+ partner_id = fields.Many2one('res.partner', required=True)
+ property_id = fields.Many2one('estate.property', required=True)
+
+ property_type_id = fields.Many2one(
+ 'estate.property.type', related='property_id.property_type_id', store=True)
+
+ # Compute Fields
+
+ deadline_date = fields.Date(
+ compute='_compute_deadline_date', inverse='_inverse_deadline_date')
+
+ # Constraints
+
+ _sql_constraints = [
+ ('check_offer_price', 'CHECK(price > 0)',
+ 'The offer price must be strictly positive.'),
+ ]
+
+ # Compute Methods
+
+ @api.depends("create_date", "validity")
+ def _compute_deadline_date(self):
+ for offer in self:
+ date = offer.create_date.date() if offer.create_date else fields.Date.today()
+ offer.deadline_date = date + relativedelta(days=offer.validity)
+
+ def _inverse_deadline_date(self):
+ for offer in self:
+ date = offer.create_date.date() if offer.create_date else fields.Date.today()
+ offer.validity = (offer.deadline_date - date).days
+
+ # Action Methods
+
+ def action_accept_offer(self):
+ for record in self:
+ offers = record.property_id.offer_ids
+ for offer in offers:
+ offer.action_refuse_offer()
+ record.status = 'accepted'
+ record.property_id.state = 'offer_accepted'
+ 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'
+ if record.status == 'accepted':
+ record.property_id.selling_price = None
+ record.property_id.buyer_id = None
+ return True
+
+ # CRUD Methods:
+
+ @api.model
+ def create(self, vals):
+ properties = self.env['estate.property'].browse(vals['property_id'])
+ properties.state = 'offer_received'
+ if vals['price'] < properties.best_price:
+ raise UserError(f"The offer must be higher than {properties.best_price}")
+ return super().create(vals)
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..13befdaa9d1
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,17 @@
+from odoo import models, fields
+
+
+class EstatePropertyTag(models.Model):
+ _name = 'estate.property.tag'
+ _description = "The property tag model"
+ _order = "tag_name"
+ _rec_name = 'tag_name'
+
+
+ tag_name = fields.Char(required=True)
+ color = fields.Integer()
+
+ _sql_constraints = [
+ ('name_uniq', 'UNIQUE (tag_name)',
+ 'The Tag name 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..d3c4319bb66
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,47 @@
+from odoo import models, fields, api
+
+class EstatePropertyType(models.Model):
+ _name = 'estate.property.type'
+ _description = "The property type model"
+ _order = "sequence"
+ _rec_name = 'property_type'
+
+ # Fields
+
+ property_type = fields.Char()
+
+ # Relational Fields
+
+ property_ids = fields.One2many('estate.property', 'property_type_id')
+
+ offer_ids = fields.One2many('estate.property.offer', 'property_type_id')
+
+ # Compute Fields
+
+ offer_count = fields.Integer(compute='_check_offer_count')
+
+ sequence = fields.Integer('Sequence', default=1,
+ help="Used to order stages. Lower is better.")
+
+ _sql_constraints = [
+ ('type_unique', 'UNIQUE (property_type)',
+ 'The Property type must be unique!')
+ ]
+
+ @api.depends('offer_ids')
+ def _check_offer_count(self):
+ for record in self:
+ record.offer_count = len(record.offer_ids)
+
+ # def view_offers(self):
+ # self.ensure_one()
+ # return {
+ # "type": "ir.actions.act_window",
+ # "name": "Property Offers",
+ # "res_model": "estate.property.offer",
+ # "view_mode": "tree,form"
+ # # "domain": [("change_id", "=", self.id)],
+ # # "context": "{'create': False}"
+ # }
+
+
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 00000000000..768caa81351
--- /dev/null
+++ b/estate/models/res_users.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+class RecUsers(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..6416b99401b
--- /dev/null
+++ b/estate/security/ir.model.access.csv
@@ -0,0 +1,9 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+
+estate.access_estate_property,access_estate_property,estate.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
\ 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..6f63e9ec2a5
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ 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..ecfcf5b7abf
--- /dev/null
+++ b/estate/views/estate_property_offer_views.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+ estate.property.offer.tree
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Property offers
+ estate.property.offer
+ tree,form
+ [('property_type_id', '=', active_id)]
+
+
+
+
\ 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..ea315bbba84
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ estate.property.tag.tree
+ estate.property.tag
+
+
+
+
+
+
+
+
+ Property Tags
+ estate.property.tag
+ tree,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..71f98519507
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
+
+ estate.property.type.tree
+ estate.property.type
+
+
+
+
+
+
+
+
+
+ Property Types
+ estate.property.type
+ tree,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..fde676ff4bc
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+ estate.property.tree
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.kanban
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Expected Price:
+
+
+ Selling Price:
+
+
+ Best Offer:
+
+
+
+
+
+
+
+
+
+
+ Properties
+ estate.property
+ tree,form,kanban
+ {'search_default_state': 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..dd941230824
--- /dev/null
+++ b/estate/views/res_users_views.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ res.users.form.view.inherit
+ 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..34f150bc6ae
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,9 @@
+{
+ 'name': 'Estate Account',
+ 'depends': ['base', 'estate', 'account'],
+ 'installable': True,
+ 'application': True,
+ 'data': [
+ ],
+ 'license': 'LGPL-3'
+}
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
new file mode 100644
index 00000000000..5e1963c9d2f
--- /dev/null
+++ b/estate_account/models/__init__.py
@@ -0,0 +1 @@
+from . import estate_property
diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py
new file mode 100644
index 00000000000..7ed9a682697
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,17 @@
+from odoo import fields, models, Command
+
+class EstateProperty(models.Model):
+ _inherit = "estate.property"
+
+ def action_sold_property(self):
+ invoice_lines = [
+ {"name": self.name, "quantity": 1, "price_unit": self.selling_price * (self.selling_price * 0.01)},
+ {"name": 'administrative fees', "quantity": 1, "price_unit": 100.00}
+ ]
+ invoice = {
+ 'partner_id': self.buyer_id.id,
+ 'move_type': 'out_invoice',
+ "invoice_line_ids": [line for line in invoice_lines],
+ }
+ self.env['account.move'].create(invoice)
+ return super().action_sold_property()