Skip to content

Comments

[IMP] awesomeclicker: add editor metadata in Python file#1173

Open
jupao-odoo wants to merge 22 commits intoodoo:19.0from
odoo-dev:19.0-add-editor-awesomeclicker-jupao
Open

[IMP] awesomeclicker: add editor metadata in Python file#1173
jupao-odoo wants to merge 22 commits intoodoo:19.0from
odoo-dev:19.0-add-editor-awesomeclicker-jupao

Conversation

@jupao-odoo
Copy link

Added a new line 'editor': "jupao" in the module code to indicate the editor used for this script. This helps with code clarity and consistency within the module.

Added a new line 'editor': "jupao" in the module code to
indicate the editor used for this script. This helps with
code clarity and consistency within the module.
@robodoo
Copy link

robodoo commented Feb 16, 2026

Pull request status dashboard

@ltinel ltinel self-requested a review February 16, 2026 16:14
Copy link

@ltinel ltinel left a comment

Choose a reason for hiding this comment

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

Great work :)

FYI, if you scroll to the bottom of your PR, you'll find some Runbot links, among which the ci/style check. In there, you'll find a few styling suggestions (typically incorrect use of whitespace or newlines). Could you implement those to make the check pass?

""",

'author': "Odoo",
'editor': "jupao",
Copy link

Choose a reason for hiding this comment

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

No need for this change :) I guess it was just a test?


available_from = fields.Date(
copy=False,
default=lambda self: fields.Date.today() + timedelta(days=90)
Copy link

Choose a reason for hiding this comment

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

We usually work with relativedelta (from dateutil.relativedelta import relativedelta):

Suggested change
default=lambda self: fields.Date.today() + timedelta(days=90)
default=lambda self: fields.Date.today() + relativedelta(months=3)

<field name="arch" type="xml">
<search string="Property">

<field name="name" string="Title"/>
Copy link

Choose a reason for hiding this comment

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

Instead of overriding the string here (which is fine, by the way), you could also set it directly in the model (in the py file).

Comment on lines 5 to 11
'summary': """
Starting module for "Master the Odoo web framework, chapter 1: Build a new application"
""",

'description': """
Starting module for "Master the Odoo web framework, chapter 1: Build a new application"
""",
Copy link

Choose a reason for hiding this comment

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

These don't seem quite right :)

Comment on lines 4 to 19
<!-- Root Menu -->
<menuitem
id="estate_menu_root"
name="Real Estate"/>

<!-- First Level -->
<menuitem
id="estate_menu_properties"
name="Properties"
parent="estate_menu_root"/>

<!-- Action Menu -->
<menuitem
id="estate_menu_properties_action"
parent="estate_menu_properties"
action="estate_property_action"/>
Copy link

Choose a reason for hiding this comment

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

If you nest the menu items, you won't need to explicitly specify a parent anymore.

'category': 'Tutorials',
'version': '0.1',
'application': True,
'installable': True,
Copy link

Choose a reason for hiding this comment

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

FYI, I believe this is the default, so you don't strictly need to specify it.

Starting module for "Master the Odoo web framework, chapter 1: Build a new application"
""",

'author': "Odoo",
Copy link

Choose a reason for hiding this comment

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

In general, we put Odoo S.A. here, but it doesn't matter much in this context 😄

'views/estate_menus.xml'
],

'assets': {},
Copy link

Choose a reason for hiding this comment

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

Feel free to remove this since it's empty.

settings.json Outdated
Copy link

Choose a reason for hiding this comment

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

This is your own config file, so it should probably stay local :) Maybe add it to your gitignore file to avoid including it in your PRs.

Copy link

@ltinel ltinel left a comment

Choose a reason for hiding this comment

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

Good job :)

'estate.property',
string="Property",
required=True,
ondelete='cascade'
Copy link

Choose a reason for hiding this comment

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

Good idea :) Although maybe not ideal in practice for tracking offers.

Comment on lines 23 to 26
<menuitem
id="estate_menu_type"
name="Property Type"
parent="estate_menu_root"/>
Copy link

Choose a reason for hiding this comment

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

I believe this menu item (and the "Property Tags" one) should be under a "Settings" menu item. But all of this would be simpler if you use nesting instead of the "parent" field :)

Comment on lines 28 to 33
<!-- Property Type Action -->
<record id="estate_property_type_action" model="ir.actions.act_window">
<field name="name">Property Type</field>
<field name="res_model">estate.property.type</field>
<field name="view_mode">list,form</field>
</record>
Copy link

Choose a reason for hiding this comment

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

Actions should be in the "_views" file of the corresponding record. (same below)

<field name="expected_price"/>
<field name="selling_price"/>
<field name="available_from"/>
<field name="name" width="100px"/>
Copy link

