From 5e7de91628139909f188a5ce9234e3d2def9e793 Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Tue, 18 Nov 2025 13:48:55 +0100 Subject: [PATCH 01/16] [ADD] estate: init --- estate/__init__.py | 0 estate/__manifest__.py | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..4cf1d0db1b8 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Estate", + 'depends': ['base'], + 'application': True +} \ No newline at end of file From b1566b83ded402e9fc7e43be9f3da158fd487609 Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Tue, 18 Nov 2025 15:03:44 +0100 Subject: [PATCH 02/16] [IMP] estate: create the estate property table --- estate/__init__.py | 1 + estate/models/__init__.py | 1 + estate/models/estate_property.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py diff --git a/estate/__init__.py b/estate/__init__.py index e69de29bb2d..9a7e03eded3 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ 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..168c58e3987 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,21 @@ +from odoo import fields, models + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Property' + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date() + expected_price = fields.Float(required=True) + selling_price = fields.Float() + bedrooms = fields.Integer() + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string='Type', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) \ No newline at end of file From 7a05e0691e088afab81ca3bb1e8473d6828a228d Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Tue, 18 Nov 2025 15:42:36 +0100 Subject: [PATCH 03/16] [IMP] estate: add access rule to the estate property table --- estate/__init__.py | 2 +- estate/__manifest__.py | 8 +++++--- estate/data/ir.model.access.csv | 2 ++ estate/models/__init__.py | 2 +- estate/models/estate_property.py | 9 ++++++++- 5 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 estate/data/ir.model.access.csv diff --git a/estate/__init__.py b/estate/__init__.py index 9a7e03eded3..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1 +1 @@ -from . import models \ No newline at end of file +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 4cf1d0db1b8..a4b424774a8 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,6 +1,8 @@ -# -*- coding: utf-8 -*- { 'name': "Estate", 'depends': ['base'], - 'application': True -} \ No newline at end of file + 'application': True, + 'data': [ + 'data/ir.model.access.csv' + ] +} diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv new file mode 100644 index 00000000000..fe21e56c6d2 --- /dev/null +++ b/estate/data/ir.model.access.csv @@ -0,0 +1,2 @@ +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 \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py index f4c8fd6db6d..5e1963c9d2f 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1 @@ -from . import estate_property \ No newline at end of file +from . import estate_property diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 168c58e3987..35b6517faf7 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,6 @@ from odoo import fields, models + class EstateProperty(models.Model): _name = 'estate.property' _description = 'Property' @@ -18,4 +19,10 @@ class EstateProperty(models.Model): garden_area = fields.Integer() garden_orientation = fields.Selection( string='Type', - selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) \ No newline at end of file + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], + ) From 0915bdb0f719b6d5daafab4be8ec24f9e79b6155 Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Tue, 18 Nov 2025 17:00:47 +0100 Subject: [PATCH 04/16] [IMP] estate: add author and license --- estate/__manifest__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index a4b424774a8..c80686803be 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,5 +1,7 @@ { 'name': "Estate", + 'author': "sypol", + 'license': "LGPL-3", 'depends': ['base'], 'application': True, 'data': [ From 0c570c88355140b97f9a7ee78acede2761d42a10 Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Wed, 19 Nov 2025 10:45:21 +0100 Subject: [PATCH 05/16] [IMP] estate: add views to estate property --- estate/__manifest__.py | 4 +++- estate/models/estate_property.py | 22 +++++++++++++++++++--- estate/views/estate_menus.xml | 8 ++++++++ estate/views/estate_property_views.xml | 8 ++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index c80686803be..222efa1eec9 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -5,6 +5,8 @@ 'depends': ['base'], 'application': True, 'data': [ - 'data/ir.model.access.csv' + 'data/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', ] } diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 35b6517faf7..5f33e644962 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,3 +1,5 @@ +from dateutil.relativedelta import relativedelta + from odoo import fields, models @@ -8,10 +10,10 @@ class EstateProperty(models.Model): name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() - date_availability = fields.Date() + date_availability = fields.Date(copy=False, default=fields.Date.today() + relativedelta(months=+3)) expected_price = fields.Float(required=True) - selling_price = fields.Float() - bedrooms = fields.Integer() + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) living_area = fields.Integer() facades = fields.Integer() garage = fields.Boolean() @@ -26,3 +28,17 @@ class EstateProperty(models.Model): ('west', 'West'), ], ) + active = fields.Boolean(default=True) + state = fields.Selection( + string='State', + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + required=True, + copy=False, + default='new' + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..49062c9a34d --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..7534b527918 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,8 @@ + + + + Property + estate.property + list,form + + From 70f2037f8ca225d1c2be89c0f4f0b3b33d0a757d Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Wed, 19 Nov 2025 14:31:57 +0100 Subject: [PATCH 06/16] [IMP] estate: add list, form and search views to estate property --- estate/__manifest__.py | 2 +- estate/data/ir.model.access.csv | 2 +- estate/models/estate_property.py | 37 +++++++------ estate/views/estate_property_views.xml | 75 ++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 19 deletions(-) diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 222efa1eec9..417f9179470 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -8,5 +8,5 @@ 'data/ir.model.access.csv', 'views/estate_property_views.xml', 'views/estate_menus.xml', - ] + ], } diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv index fe21e56c6d2..85de405deb2 100644 --- a/estate/data/ir.model.access.csv +++ b/estate/data/ir.model.access.csv @@ -1,2 +1,2 @@ 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 \ No newline at end of file +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 5f33e644962..59ebbc4ca83 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,5 +1,3 @@ -from dateutil.relativedelta import relativedelta - from odoo import fields, models @@ -7,28 +5,33 @@ class EstateProperty(models.Model): _name = 'estate.property' _description = 'Property' - name = fields.Char(required=True) - description = fields.Text() - postcode = fields.Char() - date_availability = fields.Date(copy=False, default=fields.Date.today() + 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() + name = fields.Char('Title', required=True) + description = fields.Text('Description') + postcode = fields.Char('Postcode') + date_availability = fields.Date( + 'Available From', + copy=False, + default=fields.Date.add(fields.Date.today(), months=+3), + ) + expected_price = fields.Float('Expected Price', required=True) + selling_price = fields.Float('Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer('Bedrooms', default=2) + living_area = fields.Integer('Living Area (sqm)') + facades = fields.Integer('Facades') + garage = fields.Boolean('Garage') + garden = fields.Boolean('Garden') + garden_area = fields.Integer('Garden Area (sqm)') garden_orientation = fields.Selection( - string='Type', + string='Garden Orientation', selection=[ ('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West'), ], + default='north', ) - active = fields.Boolean(default=True) + active = fields.Boolean('Active', default=True) state = fields.Selection( string='State', selection=[ @@ -40,5 +43,5 @@ class EstateProperty(models.Model): ], required=True, copy=False, - default='new' + default='new', ) diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index 7534b527918..f188bd25bea 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -1,5 +1,80 @@ + + estate.property.view.search + estate.property + 15 + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+ + +

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + Property estate.property From b7d46a1d0663d5e9cbe3a48180435fefe7b1df52 Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Wed, 19 Nov 2025 17:13:43 +0100 Subject: [PATCH 07/16] [IMP] estate: introduce property types, tags, and offers --- estate/__manifest__.py | 9 +++++- estate/data/ir.model.access.csv | 2 -- estate/models/__init__.py | 7 ++++- estate/models/estate_property.py | 9 ++++++ estate/models/estate_property_offer.py | 18 ++++++++++++ estate/models/estate_property_tag.py | 8 ++++++ estate/models/estate_property_type.py | 8 ++++++ estate/security/ir.model.access.csv | 5 ++++ estate/views/estate_menus.xml | 10 +++++-- estate/views/estate_property_offer_views.xml | 30 ++++++++++++++++++++ estate/views/estate_property_tag_views.xml | 8 ++++++ estate/views/estate_property_type_views.xml | 8 ++++++ estate/views/estate_property_views.xml | 21 +++++++++++--- 13 files changed, 133 insertions(+), 10 deletions(-) delete mode 100644 estate/data/ir.model.access.csv 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/security/ir.model.access.csv create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 417f9179470..de9ae42ed1f 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,12 +1,19 @@ { 'name': "Estate", + 'description': """ + Track real estate properties + """, + 'version': '1.0', 'author': "sypol", 'license': "LGPL-3", 'depends': ['base'], 'application': True, 'data': [ - 'data/ir.model.access.csv', + 'security/ir.model.access.csv', 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', 'views/estate_menus.xml', ], } diff --git a/estate/data/ir.model.access.csv b/estate/data/ir.model.access.csv deleted file mode 100644 index 85de405deb2..00000000000 --- a/estate/data/ir.model.access.csv +++ /dev/null @@ -1,2 +0,0 @@ -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 diff --git a/estate/models/__init__.py b/estate/models/__init__.py index 5e1963c9d2f..a0a8b8c501c 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1 +1,6 @@ -from . import estate_property +from . import ( + estate_property, + estate_property_type, + estate_property_tag, + estate_property_offer, +) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 59ebbc4ca83..27f0ae0e572 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -45,3 +45,12 @@ class EstateProperty(models.Model): copy=False, default='new', ) + property_type_id = fields.Many2one('estate.property.type', string='Property Types') + property_tag_ids = fields.Many2many('estate.property.tag', string='Property Tags') + buyer = fields.Many2one('res.partner', string='Buyer', copy=False) + salesperson = fields.Many2one( + 'res.users', string='Salesperson', default=lambda self: self.env.user + ) + offer_ids = fields.One2many( + 'estate.property.offer', 'property_id', string='Property Offers' + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..cacbf75ac20 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,18 @@ +from odoo import fields, models + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Property Offer" + + price = fields.Float('Price') + status = fields.Selection( + string='Status', + selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ], + copy=False, + ) + partner_id = fields.Many2one('res.partner', string='Partner', required=True) + property_id = fields.Many2one('estate.property', string='Property', required=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..4aa79c7bd36 --- /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 = "Property Tag" + + name = fields.Char('Tag', required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..e17dcba9b3e --- /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 = "Property Type" + + name = fields.Char('Type', required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..c79331f2f1c --- /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 +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 index 49062c9a34d..6ff47288b05 100644 --- a/estate/views/estate_menus.xml +++ b/estate/views/estate_menus.xml @@ -1,8 +1,14 @@ - - + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..d98ede64d2d --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,30 @@ + + + + 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_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..7733f786120 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,8 @@ + + + + Property Tags + estate.property.tag + list,form + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..0ceab096f70 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,8 @@ + + + + Property Types + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index f188bd25bea..e1a74bfc6f7 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -7,13 +7,14 @@ + + domain="[('state', 'in', ('new', 'offer_received'))]" /> @@ -25,13 +26,15 @@
- +

- + +
+ @@ -53,6 +56,15 @@ + + + + + + + + +
@@ -63,8 +75,9 @@ estate.property.list estate.property - + + From 91959804ed5f0442ff0fb04d1e41fad1b73225b1 Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Thu, 20 Nov 2025 13:53:34 +0100 Subject: [PATCH 08/16] [IMP] estate: add computations to estate property --- estate/models/estate_property.py | 26 +++++++++++++++++++- estate/models/estate_property_offer.py | 20 ++++++++++++++- estate/views/estate_property_offer_views.xml | 4 +++ estate/views/estate_property_views.xml | 2 ++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 27f0ae0e572..90ddd4ee7f9 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 class EstateProperty(models.Model): @@ -54,3 +54,27 @@ class EstateProperty(models.Model): offer_ids = fields.One2many( 'estate.property.offer', 'property_id', string='Property Offers' ) + total_area = fields.Integer('Total Area (sqm)', compute='_compute_total_area') + best_price = fields.Float('Best Offer', compute='_compute_best_price') + + @api.depends('garden_area', 'living_area') + def _compute_total_area(self): + for property in self: + property.total_area = property.garden_area + property.living_area + + @api.depends('offer_ids.price') + def _compute_best_price(self): + for property in self: + if len(self.offer_ids): + property.best_price = max(property.offer_ids.mapped('price')) + else: + property.best_price = 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 = '' diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index cacbf75ac20..92de19e5f99 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ -from odoo import fields, models +from odoo import api, fields, models +from odoo.tools import date_utils class EstatePropertyOffer(models.Model): @@ -16,3 +17,20 @@ class EstatePropertyOffer(models.Model): ) partner_id = fields.Many2one('res.partner', string='Partner', required=True) property_id = fields.Many2one('estate.property', string='Property', required=True) + validity = fields.Integer('Validity (days)', default=7) + date_deadline = fields.Date( + 'Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline' + ) + + @api.depends('validity') + def _compute_date_deadline(self): + for offer in self: + offer.date_deadline = date_utils.add( + (offer.create_date or fields.Date.today()), days=offer.validity + ) + + def _inverse_date_deadline(self): + for offer in self: + offer.validity = ( + offer.date_deadline - (offer.create_date.date() or fields.Date.today()) + ).days diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index d98ede64d2d..eb0a6d20506 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -9,6 +9,8 @@ + + @@ -23,6 +25,8 @@ + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index e1a74bfc6f7..03ebfcbef01 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -40,6 +40,7 @@ + @@ -54,6 +55,7 @@ + From 9864fd1cb4c3e22526e29b75654dc99145dbaa7f Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Thu, 20 Nov 2025 15:00:54 +0100 Subject: [PATCH 09/16] [IMP] estate: implement offer acceptance and refusal actions, and add property sold/canceled state management --- estate/models/estate_property.py | 19 +++++++++++++++++-- estate/models/estate_property_offer.py | 18 ++++++++++++++++++ estate/views/estate_property_offer_views.xml | 2 ++ estate/views/estate_property_views.xml | 4 ++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py index 90ddd4ee7f9..bae02d1c43b 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 class EstateProperty(models.Model): @@ -39,7 +40,7 @@ class EstateProperty(models.Model): ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), - ('cancelled', 'Cancelled'), + ('canceled', 'Canceled'), ], required=True, copy=False, @@ -77,4 +78,18 @@ def _onchange_garden(self): self.garden_orientation = 'north' else: self.garden_area = 0 - self.garden_orientation = '' + self.garden_orientation = False + + def action_set_property_as_sold(self): + for property in self: + if property.state == 'canceled': + raise UserError('Canceled properties cannot be sold.') + property.state = 'sold' + return True + + def action_set_property_as_canceled(self): + for property in self: + if property.state == 'sold': + raise UserError('Sold properties cannot be canceled.') + property.state = 'canceled' + return True diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index 92de19e5f99..afa2c6435f9 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError from odoo.tools import date_utils @@ -34,3 +35,20 @@ def _inverse_date_deadline(self): offer.validity = ( offer.date_deadline - (offer.create_date.date() or fields.Date.today()) ).days + + def action_accept(self): + for offer in self: + other_offers = offer.property_id.offer_ids - offer + if any(other_offers.filtered(lambda o: o.status == 'accepted')): + raise UserError('An offer has already been accepted.') + + offer.status = 'accepted' + offer.property_id.state = 'offer_accepted' + offer.property_id.buyer = offer.partner_id + offer.property_id.selling_price = offer.price + return True + + def action_refuse(self): + for offer in self: + offer.status = 'refused' + return True diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml index eb0a6d20506..357d34df268 100644 --- a/estate/views/estate_property_offer_views.xml +++ b/estate/views/estate_property_offer_views.xml @@ -27,6 +27,8 @@ + + + + diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1aaea902b55..b78166ed913 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -4,7 +4,7 @@ import { Playground } from "./playground"; const config = { dev: true, - name: "Owl Tutorial" + name: "Owl Tutorial" }; // Mount the Playground component when the document.body is ready diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..7f7687c3d55 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,20 @@ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from './counter/counter'; +import { Card } from "./card/card"; +import { TodoList } from "./todo/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList }; + static props = {}; + + html = markup('Some content'); + + setup() { + this.state = useState({ sum: 2 }); + } + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..adf4bc23b98 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,19 @@ - + -
- hello world -
+ + + + + + + + + + + +
diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js new file mode 100644 index 00000000000..bd52956d415 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,14 @@ +import { Component } from '@odoo/owl'; + +export class TodoItem extends Component { + static template = 'awesome_owl.todo_item'; + static props = { + todo: { + type: { + id: { type: Number }, + description: { type: String }, + isCompleted: { type: Boolean }, + }, + }, + }; +} diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml new file mode 100644 index 00000000000..cdf04f2fd17 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,10 @@ + + + + +
+ . +
+
+ +
diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js new file mode 100644 index 00000000000..c2000a09628 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,25 @@ +import { Component, useState } from '@odoo/owl'; +import { TodoItem } from './todo_item'; + +export class TodoList extends Component { + static template = 'awesome_owl.todo_list'; + static components = { TodoItem }; + static props = {}; + + setup() { + this.todos = useState([]); + } + + addTodo(ev) { + if (ev.keyCode === 13 && ev.target.value) { + const lastTodo = this.todos.slice(-1)[0]; + this.todos.push({ + id: lastTodo === undefined ? 0 : lastTodo.id + 1, + description: ev.target.value, + isCompleted: false, + }); + + ev.target.value = ''; + } + } +} diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml new file mode 100644 index 00000000000..9bf033e0297 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,15 @@ + + + + +
+
+ + + + +
+
+
+ +
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py index efb59009caf..97750cf97bc 100644 --- a/estate/models/estate_property_offer.py +++ b/estate/models/estate_property_offer.py @@ -49,7 +49,11 @@ def _inverse_date_deadline(self): @api.model def create(self, vals_list): for offer in vals_list: - linked_property = self.env['estate.property'].browse(offer['property_id']) + property_id = offer.get('property_id') + if not property_id: + continue + + linked_property = self.env['estate.property'].browse(property_id) lowest_price = min(linked_property.offer_ids.mapped('price'), default=0.0) if offer['price'] < lowest_price: From c01ef3a7ac698b12d92c2feaf935981a3b011ff9 Mon Sep 17 00:00:00 2001 From: "Poliart Sylvio (sypol)" Date: Wed, 26 Nov 2025 13:59:15 +0100 Subject: [PATCH 16/16] [IMP] awesome_owl: update Playground, Card, and TodoList components with new features --- awesome_owl/static/src/card/card.js | 8 +++++-- awesome_owl/static/src/card/card.xml | 7 ++++-- awesome_owl/static/src/playground.js | 2 +- awesome_owl/static/src/playground.xml | 26 +++++++++++++---------- awesome_owl/static/src/todo/todo_item.js | 2 ++ awesome_owl/static/src/todo/todo_item.xml | 2 ++ awesome_owl/static/src/todo/todo_list.js | 10 +++++++++ awesome_owl/static/src/todo/todo_list.xml | 4 ++-- awesome_owl/static/src/utils.js | 10 +++++++++ 9 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js index 491c1b275c6..884e16f960f 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -1,9 +1,13 @@ -import { Component } from '@odoo/owl'; +import { Component, useState } from '@odoo/owl'; export class Card extends Component { static template = 'awesome_owl.card'; static props = { title: { type: String }, - content: { type: [String, Number] }, + slots: { type: Object, optional: true }, }; + + setup() { + this.state = useState({ isOpen: true }); + } } diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index 690cd4e667e..42b56f7ba26 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -4,8 +4,11 @@
-
-

