Skip to content

Conversation

@jakan-odoo
Copy link

@jakan-odoo jakan-odoo commented Jan 1, 2026

  • Added models for properties, property types, tags, and offers
  • Designed a structured data model to manage real estate records
  • Created menus and actions for a clear and user-friendly interface
  • Implemented validations for expected and selling prices
  • Handled business rule violations using UserError and ValidationError

- Real Estate module created
- Basic structure and required fields added
- CHAPTER 1, 2, and 3
- Added basic security access for estate records
- Fixed missing whitespace and formatting issues
- Covers CHAPTER 4
@robodoo
Copy link

robodoo commented Jan 1, 2026

Pull request status dashboard

- Added form and list views for estate records
- Added menu and action to access the module
- Covers CHAPTER 5
- Added list, form, and search views
- Added domains and group by options
- Covers CHAPTER 6
Copy link

@mash-odoo mash-odoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @jakan-odoo,
Good start on the PR,
Please view the comments and apply the needed changes.

Comment on lines 1 to 2
from odoo import fields, models
from dateutil.relativedelta import relativedelta

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 9 to 10
<record id="estate_property_list_view" model="ir.ui.view">
<field name="name">estate.property.list</field>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For naming a view: <model_name>_view_<view_type>, where view_type is kanban, form, list, search, …
You can refer this guideline: https://www.odoo.com/documentation/19.0/contributing/development/coding_guidelines.html#xml-ids-and-naming

Comment on lines 25 to 26
<record id="estate_property_form_view" model="ir.ui.view">
<field name="name">estate.property.form</field>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do same as suggested above

- Added Many2one relation between models
- Improves data linking between records
- Covers CHAPTER 7
- Added One2many and Many2many relations between models
- Improves linking and management of related records
- Covers CHAPTER 7
- Added computed fields for automatic values
- Added onchange methods to update fields
- Covers CHAPTER 8
- Created Sell, Cancel, Accept, and Refuse buttons
- Buttons call related methods on the estate models
- Buyer and selling price are set when an offer is accepted
- Covers CHAPTER 9
Copy link

@mash-odoo mash-odoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello,
Good going on the task..
Here are some suggestions..

<field name="bedrooms"/>
<field name="living_area"/>
<field name="selling_price"/>
<filter string="Available Properties" name="available properties" domain="['|', ('state', '=', 'new'), ('state', '=', 'offer_received')]"/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we write this filter in any other way?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes ma'am,
We can write filter like this
<filter string="Available Properties" name="available_properties" domain="[('state', 'in', ('new', 'offer_received'))]"/>

</form>
</field>
</record>
</odoo> No newline at end of file

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave a line at the end of file

Comment on lines 31 to 34
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West")])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West")])
('north', "North"),
('south', "South"),
('east', "East"),
('west', "West")])