Choose a reason for hiding this comment

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

It's fine to set a width, but hardcoding pixels can be risky, since users can have different screen sizes (monitor, laptop, smartphone, ...)


<group>
<group>
<field name="tag_ids" widget="many2many_tags"/>
Copy link

Choose a reason for hiding this comment

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

Could you move this in the oe_title div?

Comment on lines 74 to 78
<list editable="bottom">
<field name="price"/>
<field name="partner_id"/>
<field name="status"/>
</list>
Copy link

Choose a reason for hiding this comment

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

No need for this :) You can put the editable="bottom" attribute directly in the list view of the offer (in estate/views/estate_property_offer_views.xml)

Suggested change
<list editable="bottom">
<field name="price"/>
<field name="partner_id"/>
<field name="status"/>
</list>

Copy link

@ltinel ltinel left a comment

Choose a reason for hiding this comment

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

Good job :)

Comment on lines 43 to 50
@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
Copy link

Choose a reason for hiding this comment

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

def _compute_best_offer(self):
for record in self:
if record.offer_ids:
record.best_offer = max(record.offer_ids.mapped('price'))
Copy link

Choose a reason for hiding this comment

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

I think this will fail if there are no offers. Could you use a default in that case?

Copy link

Choose a reason for hiding this comment

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

My bad, I didn't see you already checked that. However, there's a more elegant way to do the same

- if record.offer_ids:
-     record.best_offer = max(record.offer_ids.mapped('price'))
- else:
-     record.best_offer = 0.0
+ record.best_offer = max(record.offer_ids.mapped('price'), default=0)

Comment on lines 152 to 159
@api.constrains('expected_price', 'selling_price')
def _check_prices_positive(self):
"""Ensure expected_price > 0 and selling_price >= 0"""
for record in self:
if float_compare(record.expected_price, 0.0, precision_rounding=0.01) <= 0:
raise ValidationError("The expected price must be strictly positive.")
if record.selling_price and float_compare(record.selling_price, 0.0, precision_rounding=0.01) < 0:
raise ValidationError("The selling price cannot be negative.")
Copy link

Choose a reason for hiding this comment

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

Could you convert those to SQL constraints?


@api.depends('living_area', 'garden_area')
def _compute_total_area(self):
for record in self:
Copy link

Choose a reason for hiding this comment

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

Ideally, we'd give a more descriptive name for the record, e.g. property:

Suggested change
for record in self:
for property in self:

Same below and in the other models.

# Check if another offer was already accepted
if property.offer_ids.filtered(lambda o: o.status == 'accepted'):
raise UserError("Only one offer can be accepted per property.")
offer.status = 'accepted'
Copy link

Choose a reason for hiding this comment

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

Could you also mark all other offers as refused?

Comment on lines 11 to 17
@api.constrains('name')
def _check_name_unique(self):
for record in self:
# Recherche d'autres tags avec le même nom
existing = self.search([('name', '=', record.name), ('id', '!=', record.id)])
if existing:
raise ValidationError("The property tag name must be unique.")
Copy link

Choose a reason for hiding this comment

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

This can be an SQL constraint.

Comment on lines 11 to 16
@api.constrains('name')
def _check_name_unique(self):
for record in self:
existing = self.search([('name', '=', record.name), ('id', '!=', record.id)])
if existing:
raise ValidationError("The property type name must be unique.")
Copy link

Choose a reason for hiding this comment

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

Same here.

Comment on lines +10 to +11
<button name="action_accept" type="object" string="Accept" class="btn-success"/>
<button name="action_refuse" type="object" string="Refuse" class="btn-danger"/>
Copy link

Choose a reason for hiding this comment

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

I believe these buttons should be in the list view?

Comment on lines 78 to 86
<list>
<field name="price"/>
<field name="partner_id"/>
<field name="validity" width="200px"/>
<field name="date_deadline" width="200px"/>
<button name="action_accept" type="object" string="Accept" class="btn-success"/>
<button name="action_refuse" type="object" string="Refuse" class="btn-danger"/>
<field name="status"/>
</list>
Copy link

Choose a reason for hiding this comment

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

No need for this :) Just specifying the field like <field name="offer_ids"/> will render the list view

.gitignore Outdated
Copy link

Choose a reason for hiding this comment

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

Could you also put your own .gitignore in your .gitignore 😅 (and remove the .gitignore, .flake8, and settings.json files from the PR)

Comment on lines 50 to 69
class EstateProperty(models.Model):
_name = "estate.property"
_description = "Real Estate Property"

name = fields.Char(required=True)
expected_price = fields.Float()
state = fields.Selection(
[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('canceled', 'Canceled')
],
default='new'
)
property_type_id = fields.Many2one(
'estate.property.type',
string='Property Type'
)
Copy link

