[IMP] awesomeclicker: add editor metadata in Python file#1173
[IMP] awesomeclicker: add editor metadata in Python file#1173jupao-odoo wants to merge 22 commits intoodoo:19.0from
Conversation
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.
ltinel
left a comment
There was a problem hiding this comment.
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?
awesome_clicker/__manifest__.py
Outdated
| """, | ||
|
|
||
| 'author': "Odoo", | ||
| 'editor': "jupao", |
There was a problem hiding this comment.
No need for this change :) I guess it was just a test?
estate/models/estate_property.py
Outdated
|
|
||
| available_from = fields.Date( | ||
| copy=False, | ||
| default=lambda self: fields.Date.today() + timedelta(days=90) |
There was a problem hiding this comment.
We usually work with relativedelta (from dateutil.relativedelta import relativedelta):
| 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"/> |
There was a problem hiding this comment.
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).
| '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" | ||
| """, |
estate/views/estate_menus.xml
Outdated
| <!-- 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"/> |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
FYI, I believe this is the default, so you don't strictly need to specify it.
estate/__manifest__.py
Outdated
| Starting module for "Master the Odoo web framework, chapter 1: Build a new application" | ||
| """, | ||
|
|
||
| 'author': "Odoo", |
There was a problem hiding this comment.
In general, we put Odoo S.A. here, but it doesn't matter much in this context 😄
estate/__manifest__.py
Outdated
| 'views/estate_menus.xml' | ||
| ], | ||
|
|
||
| 'assets': {}, |
There was a problem hiding this comment.
Feel free to remove this since it's empty.
settings.json
Outdated
There was a problem hiding this comment.
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.
| 'estate.property', | ||
| string="Property", | ||
| required=True, | ||
| ondelete='cascade' |
There was a problem hiding this comment.
Good idea :) Although maybe not ideal in practice for tracking offers.
estate/views/estate_menus.xml
Outdated
| <menuitem | ||
| id="estate_menu_type" | ||
| name="Property Type" | ||
| parent="estate_menu_root"/> |
There was a problem hiding this comment.
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 :)
estate/views/estate_menus.xml
Outdated
| <!-- 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> |
There was a problem hiding this comment.
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"/> |
There was a problem hiding this comment.
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"/> |
There was a problem hiding this comment.
Could you move this in the oe_title div?
| <list editable="bottom"> | ||
| <field name="price"/> | ||
| <field name="partner_id"/> | ||
| <field name="status"/> | ||
| </list> |
There was a problem hiding this comment.
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)
| <list editable="bottom"> | |
| <field name="price"/> | |
| <field name="partner_id"/> | |
| <field name="status"/> | |
| </list> |
estate/models/estate_property.py
Outdated
| @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 |
There was a problem hiding this comment.
Could you organize your methods and fields according to https://www.odoo.com/documentation/master/contributing/development/coding_guidelines.html#symbols-and-conventions ?
estate/models/estate_property.py
Outdated
| def _compute_best_offer(self): | ||
| for record in self: | ||
| if record.offer_ids: | ||
| record.best_offer = max(record.offer_ids.mapped('price')) |
There was a problem hiding this comment.
I think this will fail if there are no offers. Could you use a default in that case?
There was a problem hiding this comment.
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)
estate/models/estate_property.py
Outdated
| @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.") |
There was a problem hiding this comment.
Could you convert those to SQL constraints?
estate/models/estate_property.py
Outdated
|
|
||
| @api.depends('living_area', 'garden_area') | ||
| def _compute_total_area(self): | ||
| for record in self: |
There was a problem hiding this comment.
Ideally, we'd give a more descriptive name for the record, e.g. property:
| 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' |
There was a problem hiding this comment.
Could you also mark all other offers as refused?
estate/models/estate_property_tag.py
Outdated
| @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.") |
| @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.") |
| <button name="action_accept" type="object" string="Accept" class="btn-success"/> | ||
| <button name="action_refuse" type="object" string="Refuse" class="btn-danger"/> |
There was a problem hiding this comment.
I believe these buttons should be in the list view?
| <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> |
There was a problem hiding this comment.
No need for this :) Just specifying the field like <field name="offer_ids"/> will render the list view
.gitignore
Outdated
There was a problem hiding this comment.
Could you also put your own .gitignore in your .gitignore 😅 (and remove the .gitignore, .flake8, and settings.json files from the PR)
| 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' | ||
| ) |
| if isinstance(vals_list, dict): | ||
| vals_list = [vals_list] |
estate/views/res_users_views.xml
Outdated
| <field name="arch" type="xml"> | ||
|
|
||
| <!-- On cible le notebook principal du formulaire --> | ||
| <xpath expr="//notebook" position="inside"> |
There was a problem hiding this comment.
Alternative (and shorter) syntax:
| <xpath expr="//notebook" position="inside"> | |
| <notebook position="inside"> |
estate/views/res_users_views.xml
Outdated
| <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> |
There was a problem hiding this comment.
No need for this :)
| <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> |
estate/models/estate_property.py
Outdated
| ) | ||
|
|
||
| _check_selling_price_positive = models.Constraint( | ||
| "CHECK(selling_price > 0)", |
There was a problem hiding this comment.
| "CHECK(selling_price > 0)", | |
| "CHECK(selling_price >= 0)", |
estate/models/estate_property.py
Outdated
| _check_selling_price_min = models.Constraint( | ||
| """ | ||
| CHECK( | ||
| selling_price > 0 |
| if not offer.buyer_id or not offer.selling_price: | ||
| continue | ||
|
|
||
| invoice_vals = { |
| } | ||
|
|
||
| invoice = self.env["account.move"].create(invoice_vals) | ||
| print(f"Invoice created: {invoice.name}") |
| create="true" | ||
| edit="true" | ||
| delete="true" | ||
| class="o_kanban_small_column" | ||
| quick_create="false" |
| <field name="best_offer"/> | ||
| <field name="state"/> | ||
| <field name="tag_ids"/> | ||
| <templates> |
There was a problem hiding this comment.
I'm not sure these are needed either, except state
There was a problem hiding this comment.
In chapter 14 on the picture of the Kanban view there are these fields are presents
There was a problem hiding this comment.
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.
| # Refuse others offers | ||
| other_offers = property.offer_ids.filtered(lambda o: o != offer) | ||
| other_offers.write({'status': 'refused'}) |
There was a problem hiding this comment.
Optional: you could also just refuse all offers (including the one you're about to accept, since you're accepting it after):
| # Refuse others offers | |
| other_offers = property.offer_ids.filtered(lambda o: o != offer) | |
| other_offers.write({'status': 'refused'}) | |
| property.offer_ids.status = 'refused' |
estate/models/estate_property.py
Outdated
| 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 |
There was a problem hiding this comment.
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}", |
There was a problem hiding this comment.
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
3a87b1b to
e205ffa
Compare
- 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

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.