+

+
+ +
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 7f7687c3d55..8941d5babe4 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -11,7 +11,7 @@ export class Playground extends Component { html = markup('Some content'); setup() { - this.state = useState({ sum: 2 }); + this.state = useState({ sum: 0 }); } incrementSum() { diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index adf4bc23b98..5c83e965dd4 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,17 +2,21 @@ - - - - - - - - - - - + + + + + + +

+ + + + Content + + + + diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js index bd52956d415..08eb74cbaf5 100644 --- a/awesome_owl/static/src/todo/todo_item.js +++ b/awesome_owl/static/src/todo/todo_item.js @@ -10,5 +10,7 @@ export class TodoItem extends Component { isCompleted: { type: Boolean }, }, }, + toggleState: { type: Function }, + removeTodo: { type: Function }, }; } diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml index cdf04f2fd17..ef314dbd1de 100644 --- a/awesome_owl/static/src/todo/todo_item.xml +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -3,7 +3,9 @@

+ . +
diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js index c2000a09628..1c63cf6b9dd 100644 --- a/awesome_owl/static/src/todo/todo_list.js +++ b/awesome_owl/static/src/todo/todo_list.js @@ -1,5 +1,6 @@ import { Component, useState } from '@odoo/owl'; import { TodoItem } from './todo_item'; +import { useAutoFocus } from '../utils'; export class TodoList extends Component { static template = 'awesome_owl.todo_list'; @@ -8,6 +9,8 @@ export class TodoList extends Component { setup() { this.todos = useState([]); + + useAutoFocus('input'); } addTodo(ev) { @@ -22,4 +25,11 @@ export class TodoList extends Component { ev.target.value = ''; } } + + removeTodo(elemId) { + const index = this.todos.findIndex((elem) => elem.id === elemId); + if (index >= 0) { + this.todos.splice(index, 1); + } + } } diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml index 9bf033e0297..9c90f78ee81 100644 --- a/awesome_owl/static/src/todo/todo_list.xml +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -4,9 +4,9 @@
- + - +
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..44533c50f1b --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,10 @@ +import { useRef, onMounted } from '@odoo/owl'; + +function useAutoFocus(refName) { + const inputRef = useRef(refName); + onMounted(() => { + inputRef.el.focus(); + }); +} + +export { useAutoFocus };