Choose a reason for hiding this comment

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

This should probably be removed :)

Copy link

@ltinel ltinel left a comment

Choose a reason for hiding this comment

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

Nicely done :)

Comment on lines 77 to 78
if isinstance(vals_list, dict):
vals_list = [vals_list]
Copy link

Choose a reason for hiding this comment

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

Not sure this is necessary

<field name="arch" type="xml">

<!-- On cible le notebook principal du formulaire -->
<xpath expr="//notebook" position="inside">
Copy link

Choose a reason for hiding this comment

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

Alternative (and shorter) syntax:

Suggested change
<xpath expr="//notebook" position="inside">
<notebook position="inside">

Comment on lines 12 to 23
<list string="Properties"
decoration-success="state in ('offer_accepted')"
decoration-bf="state == 'offer_accepted'">
<field name="name" width="100px"/>
<field name="property_type_id" width="100px"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="bedrooms" width="100px"/>
<field name="living_area" width="200px"/>
<field name="expected_price" width="200px"/>
<field name="selling_price" width="200px"/>
<field name="available_from" width="200px" optional="hide"/>
</list>
Copy link

Choose a reason for hiding this comment

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

No need for this :)

Suggested change
<list string="Properties"
decoration-success="state in ('offer_accepted')"
decoration-bf="state == 'offer_accepted'">
<field name="name" width="100px"/>
<field name="property_type_id" width="100px"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="bedrooms" width="100px"/>
<field name="living_area" width="200px"/>
<field name="expected_price" width="200px"/>
<field name="selling_price" width="200px"/>
<field name="available_from" width="200px" optional="hide"/>
</list>

)