Comment on lines 38 to 42
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled'),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled'),
('new', "New"),
('offer_received', "Offer Received"),
('offer_accepted', "Offer Accepted"),
('sold', "Sold"),
('cancelled', '"Cancelled"),

@@ -0,0 +1,20 @@
{
'name': 'Real Estate',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'name': 'Real Estate',
'name': "Real Estate",

'version': '1.0',
'category': 'Real Estate',
'summary': 'Manage real estate properties',
'description': 'This module allows managing properties.',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'description': 'This module allows managing properties.',
'description': "This module allows managing properties.",

Comment on lines 1 to 2
from dateutil.relativedelta import relativedelta
from odoo import fields, models, api

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- Added SQL constraints to ensure data validity
- Added Python constraints for business rules
- Covers CHAPTER 10
- Added inline views for related records
- Applied widgets to improve user interface
- Set default ordering using _order
- CHAPTER 11
- Added maintenance request model with title, cost, and status
- Ensured approved requests have a positive cost
- Displayed total maintenance cost on the property
- Prevented property sale when maintenance is not completed
- Replaced direct cost check with float_is_zero
- Ensures correct validation for decimal precision
- Added manual ordering and widget options
- Applied conditional button visibility using invisible
- Made list views editable and some fields optional
- Added decorations, default filters, and stat buttons
- Covers CHAPTER 11
Copy link

@mash-odoo mash-odoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello!
Thank you for your work..
Here are some suggestions and questions..

offer.status = "refused"
return True

_check_offer_price_positive = models.Constraint(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow this conventions while declaring fields, constraints, methods or CRUD operations.

_order = 'name'

name = fields.Char(required=True)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

property.state = 'cancelled'
return True

_check_expected_price_positive = models.Constraint(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow this conventions while declaring fields, constraints, methods or CRUD operations.

Comment on lines 36 to 43
<header>
<field name="state" widget="statusbar" statusbar_visible="new,offer_received,offer_accepted,sold"/>
</header>
<sheet>
<header>
<button name="action_sold" type="object" string="Sold" class="btn-primary" invisible="state in ('sold', 'cancelled')"/>
<button name="action_cancel" type="object" string="Cancel" class="btn-secondary" invisible="state in ('sold', 'cancelled')"/>
</header>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<header>
<field name="state" widget="statusbar" statusbar_visible="new,offer_received,offer_accepted,sold"/>
</header>
<sheet>
<header>
<button name="action_sold" type="object" string="Sold" class="btn-primary" invisible="state in ('sold', 'cancelled')"/>
<button name="action_cancel" type="object" string="Cancel" class="btn-secondary" invisible="state in ('sold', 'cancelled')"/>
</header>
<header>
<field name="state" widget="statusbar" statusbar_visible="new,offer_received,offer_accepted,sold"/>
<button name="action_sold" type="object" string="Sold" class="btn-primary" invisible="state in ('sold', 'cancelled')"/>
<button name="action_cancel" type="object" string="Cancel" class="btn-secondary" invisible="state in ('sold', 'cancelled')"/>
</header>
<sheet>

You can add these buttons directly in the header.

- Used Python inheritance to extend existing logic
- Applied model inheritance to add fields and behavior
- Used view inheritance to update and extend UI
- Covers CHAPTER 12
- Added translatable strings for the module
- Updated constraint names for clarity
- Applied small user interface changes
- Added archived filter for records
- Created new estate_account module
- Linked estate module with account module
- Added invoice creation for sold properties
- Covers CHAPTER 13
- Added investor profile linked to partner data
- Deleting partner removes investor, investor delete keeps partner
- Computed total unsold property value on res.partner
- Sum based on expected price of non-sold properties
- Added a Kanban view for estate properties
- Grouped properties by type by default
- Covers CHAPTER 14
- Implemented a Counter component to show reactive state using useState
- Split logic into reusable child components and used them in Playground
- Add Card component to showcase props, markup rendering, and validation
- Added offer accept button to automatically accept the highest offer price

CHAPTER 1
- Added props validation to Owl components
- Implemented logic to calculate the sum of two counters

CHAPTER 1
- Added a basic todo list component
- Used dynamic attributes for state-based updates
- Implemented event handler to add new todos

CHAPTER 1
@jakan-odoo jakan-odoo changed the title 19.0 real estate tutorial jakan [ADD] estate: implement core estate management features Jan 29, 2026
Copy link

@mash-odoo mash-odoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello!
Thank you for your work.
I have added some questions and suggestions..

<field name="state" widget="statusbar" statusbar_visible="new,offer_received,offer_accepted,sold" options="{'clickable': '1'}"/>
<button name="action_sold" type="object" string="Sold" class="btn-primary" invisible="state in ('sold', 'cancelled')"/>
<button name="action_cancel" type="object" string="Cancel" class="btn-secondary" invisible="state in ('sold', 'cancelled')"/>
<button name="offer_accepted" type="object" string="Offer Accept" class="btn-primary"/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<button name="offer_accepted" type="object" string="Offer Accept" class="btn-primary"/>
<button name="action_best_offer" type="object" string="Best Offer" class="btn-primary"/>

Give meaningful names

Comment on lines 137 to 143
def offer_accepted(self):
for record in self:
if not record.offer_ids:
raise UserError("There are no offers to accept")

best_offer = max(record.offer_ids, key=lambda p: p.price)
best_offer.action_accept()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try optimizing this. Instead of throwing user error, maybe handle the visibility of the button directly on the state where it should be shown.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

estate_property.py

    def action_best_offer(self):
        for record in self:
            best_offer = max(record.offer_ids, key=lambda p: p.price)
            best_offer.action_accept()

        return True

estate_property_views.xml
<button name="action_best_offer" type="object" string="Best Offer" class="btn-primary" invisible="not offer_ids"/>

('new', "New"),
('approved', "Approved"),
('done', "Done"),
('cancle', "Cancle")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
('cancle', "Cancle")
('cancel', "Cancel")

_description = 'Estate Property maintenance'

title = fields.Char(required=True)
cost = fields.Float(string="Cost")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to give the string

<list>
<field name="title"/>
<field name="cost"/>
<button name="maintenance_accept" type="object" string="Confirm" icon="fa-check"/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<button name="maintenance_accept" type="object" string="Confirm" icon="fa-check"/>
<button name="action_accept_maintenance" type="object" string="Confirm" icon="fa-check"/>

.gitignore Outdated
Comment on lines 130 to 131

i18n No newline at end of file

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnecessary diff

# Set property state
property_rec.state = "offer_received"

return super().create(vals_list)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we call super()?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We call super because it calls to parent create() method and the parent method is used for saving the record in database.

seller_id = fields.Many2one("res.users", string="Seller", default=lambda self: self.env.user)
tag_ids = fields.Many2many("estate.property.tag", string="Property Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Property Offers")
total_area = fields.Float(string="Total Area", compute="_compute_total_area", store=True)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try implementing searching/filtering of data without using store=True?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without using store=True i can use search method for searching data

estate_property.py
total_area = fields.Float(string="Total Area", compute="_compute_total_area", search="_search_total_area")

@api.depends('living_area', 'garden_area')
    def _compute_total_area(self):
        for property in self:
            property.total_area = property.living_area + property.garden_area
def _search_total_area(self, operator, value):
        if operator == '>' and value == 500:
            records = self.search([])
            ids = []

            for rec in records:
                if (rec.living_area) + (rec.garden_area) > 500:
                    ids.append(rec.id)

            return [('id', 'in', ids)]

        return []

estate_property_views.xml
<filter string="Total Area > 500" name="total_area_gt_500" domain="[('total_area', '>', 500)]"/>

Comment on lines +130 to +135
def action_cancel(self):
for property in self:
if property.state == 'sold':
raise UserError("Cancelled property cannot be sold")
property.state = 'cancelled'
return True

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrite this without using for loop.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    def action_cancel(self):
        if self.filtered(lambda p: p.state == 'sold'):
            raise UserError("Cancelled property cannot be sold")
        
        self.write({'state': 'cancelled'})
        return True

- Added default properties, property types, tags, and offers
- Automatically loads sample records when a new database is created
- Helps users see estate records on first login
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants