From 15bd461c8141b3646c6c8f985819584941efae8b Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Mon, 16 Feb 2026 17:26:41 +0100 Subject: [PATCH 01/16] [ADD] estate: property model with fields, views, and menu access - Add estate_property model with fields - Add active field with default True - Set date_availability default to 3 months from today - Create list, form, and search views for estate.property - Add search filters and group by postcode - Configure access rights in ir.model.access.csv --- estate/__init__.py | 1 + estate/__manifest__.py | 15 +++++ estate/models/__init__.py | 2 + estate/models/estate_property.py | 45 +++++++++++++ estate/security/ir.model.access.csv | 3 + estate/views/estate_property_menus.xml | 9 +++ estate/views/estate_property_views.xml | 92 ++++++++++++++++++++++++++ 7 files changed, 167 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_property_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..8f013a95959 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,15 @@ + +{ + 'name': 'estate', + 'depends': [ + 'base', + ], + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + # "views/estate_property_type_views.xml", + "views/estate_property_menus.xml", + # "views/estate_property_type_menus.xml", + ], + 'application': True, +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..8e433062639 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,2 @@ +from . import estate_property +# from . import estate_property_type \ 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..9a62783aa54 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,45 @@ +from odoo import fields, models + +def get_date_in_3_months(): + ''' + This function calculates the date that is three months from the current date. + + returns: + A date object representing the date that is three months from today. + ''' + today_data = fields.Date.today() + three_months_later = fields.Date.add(today_data, months=3) + return three_months_later + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property Model" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date(copy=False, default=get_date_in_3_months()) + 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() + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ]) + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Canceled'), + ], default="new", required=True, copy=False) + active = fields.Boolean(default=True) + + property_type_id = fields.Many2one("estate.property.type", string="Property Type") \ 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..db554fd92fd --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,3 @@ +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 \ No newline at end of file diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml new file mode 100644 index 00000000000..c4614c18783 --- /dev/null +++ b/estate/views/estate_property_menus.xml @@ -0,0 +1,9 @@ + + + + + + \ 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..758e092cffb --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,92 @@ + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + + + + Properties + estate.property + list,form + +

+ Define a new estate property to sell or rent. +

+ You can specify the expected price and the selling/renting price, the buyer/tenant, the salesperson, and the status of the property. +

+
+
+
\ No newline at end of file From 8fa7278f53150741fd41e35465c73c8493fc4fe9 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Tue, 17 Feb 2026 10:10:42 +0100 Subject: [PATCH 02/16] [IMP] estate: property types, tags, and offers * Add estate.property.type model with form view and Settings menu * Add estate.property.tag model with form view and Settings menu * Add estate.property.offer model with form/list views for tracking offers * Update estate.property model with: - property_type_id: Many2one relation to property types - tag_ids: Many2many relation to property tags - offer_ids: One2many relation to offers - buyer_id and salesperson_id fields * Update property form view to display tags, type, offers, buyer, and salesperson * Reorganize menus: Advertisements and Settings submenus * Add security access rules for all new models --- estate/__manifest__.py | 7 ++-- estate/models/__init__.py | 4 ++- estate/models/estate_property.py | 8 +++-- estate/models/estate_property_offer.py | 10 ++++++ estate/models/estate_property_tag.py | 8 +++++ estate/models/estate_property_type.py | 8 +++++ estate/security/ir.model.access.csv | 4 ++- estate/views/estate_property_offers_views.xml | 33 +++++++++++++++++++ estate/views/estate_property_tag_menus.xml | 4 +++ estate/views/estate_property_tag_views.xml | 31 +++++++++++++++++ estate/views/estate_property_type_menus.xml | 6 ++++ estate/views/estate_property_type_views.xml | 31 +++++++++++++++++ estate/views/estate_property_views.xml | 12 ++++++- 13 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/views/estate_property_offers_views.xml create mode 100644 estate/views/estate_property_tag_menus.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_menus.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8f013a95959..fdea66cd571 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -7,9 +7,12 @@ "data": [ "security/ir.model.access.csv", "views/estate_property_views.xml", - # "views/estate_property_type_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offers_views.xml", "views/estate_property_menus.xml", - # "views/estate_property_type_menus.xml", + "views/estate_property_type_menus.xml", + "views/estate_property_tag_menus.xml", ], 'application': True, } diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 8e433062639..09b2099fe84 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,2 +1,4 @@ from . import estate_property -# from . import estate_property_type \ No newline at end of file +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 9a62783aa54..dd06aefaaa8 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,6 +1,6 @@ from odoo import fields, models -def get_date_in_3_months(): +def get_date_in_3_months() -> fields.Date: ''' This function calculates the date that is three months from the current date. @@ -42,4 +42,8 @@ class EstateProperty(models.Model): ], default="new", required=True, copy=False) active = fields.Boolean(default=True) - property_type_id = fields.Many2one("estate.property.type", string="Property Type") \ No newline at end of file + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..8caf5d371be --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,10 @@ +from odoo import models, fields + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer' + + price = fields.Float(string='Price') + status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', copy=False) + partner_id = fields.Many2one('res.partner', string='Partner') + property_id = fields.Many2one('estate.property', string='Property') \ 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..e63d6d7a781 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,8 @@ +from odoo import fields, models + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag Model" + + name = fields.Char(required=True) + \ 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..228eb2b0e30 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type Model" + + name = fields.Char(required=True) + \ No newline at end of file diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index db554fd92fd..c79331f2f1c 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,3 +1,5 @@ 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 \ No newline at end of file +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_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 00000000000..7d332d91e7c --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,33 @@ + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + +
+
+
+ + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_tag_menus.xml b/estate/views/estate_property_tag_menus.xml new file mode 100644 index 00000000000..4869a0384ed --- /dev/null +++ b/estate/views/estate_property_tag_menus.xml @@ -0,0 +1,4 @@ + + + + \ 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..b40144216b4 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,31 @@ + + + + estate.property.tag.form + estate.property.tag + +
+ +

+ +

+
+
+
+
+ + + + + Property Tags + estate.property.tag + list,form + +

+ Define a new estate property tag. +

+ You can specify the name of each property tag. +

+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_type_menus.xml b/estate/views/estate_property_type_menus.xml new file mode 100644 index 00000000000..80513ee0adb --- /dev/null +++ b/estate/views/estate_property_type_menus.xml @@ -0,0 +1,6 @@ + + + + + + \ 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..474ad7ef0fb --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,31 @@ + + + + estate.property.type.form + estate.property.type + +
+ +

+ +

+
+
+
+
+ + + + + Property Types + estate.property.type + list,form + +

+ Define a new estate property type. +

+ You can specify the name and description of each property type. +

+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 758e092cffb..5246797285b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -30,8 +30,10 @@

+ + @@ -51,7 +53,15 @@ - + + + + + + + + + From fa5ac28485af584e0a0fa7fcbcce6b746e09d8c5 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Tue, 17 Feb 2026 11:13:05 +0100 Subject: [PATCH 03/16] [IMP] estate: computed fields and onchange methods * estate.property: - Add total_area computed field (living_area + garden_area) - Add best_offer computed field (max of offer prices) - Add onchange method for garden field to auto-populate garden_area and orientation * estate.property.offer: - Add validity field (days, default 7) - Add date_deadline computed field based on create_date + validity - Add inverse method to update validity when deadline is changed * Update views to display new computed fields (total_area, best_offer) * Update offer views to include validity and date_deadline fields --- estate/models/estate_property.py | 31 +++++++++++++++++-- estate/models/estate_property_offer.py | 19 ++++++++++-- estate/views/estate_property_offers_views.xml | 7 +++-- estate/views/estate_property_views.xml | 2 ++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index dd06aefaaa8..f33b93983c6 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models def get_date_in_3_months() -> fields.Date: ''' @@ -41,9 +41,36 @@ class EstateProperty(models.Model): ('canceled', 'Canceled'), ], default="new", required=True, copy=False) active = fields.Boolean(default=True) + + total_area = fields.Integer(compute="_compute_total_area") + best_offer = fields.Float(compute="_compute_best_offer", string="Best Offer") property_type_id = fields.Many2one("estate.property.type", string="Property Type") salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user) buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) tag_ids = fields.Many2many("estate.property.tag", string="Tags") - offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") \ No newline at end of file + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + + @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: + # If the property has offers, the best offer is the maximum of the expected prices of the offers. Otherwise, it is 0. + if record.offer_ids: + record.best_offer = max(record.offer_ids.mapped("price")) + else: + record.best_offer = 0.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 \ No newline at end of file diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 8caf5d371be..769f32d30bf 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import models, fields, api class EstatePropertyOffer(models.Model): _name = 'estate.property.offer' @@ -7,4 +7,19 @@ class EstatePropertyOffer(models.Model): price = fields.Float(string='Price') status = fields.Selection([('accepted', 'Accepted'), ('refused', 'Refused')], string='Status', copy=False) partner_id = fields.Many2one('res.partner', string='Partner') - property_id = fields.Many2one('estate.property', string='Property') \ No newline at end of file + property_id = fields.Many2one('estate.property', string='Property') + validity = fields.Integer(string='Validity (days)', default=7) + date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline') + + @api.depends('validity', 'create_date') + def _compute_date_deadline(self): + for offer in self: + start = fields.Date.to_date(offer.create_date) or fields.Date.today() + offer.date_deadline = fields.Date.add(start, days=offer.validity) + + def _inverse_date_deadline(self): + for offer in self: + start = fields.Date.to_date(offer.create_date) or fields.Date.today() + if offer.date_deadline and start: + delta = offer.date_deadline - start + offer.validity = delta.days \ No newline at end of file diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml index 7d332d91e7c..32e643a4a32 100644 --- a/estate/views/estate_property_offers_views.xml +++ b/estate/views/estate_property_offers_views.xml @@ -8,8 +8,10 @@ - + + + @@ -23,8 +25,9 @@ - + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 5246797285b..e843946ba8b 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -39,6 +39,7 @@ + @@ -53,6 +54,7 @@ + From 06ea4370c815a73e3df85ae3295977e5bf421a91 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Tue, 17 Feb 2026 11:44:04 +0100 Subject: [PATCH 04/16] [IMP] estate: action methods and offer management buttons * estate.property: - Add action_set_sold() method to mark property as sold - Add action_cancel() method to cancel property - Add validation: prevent selling canceled properties - Add validation: prevent canceling sold properties * estate.property.offer: - Add accept_offer() method: sets status to accepted, updates selling price, property state, and buyer - Add refuse_offer() method: sets status to refused, updates property state * Update views: - Add "Mark as Sold" and "Cancel" buttons to property form header - Add accept/refuse buttons (icons) to offer list view --- estate/models/estate_property.py | 19 ++++++++++++++++++- estate/models/estate_property_offer.py | 15 ++++++++++++++- estate/views/estate_property_offers_views.xml | 3 +++ estate/views/estate_property_views.xml | 4 ++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index f33b93983c6..c90795bb704 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError def get_date_in_3_months() -> fields.Date: ''' @@ -51,6 +52,7 @@ class EstateProperty(models.Model): tag_ids = fields.Many2many("estate.property.tag", string="Tags") offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + @api.depends("living_area", "garden_area") def _compute_total_area(self): for record in self: @@ -73,4 +75,19 @@ def _onchange_garden(self): self.garden_orientation = 'north' else: self.garden_area = 0 - self.garden_orientation = False \ No newline at end of file + self.garden_orientation = False + + + def action_set_sold(self): + for record in self: + if record.state == "canceled": + raise UserError("Canceled properties cannot be sold.") + record.state = "sold" + return True + + def action_cancel(self): + for record in self: + if record.state == "sold": + raise UserError("Sold properties cannot be canceled.") + record.state = "canceled" + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 769f32d30bf..922414d7acd 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -22,4 +22,17 @@ def _inverse_date_deadline(self): start = fields.Date.to_date(offer.create_date) or fields.Date.today() if offer.date_deadline and start: delta = offer.date_deadline - start - offer.validity = delta.days \ No newline at end of file + offer.validity = delta.days + + def accept_offer(self): + for offer in self: + offer.status = 'accepted' + offer.property_id.selling_price = offer.price + offer.property_id.state = 'offer_accepted' + offer.property_id.buyer_id = offer.partner_id + + def refuse_offer(self): + for offer in self: + offer.status = 'refused' + if offer.property_id.state != 'offer_accepted': + offer.property_id.state = 'offer_received' \ No newline at end of file diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml index 32e643a4a32..488d2a71104 100644 --- a/estate/views/estate_property_offers_views.xml +++ b/estate/views/estate_property_offers_views.xml @@ -28,6 +28,9 @@ + +

+ + + + + + +
- + + estate.property.type.list + estate.property.type + + + + + + + + Property Types @@ -28,4 +58,5 @@

+ diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 252a8e6c220..24287d55d72 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -10,7 +10,7 @@ - + @@ -27,17 +27,19 @@
-

- + + - + @@ -56,13 +58,13 @@ - - + + - + @@ -80,15 +82,19 @@ estate.property.list estate.property - + + + - - + @@ -97,6 +103,7 @@ Properties estate.property list,form + {'search_default_available': 1}

Define a new estate property to sell or rent. From b5dc9949565c21255d6b8e6c5883ed3d7acce00e Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Wed, 18 Feb 2026 10:34:15 +0100 Subject: [PATCH 11/16] [CLN] estate: clean up code to comply with linting standards - Remove trailing whitespace - Remove unnecessary blank lines - Add missing newline at end of files --- estate/models/estate_property.py | 13 +++++++------ estate/models/estate_property_offer.py | 7 ++++--- estate/models/estate_property_tag.py | 1 + estate/models/estate_property_type.py | 3 ++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 3299db37d64..d605050b99d 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -2,6 +2,7 @@ from odoo.exceptions import UserError from odoo.tools.float_utils import float_compare, float_is_zero + def get_date_in_3_months() -> fields.Date: ''' This function calculates the date that is three months from the current date. @@ -13,15 +14,16 @@ def get_date_in_3_months() -> fields.Date: three_months_later = fields.Date.add(today_data, months=3) return three_months_later + class EstateProperty(models.Model): _name = "estate.property" _description = "Estate Property Model" _order = "id desc" - + name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date(copy=False, default= lambda self: get_date_in_3_months()) + date_availability = fields.Date(copy=False, default=lambda self: get_date_in_3_months()) expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) @@ -44,7 +46,7 @@ class EstateProperty(models.Model): ('canceled', 'Canceled'), ], default="new", required=True, copy=False) active = fields.Boolean(default=True) - + total_area = fields.Integer(compute="_compute_total_area") best_offer = fields.Float(compute="_compute_best_offer", string="Best Offer") @@ -55,7 +57,7 @@ class EstateProperty(models.Model): offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") _check_expected_price_pos = models.Constraint( - 'CHECK(expected_price > 0)', + 'CHECK(expected_price > 0)', 'The expected price must be strictly positive.' ) @@ -73,7 +75,7 @@ def _compute_total_area(self): def _compute_best_offer(self): for record in self: # If the property has offers, the best offer is the maximum of the expected prices of the offers. Otherwise, it is 0. - if record.offer_ids: + if record.offer_ids: record.best_offer = max(record.offer_ids.mapped("price")) else: record.best_offer = 0.0 @@ -87,7 +89,6 @@ def _onchange_garden(self): self.garden_area = 0 self.garden_orientation = False - @api.constrains("selling_price", "expected_price") def _check_selling_price_vs_expected_price(self): for record in self: diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 97fa1f1fb0f..c3b68834a08 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,6 +1,7 @@ from odoo import models, fields, api from odoo.exceptions import UserError + class EstatePropertyOffer(models.Model): _name = 'estate.property.offer' _description = 'Estate Property Offer' @@ -12,7 +13,7 @@ class EstatePropertyOffer(models.Model): property_id = fields.Many2one('estate.property', string='Property') validity = fields.Integer(string='Validity (days)', default=7) date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline') - + property_type_id = fields.Many2one( 'estate.property.type', related='property_id.property_type_id', @@ -23,7 +24,7 @@ class EstatePropertyOffer(models.Model): 'CHECK(price > 0)', 'The offer price must be strictly positive.' ) - + @api.depends('validity', 'create_date') def _compute_date_deadline(self): for offer in self: @@ -42,7 +43,7 @@ def accept_offer(self): # Check no other offer has been accepted for the same property if offer.property_id.offer_ids.filtered(lambda o: o.status == 'accepted'): raise UserError('Another offer has already been accepted for this property.') - + # Accept the offer offer.status = 'accepted' offer.property_id.selling_price = offer.price diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 8d8bc54592d..2d2cbfe7219 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -1,5 +1,6 @@ from odoo import fields, models + class EstatePropertyTag(models.Model): _name = "estate.property.tag" _description = "Estate Property Tag Model" diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index de66c2ed357..de28038b838 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,5 +1,6 @@ from odoo import fields, models, api + class EstatePropertyType(models.Model): _name = "estate.property.type" _description = "Estate Property Type Model" @@ -21,4 +22,4 @@ class EstatePropertyType(models.Model): @api.depends("offers_ids") def _compute_offers_count(self): for record in self: - record.offers_count = len(record.offers_ids) \ No newline at end of file + record.offers_count = len(record.offers_ids) From c62c4eb6f96ff8d53f66e12fe0a08e605f7579d9 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Wed, 18 Feb 2026 11:45:28 +0100 Subject: [PATCH 12/16] [IMP] estate: add business logic to CRUD and extend users view Control the property CRUD operations by adding buisness logic, blocking deletions and checking offer prices. We also updated the user profile so salespeople can see and manage all their properties in one place. - Prevent deletion of properties unless state is 'New' or 'Cancelled' using the 'ondelete' decorator. - Update property state to 'Offer Received' upon offer creation. - Raise a validation error if a new offer price is lower than existing offers. - Extend 'res.users' to include a list of properties assigned to the salesperson via a One2many field with a domain for available properties. - Inherit the users form view to display assigned properties in a new notebook page. --- estate/__manifest__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 12 ++++++++++++ estate/models/estate_property_offer.py | 16 ++++++++++++++++ estate/models/estate_sales_person.py | 7 +++++++ estate/views/estate_sales_person_views.xml | 21 +++++++++++++++++++++ 6 files changed, 58 insertions(+) create mode 100644 estate/models/estate_sales_person.py create mode 100644 estate/views/estate_sales_person_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index ab0d414a156..a62702960d0 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,6 +12,7 @@ "views/estate_property_type_views.xml", "views/estate_property_tag_views.xml", "views/estate_property_offers_views.xml", + "views/estate_sales_person_views.xml", "views/estate_property_menus.xml", "views/estate_property_type_menus.xml", "views/estate_property_tag_menus.xml", diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 2f1821a39c1..0cdcf5296e6 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,3 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer +from . import estate_sales_person diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index d605050b99d..cc8ae6435fb 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -108,3 +108,15 @@ def action_set_cancel(self): raise UserError("Sold properties cannot be canceled.") record.state = "canceled" return True + + @api.ondelete(at_uninstall=False) + def _unlink_if_new_or_canceled(self): + for record in self: + if record.state not in ("new", "canceled"): + raise UserError("Only properties in 'new' or 'canceled' state can be deleted.") + + def check_has_higher_offer(self, price): + for offer in self.offer_ids: + if float_compare(offer.price, price, precision_digits=2) > 0: + return True + return False diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index c3b68834a08..2c1b50971de 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -60,3 +60,19 @@ def refuse_offer(self): offer.status = 'refused' if offer.property_id.state != 'offer_accepted': offer.property_id.state = 'offer_received' + + @api.model + def create(self, vals_list): + # Check that the offer price is higher than the current best offer for the property + for val in vals_list: + property_id = val.get('property_id') + has_higher_offer = self.env['estate.property'].browse(property_id).check_has_higher_offer(val.get('price', 0)) + if has_higher_offer: + raise UserError('The offer price must be higher than the current best offer for the property.') + + # Create the offer + offer = super().create(vals_list) + if offer.property_id.state == 'new': + offer.property_id.state = 'offer_received' + + return offer diff --git a/estate/models/estate_sales_person.py b/estate/models/estate_sales_person.py new file mode 100644 index 00000000000..e771f21225f --- /dev/null +++ b/estate/models/estate_sales_person.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class EstateSalesPerson(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", "salesperson_id", string="Estate Properties", domain=[('state', 'in', ['new', 'offer_received'])]) diff --git a/estate/views/estate_sales_person_views.xml b/estate/views/estate_sales_person_views.xml new file mode 100644 index 00000000000..dc4145da5d2 --- /dev/null +++ b/estate/views/estate_sales_person_views.xml @@ -0,0 +1,21 @@ + + + + estate.salesperson.form + res.users + + + + + + + + + + + + + + + + \ No newline at end of file From 2eb4dc848ce0d0e431ccaeb9dd1a034c03d612b8 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Wed, 18 Feb 2026 13:44:06 +0100 Subject: [PATCH 13/16] [ADD] estate_account: new module to generate invoices on property sale This module extends the estate property functionality by automatically creating a customer invoice when a property is marked as sold. - Add `estate_account` module inheriting from `estate.property` - Override `action_set_sold` to auto-generate a customer invoice containing: - 6% commission fee based on the property selling price - Fixed administration fee of 100 --- estate_account/__init__.py | 1 + estate_account/__manifest__.py | 14 +++++++++++++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 26 ++++++++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py 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..089740e1bdf --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,14 @@ + +{ + 'name': 'estate_account', + 'depends': [ + 'base', + 'estate', + 'account', + ], + "author": "Karim Gamaleldin", + "license": "LGPL-3", + "data": [ + ], + 'application': True, +} 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..15c9468a53e --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,26 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + def action_set_sold(self): + partner_id = self.buyer_id.id + move_type = 'out_invoice' + property_fee_invoice = {'name': self.name, 'quantity': 1, 'price_unit': self.selling_price * 0.06} + adminstration_fee_invoice = { + 'name': "Administration fees", + 'quantity': 1, + 'price_unit': 100, + } + + self.env['account.move'].create({ + 'move_type': move_type, + 'partner_id': partner_id, + "line_ids": [ + Command.create(property_fee_invoice), + Command.create(adminstration_fee_invoice), + ] + }) + + return super().action_set_sold() From 222c5658269c06dc09c25004667ff45714c52c72 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Wed, 18 Feb 2026 14:24:55 +0100 Subject: [PATCH 14/16] [REF] estate: refactor salesperson model and views to res.users To comply with the naming conventions for all res.users inheritances that can be found elsewhere in the codebase. - Rename `estate_sales_person.py` to `res_users.py` - Rename `estate_sales_views.xml` to `res_users_views.xml` - Rename `EstateSalesPerson` class to `ResUsers` class --- estate/__manifest__.py | 2 +- estate/models/__init__.py | 2 +- estate/models/{estate_sales_person.py => res_users.py} | 2 +- .../{estate_sales_person_views.xml => res_users_views.xml} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename estate/models/{estate_sales_person.py => res_users.py} (84%) rename estate/views/{estate_sales_person_views.xml => res_users_views.xml} (91%) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index a62702960d0..83ad38c3a47 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -12,7 +12,7 @@ "views/estate_property_type_views.xml", "views/estate_property_tag_views.xml", "views/estate_property_offers_views.xml", - "views/estate_sales_person_views.xml", + "views/res_users_views.xml", "views/estate_property_menus.xml", "views/estate_property_type_menus.xml", "views/estate_property_tag_menus.xml", diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 0cdcf5296e6..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -2,4 +2,4 @@ from . import estate_property_type from . import estate_property_tag from . import estate_property_offer -from . import estate_sales_person +from . import res_users diff --git a/estate/models/estate_sales_person.py b/estate/models/res_users.py similarity index 84% rename from estate/models/estate_sales_person.py rename to estate/models/res_users.py index e771f21225f..74cc2697b67 100644 --- a/estate/models/estate_sales_person.py +++ b/estate/models/res_users.py @@ -1,7 +1,7 @@ from odoo import fields, models -class EstateSalesPerson(models.Model): +class ResUsers(models.Model): _inherit = "res.users" property_ids = fields.One2many("estate.property", "salesperson_id", string="Estate Properties", domain=[('state', 'in', ['new', 'offer_received'])]) diff --git a/estate/views/estate_sales_person_views.xml b/estate/views/res_users_views.xml similarity index 91% rename from estate/views/estate_sales_person_views.xml rename to estate/views/res_users_views.xml index dc4145da5d2..dd7b812809f 100644 --- a/estate/views/estate_sales_person_views.xml +++ b/estate/views/res_users_views.xml @@ -1,6 +1,6 @@ - + estate.salesperson.form res.users From 51c0e8b200faef5eba3f8b5eaac18ea70e20c096 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Wed, 18 Feb 2026 15:44:27 +0100 Subject: [PATCH 15/16] [IMP] estate: add kanban view with grouping and conditional fields Add a kanban view to give users a better visual overview of properties. The view groups properties by type to keep things organized and shows important price details only when they are relevant. - Add kanban to the property action view_mode. - Create a kanban view showing property name and tags. - Show 'best price' only when an offer exists and 'selling price' only when an offer is accepted. - Group properties by type by default. - Disable the drag-and-drop feature for kanban columns. --- estate/views/estate_property_views.xml | 31 +++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 24287d55d72..edb2314e2d1 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -99,10 +99,39 @@ + + estate.property.kanban + estate.property + + + + + +

+ +
+
+ Expected Price: +
+
+ Best Offer: +
+
+ Selling Price: +
+
+ +
+ + + +
+ + Properties estate.property - list,form + list,form,kanban {'search_default_available': 1}

From 10f279484daec7ff9dc1a5f7517b6ccee1f09488 Mon Sep 17 00:00:00 2001 From: karimgamaleldin Date: Thu, 19 Feb 2026 15:40:18 +0100 Subject: [PATCH 16/16] [ADD] awesome_owl: implement owl framework components and todo list This commit introduces a series of Owl components and features as part of the Owl training chapter. It covers the core building blocks of the framework. The following features were implemented: - Playground: Converted into a counter using 'useState' and basic event handling. - Counter: Extracted as a standalone reusable component with prop validation and callback support for parent-child communication. - Card: Created a generic UI component supporting props, 'markup' for HTML rendering, and 'slots' for flexible content. Added a toggle state to collapse/expand content. - TodoList & TodoItem: - List rendering with 't-foreach' and unique keys. - Dynamic attribute binding for completion styling. - Task creation with input handling and auto-focus via 'useRef'. - Task deletion and toggling using callback props. - Utils: Added 'useAutofocus' custom hook for DOM manipulation. --- awesome_owl/__manifest__.py | 1 + .../static/src/components/card/card.js | 23 ++++++++++ .../static/src/components/card/card.xml | 14 +++++++ .../static/src/components/counter/counter.js | 21 ++++++++++ .../static/src/components/counter/counter.xml | 11 +++++ .../src/components/todo_list/todo_item.js | 27 ++++++++++++ .../src/components/todo_list/todo_item.xml | 16 +++++++ .../src/components/todo_list/todo_list.js | 42 +++++++++++++++++++ .../src/components/todo_list/todo_list.xml | 14 +++++++ awesome_owl/static/src/components/utils.js | 8 ++++ awesome_owl/static/src/main.js | 1 - awesome_owl/static/src/playground.js | 18 +++++++- awesome_owl/static/src/playground.xml | 13 +++++- 13 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 awesome_owl/static/src/components/card/card.js create mode 100644 awesome_owl/static/src/components/card/card.xml create mode 100644 awesome_owl/static/src/components/counter/counter.js create mode 100644 awesome_owl/static/src/components/counter/counter.xml create mode 100644 awesome_owl/static/src/components/todo_list/todo_item.js create mode 100644 awesome_owl/static/src/components/todo_list/todo_item.xml create mode 100644 awesome_owl/static/src/components/todo_list/todo_list.js create mode 100644 awesome_owl/static/src/components/todo_list/todo_list.xml create mode 100644 awesome_owl/static/src/components/utils.js diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 55002ab81de..beb6457de4c 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -33,6 +33,7 @@ 'web/static/src/scss/pre_variables.scss', 'web/static/lib/bootstrap/scss/_variables.scss', 'web/static/lib/bootstrap/scss/_maps.scss', + ('include', 'web._assets_bootstrap_backend'), ('include', 'web._assets_bootstrap'), ('include', 'web._assets_core'), 'web/static/src/libs/fontawesome/css/font-awesome.css', diff --git a/awesome_owl/static/src/components/card/card.js b/awesome_owl/static/src/components/card/card.js new file mode 100644 index 00000000000..446001901cd --- /dev/null +++ b/awesome_owl/static/src/components/card/card.js @@ -0,0 +1,23 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: String, + slots: { + type: Object, + shape: { + default: true + } + } + }; + + setup() { + this.state = useState({renderContent: true}); + } + + toggleContent(){ + this.state.renderContent = !this.state.renderContent; + } + +} \ No newline at end of file diff --git a/awesome_owl/static/src/components/card/card.xml b/awesome_owl/static/src/components/card/card.xml new file mode 100644 index 00000000000..cb5e9a6ad47 --- /dev/null +++ b/awesome_owl/static/src/components/card/card.xml @@ -0,0 +1,14 @@ + + + + +

+
+ +
+ +
+
+ + + diff --git a/awesome_owl/static/src/components/counter/counter.js b/awesome_owl/static/src/components/counter/counter.js new file mode 100644 index 00000000000..568a6ac7f2f --- /dev/null +++ b/awesome_owl/static/src/components/counter/counter.js @@ -0,0 +1,21 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + static props = { + onChange: {type: Function, optional: true}, + }; + + static defaultProps = { + onChange: () => {}, + }; + + setup () { + this.state = useState({value: 0}); + } + + increment () { + this.state.value++; + this.props.onChange(); + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/components/counter/counter.xml b/awesome_owl/static/src/components/counter/counter.xml new file mode 100644 index 00000000000..f05774d994f --- /dev/null +++ b/awesome_owl/static/src/components/counter/counter.xml @@ -0,0 +1,11 @@ + + + + +
+ Counter: + +
+
+ +
diff --git a/awesome_owl/static/src/components/todo_list/todo_item.js b/awesome_owl/static/src/components/todo_list/todo_item.js new file mode 100644 index 00000000000..66eab045225 --- /dev/null +++ b/awesome_owl/static/src/components/todo_list/todo_item.js @@ -0,0 +1,27 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + + static props = { + todo: { + type: Object, + shape: { + id: Number, + title: String, + isCompleted: Boolean, + } + }, + toggleTodo: Function, + deleteTodo: Function + }; + + onToggle() { + this.props.toggleTodo(this.props.todo.id); + } + + onDelete() { + this.props.deleteTodo(this.props.todo.id); + } + +} diff --git a/awesome_owl/static/src/components/todo_list/todo_item.xml b/awesome_owl/static/src/components/todo_list/todo_item.xml new file mode 100644 index 00000000000..e20447a4800 --- /dev/null +++ b/awesome_owl/static/src/components/todo_list/todo_item.xml @@ -0,0 +1,16 @@ + + + + +
+ +
+
+ +
diff --git a/awesome_owl/static/src/components/todo_list/todo_list.js b/awesome_owl/static/src/components/todo_list/todo_list.js new file mode 100644 index 00000000000..a2be5285ca8 --- /dev/null +++ b/awesome_owl/static/src/components/todo_list/todo_list.js @@ -0,0 +1,42 @@ +import { Component, useState, useRef, onMounted } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutoFocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + + static components = { TodoItem }; + + setup () { + this.todos = useState([]); + this.ids = useState({"last_id": 0}); + useAutoFocus("input"); + } + + addTodo (ev) { + if (ev.keyCode === 13 && ev.target.value) { + this.ids.last_id += 1 + this.todos.push({ + id: this.ids.last_id, + title: ev.target.value, + isCompleted: false, + }); + ev.target.value = ""; + } + } + + toggleTodo(todo_id) { + const index = this.todos.findIndex(t => t.id === todo_id); + if (index !== -1) { + this.todos[index].isCompleted = !this.todos[index].isCompleted; + } + } + + deleteTodo(todo_id){ + const index = this.todos.findIndex(t => t.id === todo_id); + if (index !== -1) { + this.todos.splice(index, 1); + } + } + +} diff --git a/awesome_owl/static/src/components/todo_list/todo_list.xml b/awesome_owl/static/src/components/todo_list/todo_list.xml new file mode 100644 index 00000000000..d176f06d25f --- /dev/null +++ b/awesome_owl/static/src/components/todo_list/todo_list.xml @@ -0,0 +1,14 @@ + + + + +
+

Todo List

+ + + + +
+
+ +
diff --git a/awesome_owl/static/src/components/utils.js b/awesome_owl/static/src/components/utils.js new file mode 100644 index 00000000000..19e0d7b8f79 --- /dev/null +++ b/awesome_owl/static/src/components/utils.js @@ -0,0 +1,8 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutoFocus (refName) { + const ref = useRef(refName); + onMounted(() => { + ref.el.focus(); + }); +} \ No newline at end of file diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1aaea902b55..6c108687e29 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -9,4 +9,3 @@ const config = { // Mount the Playground component when the document.body is ready whenReady(() => mountComponent(Playground, document.body, config)); - diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..a18bb42c5a6 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,21 @@ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./components/counter/counter"; +import { Card } from "./components/card/card"; +import { TodoList } from "./components/todo_list/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + + static components = { Counter, Card, TodoList }; + + value1 = "Some HTML content"; + value2 = markup("Some other HTML content"); + + setup () { + this.state = useState({sum: 0}); + } + + onCounterChange () { + this.state.sum += 1; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..c26120ccda1 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -3,7 +3,18 @@
- hello world +
+ + +

The sum is:

+
+ + Description for Card 1 + + + + +