_check_selling_price_positive = models.Constraint(
"CHECK(selling_price > 0)",
Copy link

Choose a reason for hiding this comment

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

Suggested change
"CHECK(selling_price > 0)",
"CHECK(selling_price >= 0)",

_check_selling_price_min = models.Constraint(
"""
CHECK(
selling_price > 0
Copy link

Choose a reason for hiding this comment

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

This one is already checked below.

if not offer.buyer_id or not offer.selling_price:
continue

invoice_vals = {
Copy link

Choose a reason for hiding this comment

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

I think invoices need a journal id

}

invoice = self.env["account.move"].create(invoice_vals)
print(f"Invoice created: {invoice.name}")
Copy link

Choose a reason for hiding this comment

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

This can be removed :)

Comment on lines 37 to 41
create="true"
edit="true"
delete="true"
class="o_kanban_small_column"
quick_create="false"
Copy link

Choose a reason for hiding this comment

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

Do you really need these?

<field name="best_offer"/>
<field name="state"/>
<field name="tag_ids"/>
<templates>
Copy link

Choose a reason for hiding this comment

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

I'm not sure these are needed either, except state

Copy link
Author

Choose a reason for hiding this comment

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

In chapter 14 on the picture of the Kanban view there are these fields are presents

Copy link

@ltinel ltinel Feb 20, 2026

Choose a reason for hiding this comment

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

Yes, but the fields you set here aren't displayed. The view is defined below, within the template tag. The main reason for adding fields here, is to let the framework know that we need to access them in the view, and the only field you need to access is state (when you call record.state.raw_value). The other fields are only accessed in <field name="..."/> statements, which are automatically handled by the framework.

Comment on lines 64 to 66
# Refuse others offers
other_offers = property.offer_ids.filtered(lambda o: o != offer)
other_offers.write({'status': 'refused'})
Copy link

Choose a reason for hiding this comment

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

Optional: you could also just refuse all offers (including the one you're about to accept, since you're accepting it after):

Suggested change
# Refuse others offers
other_offers = property.offer_ids.filtered(lambda o: o != offer)
other_offers.write({'status': 'refused'})
property.offer_ids.status = 'refused'

def _check_selling_price_min(self):
for record in self:
if (
record.selling_price and record.expected_price and record.selling_price < record.expected_price * 0.9
Copy link

Choose a reason for hiding this comment

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

Could you use the float_compare and float_is_zero utilities here? It's preferable to use "approximate" comparisons when working with floats.

}),
Command.create({
"name": "Administrative fees",
"name": f"Selling price for {property.name}",
Copy link

Choose a reason for hiding this comment

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

I think this should still be "Administrative fees" (and the price 100) 😅

- Initialize module structure
- Create estate.property model
- Add basic fields and attributes
- Add security access rules
- Add action and menus for UI
Add custom views for the `estate.property` model to improve user interface
and data interaction. The commit introduces:

- A list view showing key fields: name, postcode, bedrooms, living area,
  expected price, selling price, and available date.
- A form view with structured groups and a notebook for detailed information
  including description, property features, and reserved fields like active
  and state.
- A search view with filters and group-by options to easily find and categorize
  properties, including an "Available" filter for new and offer_received
  properties, and a group-by on postcode.

This improves the usability of the real estate module, replacing default
auto-generated views with tailored ones that match business requirements.

Closes task-6
Introduce estate.property.type and estate.property.tag models.
Extend estate.property with:

- Many2one relation to property type
- Many2many relation to property tags
- One2many relation to property offers

Add corresponding views and menus where required.

Fix flake8 issues to satisfy ci/style checks.
- estate.property:
  * added total_area computed field (living_area + garden_area)
  * added best_price computed field (maximum of offer prices)
  * added @onchange on 'garden' to set default garden_area=10 and garden_orientation='north'

- estate.property.offer:
  * added validity_date computed field (with inverse)
Chapter 9: link business logic to UI actions

- estate.property:
  * Add "Cancel" and "Sold" buttons in form view header.
  * Enforce state constraints: a cancelled property cannot be sold and a sold property cannot be cancelled.
  * Raise UserError when state transitions are invalid.

- estate.property.offer:
  * Add "Accept" and "Refuse" buttons.
  * Accepting an offer sets the buyer and selling price on the related property.
  * Only one offer can be accepted per property.

- Updated views to include buttons in header sections.
- Added Python methods implementing the business logic for these actions.
- Ensure expected_price and selling_price are positive
- Prevent selling_price from being lower than 90% of expected_price
- Add uniqueness constraints on property type names
- Add uniqueness constraints on property tag names
-Move many2many tags
-Add inline property type class
-Add widget bar
- Color decorations for property and offer list views based on state/status
- Editable list views for offers and tags
- Hide availability date by default in property list
- Set default 'Available' filter in property search
- Make living area search return properties with area >= entered value
- Add stat button on property type form to show related offers
- Refine offer count display and button layout
…perties

- Prevent deletion of properties unless state is 'New' or 'Cancelled'
  (implemented using @api.ondelete)
- Override create() on estate.offer:
  - Set property state to 'Offer Received' when an offer is created
  - Prevent creation of an offer with a lower price than existing offers
- Extend res.users model:
  - Add property_ids One2many field linked to estate.property
  - Add domain to display only available properties
- Inherit base.view_users_form to display property_ids
  in a new notebook page on the user form
- Filter the offer button to show only offers related to the current property type
- Add SQL constraints for expected_price and selling_price to enforce positivity
- Ensure property type name is unique at the database level
- Clean up form view: removed unnecessary buttons and improved layout
- Updated .gitignore to exclude local config files (.settings.json, .flake8, .vscode)
…perty sold

- Added estate_account module as a link between estate and account
- Inherited estate.property and overridden action_sold to create customer invoices
- Automatically generates invoice lines: 6% commission + 100 administrative fees
- Ensures invoices are created only if buyer and selling_price are defined
- Updated __manifest__ and module structure for proper installation and visibility in Apps
- Introduce minimal kanban template using QWeb
- Display name, tags and pricing information
- Add conditional rendering for best offer and selling price
- Enable default grouping by property type
- Disable drag & drop on kanban records
This commit applies changes following the review comments:
- Mark all other offers as refused (as suggested, though may not be ideal for tracking)
- Removed empty or unnecessary sections
- Corrected author information where needed (typically Odoo S.A.)
- Fixed minor issues flagged in previous review comments
- Reverted test changes or irrelevant edits
This commit fixes the invoice calculation to use the correct estate price and ensures the license field is properly included.
- Use Python constraint instead of SQL constraint
- Switch to @api.model_create_multi (Odoo 17)
- Remove unnecessary isinstance check
- Fix invoice creation (journal_id required)
- Minor cleanup and best practice alignment
- Format code to follow Odoo guidelines
- Remove unused imports
- Improve indentation and spacing
- Minor refactoring without functional changes
- Fix test errors
- Adjust code to satisfy CI checks
- No functional changes
- Cleaned up obsolete fields
- No functional changes
- Restore "Administrative fees" invoice line with fixed amount (100.0)
- Use float_compare for selling price constraint to handle rounding safely
- Prevents false ValidationErrors and ensures invoice lines follow business logic
@jupao-odoo jupao-odoo force-pushed the 19.0-add-editor-awesomeclicker-jupao branch from 3a87b1b to e205ffa Compare February 20, 2026 12:06
- Keep `status` field invisible (`column_invisible="True"`)
- Use it for row decorations (`decoration-success` / `decoration-danger`)
- Drive button visibility logic in list and form views
- Prevent breaking UI features if the field is removed or shown
-removed invisible columns
-removed comments
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