- hello world
+
+
+
+ Hello world:
+
+
+
+
Sum:
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js
new file mode 100644
index 00000000000..e414a72032d
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_item.js
@@ -0,0 +1,12 @@
+import { Component } from "@odoo/owl";
+
+
+export class TodoItem extends Component {
+ static template = "my_module.TodoItem";
+
+ static props = {
+ todo: { type: Object, shape: {id: Number, description: String, isCompleted: Boolean} },
+ toggleState: { type: Function },
+ removeTodo: { type: Function }
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml
new file mode 100644
index 00000000000..22bbeb8a8ca
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_item.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js
new file mode 100644
index 00000000000..70f426ac786
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.js
@@ -0,0 +1,38 @@
+import { Component, onMounted, useRef, useState } from "@odoo/owl";
+
+import { TodoItem } from "./todo_item"
+
+
+export class TodoList extends Component {
+ static template = "my_module.TodoList";
+ static components = { TodoItem };
+
+ setup() {
+ this.state = useState({ todos: [] });
+ this.taskCounter = 0;
+
+ this.inputRef = useRef('todo_list_input');
+ onMounted(() => {
+ this.inputRef.el.focus();
+ });
+ }
+
+ addTodo(ev) {
+ if (ev.keyCode === 13 && ev.srcElement.value != "") {
+ this.state.todos.push({id: this.taskCounter++, description: ev.srcElement.value, isCompleted: false});
+ ev.srcElement.value = "";
+ }
+ }
+
+ toggleTaskCompletion(toggled_item_id) {
+ var toggled_item = this.state.todos.find(todo_item => todo_item.id === toggled_item_id);
+ toggled_item.isCompleted = !toggled_item.isCompleted;
+ }
+
+ removeTask(task_id) {
+ const removal_index = this.state.todos.findIndex((todo_item) => todo_item.id === task_id);
+ if (removal_index >= 0) {
+ this.state.todos.splice(removal_index, 1);
+ }
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml
new file mode 100644
index 00000000000..a2a958a0c9b
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
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..5bba2abdc0e
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,21 @@
+{
+ 'name': 'estate',
+ 'depends': [
+ 'base',
+ ],
+ 'application': True,
+ 'data': [
+ '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',
+ 'security/ir.model.access.csv',
+ 'data/estate.property.type.csv',
+ ],
+ 'demo': [
+ 'demo/estate.property.xml',
+ 'demo/estate.property.offer.xml',
+ ],
+}
diff --git a/estate/data/estate.property.type.csv b/estate/data/estate.property.type.csv
new file mode 100644
index 00000000000..4d0d7a60f78
--- /dev/null
+++ b/estate/data/estate.property.type.csv
@@ -0,0 +1,5 @@
+"id","name"
+property_type_residential,"Residential"
+property_type_commercial,"Commercial"
+property_type_industrial,"Industrial"
+property_type_land,"Land"
diff --git a/estate/demo/estate.property.offer.xml b/estate/demo/estate.property.offer.xml
new file mode 100644
index 00000000000..4466beb42b4
--- /dev/null
+++ b/estate/demo/estate.property.offer.xml
@@ -0,0 +1,34 @@
+
+
+
+
+ 10000
+ 14
+
+
+
+
+
+
+ 1500000
+ 14
+
+
+
+
+
+
+ 1500001
+ 14
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/demo/estate.property.xml b/estate/demo/estate.property.xml
new file mode 100644
index 00000000000..626ba6ba0b4
--- /dev/null
+++ b/estate/demo/estate.property.xml
@@ -0,0 +1,60 @@
+
+
+ Big Villa
+ new
+ A nice and big villa
+ 12345
+ 2020-02-02
+ 1600000
+ 6
+ 100
+ 4
+ True
+ True
+ 100000
+ south
+
+
+
+
+ Trailer home
+ cancelled
+ Home in a trailer park
+ 54321
+ 1970-01-01
+ 100000
+ 120000
+ 1
+ 10
+ 4
+ False
+
+
+
+
+ Storefront
+ new
+ Storefront in a market
+ 5050
+ 1990-07-08
+ 1000000
+ 0
+ 50
+ 2
+ False
+
+
+
+
+
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..a64b0799dc3
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,109 @@
+import datetime
+from dateutil.relativedelta import relativedelta
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools.float_utils import float_compare, float_is_zero
+
+
+class EstateProperty(models.Model):
+ _name = "estate.property"
+ _description = "Real estate property model"
+ _order = "id desc"
+
+ name = fields.Char(required=True, string="Title")
+ description = fields.Text()
+ postcode = fields.Char()
+ date_availability = fields.Date(copy=False, default=datetime.date.today() + relativedelta(months=3), string="Available From")
+ expected_price = fields.Float(required=True)
+ selling_price = fields.Float(readonly=True, copy=False)
+ bedrooms = fields.Integer(default=2)
+ living_area = fields.Integer(string="Living Area (sqm)")
+ facades = fields.Integer()
+ garage = fields.Boolean()
+ garden = fields.Boolean()
+ garden_area = fields.Integer(string="Garden Area (sqm)")
+ garden_orientation = fields.Selection(
+ selection=[
+ ('north', 'North'),
+ ('south', 'South'),
+ ('east', 'East'),
+ ('west', 'West'),
+ ]
+ )
+ active = fields.Boolean(default=True)
+ state = fields.Selection(
+ required=True,
+ default="new",
+ copy=False,
+ string="status",
+ selection=[
+ ('new', 'New'),
+ ('offer_received', 'Offer Received'),
+ ('offer_accepted', 'Offer Accepted'),
+ ('sold', 'Sold'),
+ ('cancelled', 'Cancelled')
+ ],
+ )
+ property_type_id = fields.Many2one("estate.property.type", string="Property type")
+ buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
+ salesman_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user)
+ property_tag_ids = fields.Many2many("estate.property.tag", string="Tags")
+ offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
+ total_area = fields.Float(compute="_compute_total_area")
+ best_price = fields.Float(compute="_compute_best_price", string="Best Offer")
+
+ _check_expected_price_positive = models.Constraint(
+ 'CHECK(expected_price > 0)',
+ 'The expected price of a property cannot be negative',
+ )
+
+ _check_selling_price_positive = models.Constraint(
+ 'CHECK(selling_price >= 0)',
+ 'The selling price of a property cannot be negative',
+ )
+
+ @api.constrains('selling_price', 'expected_price')
+ def _check_selling_to_expected_price_ratio(self):
+ for record in self:
+ if float_compare(record.selling_price, 0.9 * record.expected_price, 2) == -1 and not float_is_zero(record.selling_price, 2):
+ raise ValidationError("Selling price cannot be less than 90% of expected price")
+
+ @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')
+ def _compute_best_price(self):
+ for record in self:
+ record.best_price = max(record.offer_ids.mapped("price")) if record.offer_ids else 0
+
+ @api.onchange('garden')
+ def _onchange_garden(self):
+ self.garden_area = 10 if self.garden else 0
+ self.garden_orientation = 'north' if self.garden else None
+
+ def action_mark_as_sold(self):
+ for record in self:
+ if record.state == 'cancelled':
+ raise UserError("A Cancelled property cannot be sold")
+ elif not record.offer_ids:
+ raise UserError("A property without offers cannot be sold")
+ else:
+ record.state = 'sold'
+ return True
+
+ def action_mark_as_cancelled(self):
+ for record in self:
+ if record.state == 'sold':
+ raise UserError("A sold property cannot be cancelled")
+ else:
+ record.state = 'cancelled'
+ return True
+
+ @api.ondelete(at_uninstall=False)
+ def prevent_deletion_if_not_new_or_cancelled(self):
+ for record in self:
+ if record.state not in ['new', 'cancelled']:
+ raise UserError("Only New or 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..fdbd3a5f52f
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,65 @@
+import datetime
+from dateutil.relativedelta import relativedelta
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _description = "A property offer is an amount a potential buyer offers to the seller"
+ _order = "price desc"
+
+ price = fields.Float()
+ status = fields.Selection(copy=False,
+ selection=[
+ ('accepted', 'Accepted'),
+ ('refused', 'Refused'),
+ ]
+ )
+ partner_id = fields.Many2one("res.partner", required=True)
+ property_id = fields.Many2one("estate.property", required=True)
+ validity = fields.Integer(default=7)
+ date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline", string="Deadline")
+ property_type_id = fields.Many2one("estate.property.type", related="property_id.property_type_id", store=True, string="Property type")
+ active = fields.Boolean(default=True)
+
+ _check_offer_price_positive = models.Constraint(
+ 'CHECK(price > 0)',
+ 'The offer price of a property cannot be negative',
+ )
+
+ @api.depends('create_date', 'validity')
+ def _compute_date_deadline(self):
+ for record in self:
+ creation_date = record.create_date or datetime.date.today()
+ record.date_deadline = creation_date + relativedelta(days=record.validity)
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ record.validity = record.date_deadline.day - record.create_date.day
+
+ def action_accept_offer(self):
+ for record in self:
+ if 'accepted' in record.property_id.offer_ids.mapped("status"):
+ raise UserError("Another offer has already been accepted for this property")
+ record.status = 'accepted'
+ record.property_id.state = 'offer_accepted'
+ record.property_id.buyer_id = record.partner_id
+ record.property_id.selling_price = record.price
+
+ def action_refuse_offer(self):
+ for record in self:
+ record.status = 'refused'
+
+ def create(self, vals_list):
+ for vals in vals_list:
+ property_record = self.env["estate.property"].browse(vals["property_id"])
+ if property_record.state == 'sold':
+ raise UserError('Offers cannot be created for sold properties')
+ if property_record.offer_ids and vals["price"] < max(property_record.offer_ids.mapped("price")):
+ raise UserError("Newly created offer price must not be lower than the current best offer")
+ else:
+ property_record.state = 'offer_received'
+ 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..e8e3c079cf8
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,15 @@
+from odoo import fields, models
+
+
+class EstatePropertyTag(models.Model):
+ _name = "estate.property.tag"
+ _description = "Tags describing the property such as 'cozy' and 'renovated'"
+ _order = "name"
+
+ name = fields.Char(required=True)
+ color = fields.Integer()
+
+ _tag_name_unique = models.Constraint(
+ 'UNIQUE(name)',
+ 'Tag names 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..cbc1fd92240
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,23 @@
+from odoo import api, fields, models
+
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.type"
+ _description = "Estate property type such as 'house'"
+ _order = "name"
+
+ name = fields.Char(required=True)
+ 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")
+
+ @api.depends("offer_ids")
+ def _compute_offer_count(self):
+ for record in self:
+ record.offer_count = len(record.offer_ids)
+
+ _property_type_name_unique = models.Constraint(
+ 'UNIQUE(name)',
+ 'Property type names must be unique',
+ )
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 00000000000..ecae1ef0fa1
--- /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",
+ "salesman_id",
+ string="Real Estate Properties",
+ domain=['|', ('state', '=', 'new'), ('state', '=', 'offer_received')]
+ )
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..89f97c50842
--- /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
+access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
+access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
+access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1
diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py
new file mode 100644
index 00000000000..dfd37f0be11
--- /dev/null
+++ b/estate/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_estate
diff --git a/estate/tests/test_estate.py b/estate/tests/test_estate.py
new file mode 100644
index 00000000000..f9918616d62
--- /dev/null
+++ b/estate/tests/test_estate.py
@@ -0,0 +1,59 @@
+from odoo.exceptions import UserError
+from odoo.tests import Form, tagged
+from odoo.tests.common import TransactionCase
+
+
+@tagged('post_install', '-at_install')
+class EstateTestCase(TransactionCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(EstateTestCase, cls).setUpClass()
+
+ cls.property = cls.env['estate.property'].create({
+ 'name': 'property',
+ 'state': 'new',
+ 'expected_price': 100000,
+ })
+
+ def test_prevent_offer_creation_on_property(self):
+ self.property.state = 'sold'
+ self.assertEqual(self.property.state, 'sold')
+
+ with self.assertRaises(UserError):
+ self.property.offer_ids.create([{
+ 'partner_id': self.env.uid,
+ 'property_id': self.property.id,
+ 'price': 100000,
+ }])
+
+ def test_prevent_sale_without_offer(self):
+ self.assertEqual(len(self.property.offer_ids), 0, 'Property should not have any offers')
+ with self.assertRaises(UserError):
+ self.property.action_mark_as_sold()
+
+ def test_mark_property_as_sold(self):
+ self.property.offer_ids.create([{
+ 'partner_id': self.env.uid,
+ 'property_id': self.property.id,
+ 'price': 100000,
+ }])
+ self.property.action_mark_as_sold()
+ self.assertEqual(self.property.state, 'sold', 'Property should have been updated to sold state')
+
+ def test_property_form_garden_reset(self):
+ form = Form(self.env['estate.property'])
+ form.name = 'Garden Test Property'
+ form.expected_price = 10000
+
+ form.garden = True
+ property_with_garden = form.save()
+ self.assertEqual(property_with_garden.garden, True)
+ self.assertEqual(property_with_garden.garden_area, 10)
+ self.assertEqual(property_with_garden.garden_orientation, 'north')
+
+ form.garden = False
+ property_without_garden = form.save()
+ self.assertEqual(property_without_garden.garden, False)
+ self.assertEqual(property_without_garden.garden_area, 0)
+ self.assertEqual(property_without_garden.garden_orientation, False)
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
new file mode 100644
index 00000000000..f3973e31ac3
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
new file mode 100644
index 00000000000..899883b50a1
--- /dev/null
+++ b/estate/views/estate_property_offer_views.xml
@@ -0,0 +1,41 @@
+
+
+
+ Offers
+ estate.property.offer
+ [('property_type_id', '=', active_id)]
+ list,form
+
+
+
+ estate.property.offer.list
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.form
+ estate.property.offer
+
+
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
new file mode 100644
index 00000000000..6733dd5ea8a
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,18 @@
+
+
+
+ Property Tags
+ estate.property.tag
+ list,form
+
+
+
+ estate.property.tag.list
+ estate.property.tag
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..6407e0557ae
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,48 @@
+
+
+
+ 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..ace8d2abf37
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,127 @@
+
+
+
+ Properties
+ estate.property
+ list,form,kanban
+ {'search_default_available': True}
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.kanban
+ estate.property
+
+
+
+
+
+
+ Expected Price:
+
+ Best Price:
+
+
+ Selling Price:
+
+
+
+
+
+
+
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml
new file mode 100644
index 00000000000..a95c6746a54
--- /dev/null
+++ b/estate/views/res_users_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ res.users.view.form.inherit.estate
+ 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..7bdc01a863c
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,7 @@
+{
+ 'name': 'estate_account',
+ 'depends': [
+ 'account',
+ 'estate',
+ ],
+}
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..ad8bdba341c
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,29 @@
+from odoo import Command, models
+
+
+class EstateProperty(models.Model):
+ _inherit = ["estate.property"]
+
+ def action_mark_as_sold(self):
+ super().action_mark_as_sold()
+ for record in self:
+ if record.state == 'cancelled':
+ continue
+
+ self.env['account.move'].create({
+ "partner_id": record.buyer_id.id,
+ "move_type": "out_invoice",
+ "line_ids": [
+ Command.create({
+ "name": "6% of selling price",
+ "quantity": 1,
+ "price_unit": 0.06 * record.selling_price
+ }),
+ Command.create({
+ "name": "Administrative fees",
+ "quantity": 1,
+ "price_unit": 100
+ })
+ ]
+ })
+ return True
diff --git a/importable_estate/__init__.py b/importable_estate/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/importable_estate/__manifest__.py b/importable_estate/__manifest__.py
new file mode 100644
index 00000000000..0a48da2bdb4
--- /dev/null
+++ b/importable_estate/__manifest__.py
@@ -0,0 +1,9 @@
+{
+ 'name': 'estate',
+ 'depends': [
+ 'base',
+ ],
+ 'data': [
+ 'models/estate_property.xml',
+ ]
+}
diff --git a/importable_estate/models/estate_property.xml b/importable_estate/models/estate_property.xml
new file mode 100644
index 00000000000..3f97478bd8f
--- /dev/null
+++ b/importable_estate/models/estate_property.xml
@@ -0,0 +1,22 @@
+
+
+ Real Estate Property
+ x_estate.property
+
+
+
+
+ x_name
+ Property name
+ char
+ True
+
+
+
+
+ x_expected_price
+ Expected price
+ integer
+ True
+
+