[16.0] [ADD] project_required_field_by_stage: New module#1650
[16.0] [ADD] project_required_field_by_stage: New module#1650OCA-git-bot merged 4 commits intoOCA:16.0from
Conversation
f51f3e4 to
709ab1c
Compare
d973054 to
60b2afd
Compare
|
Code LGTM. |
|
Tested its functionality on runboat, and it does what it's supposed to do: When you have added fields in the Required Field section of a stage, a task that gets put in that stage will have those fields highlighted as required. |
|
|
||
| @api.constrains("stage_id") | ||
| def _check_stage_id_(self): | ||
| for this in self: |
There was a problem hiding this comment.
Maybe I miss something, but I do not see why the inner loops are necessary. A project can only be in one stage, so why would this be wrong?
@api.constrains("stage_id")
def _check_stage_id_(self):
for this in self:
required_fields = this.stage_id.required_field_ids
if not required_fields:
continue
for field in required_fields:
if hasattr(this, field.name) and not getattr(this, field.name):
raise UserError(
_(
"Field '%(field)s' is mandatory in stage '%(stage)s'."
)
% (
{
"field": field.display_name.split(" (")[0],
"stage": this.stage_id.display_name,
}
)
)
There was a problem hiding this comment.
Ah yes I agree, I see that stage_id is a Many2one field
There was a problem hiding this comment.
Every record has one stage. we only need to check mandatory fields for that one stage, not all stages every time. thank you @NL66278 , i will simplify this , by removing the loop.
There was a problem hiding this comment.
I re-reviewed this. @NL66278
stages = this.stage_id.filtered(lambda x: x.required_field_ids) makes no sense. and some other filtering makes no sense either.
All we want in this costrains is to verify that on stage change all mandatory fields specified for that stage are there.
Added a fix commit, please re-check.
luisDIXMIT
left a comment
There was a problem hiding this comment.
When I remove required fields from a stage, it still requires those fields.
| { | ||
| "name": "Project Required Field By Stage", | ||
| "summary": """ | ||
| KMEE""", |
There was a problem hiding this comment.
Can you add a more detailed summary, please?
There was a problem hiding this comment.
Done. thank you for reviewing. if module is satisfactory please leave an approval.
|
PR tittle must be "[16.0] [ADD] project_required_field_by_stage: New module" |
0631016 to
a880627
Compare
| @@ -0,0 +1,3 @@ | |||
| * KMEE (https://kmee.com.br/): | |||
|
|
|||
| * Tiago Amaral | |||
There was a problem hiding this comment.
Enough enhancements to add yourself to contributors list
| if attrs: | ||
| if attrs.get("required"): | ||
| attrs["required"] = [ | ||
| "|", | ||
| ("stage_id", "in", stages_with_field.ids), | ||
| ] + attrs["required"] | ||
| else: | ||
| attrs["required"] = [ | ||
| ("stage_id", "in", stages_with_field.ids) | ||
| ] | ||
| else: | ||
| attrs["required"] = [("stage_id", "in", stages_with_field.ids)] |
There was a problem hiding this comment.
All this can be done much more concise (and without repeating three times the same condition added):
` ``
from odoo.osv.expression import OR # Obviously this at the top of the python file
...
required_domain = attrs.get("required", [])
attrs["required"] = OR([required_domain, [("stage_id", "in", stages_with_field.ids)]])
No `if` or `else` needed!
| mandatory fields associated.""", | ||
| "version": "16.0.1.0.0", | ||
| "license": "AGPL-3", | ||
| "author": "KMEE,Odoo Community Association (OCA)", |
There was a problem hiding this comment.
There is as yet no maintainer defined. Maybe you can add yourself as maintainer? This will give the possibility to merge stuff, even if you are not a PSC member for project and you will be notified of PR's. Of course this brings responsibility as well.
alexey-pelykh
left a comment
There was a problem hiding this comment.
Thanks for reviving this from #1341 and for the work on tests and the view cache key -- this is a useful module. A few things need attention before merge:
1. _get_view attrs logic -- apply NL66278's suggestion
As @NL66278 pointed out, the three-branch if/elif/else for building the required domain can be collapsed using odoo.osv.expression.OR:
from odoo.osv.expression import OR
...
required_domain = attrs.get("required", [])
attrs["required"] = OR([required_domain, [("stage_id", "in", stages_with_field.ids)]])This removes all the branching and is the idiomatic Odoo way to combine domains.
2. _check_stage_id_ -- redundant search
this.stage_id.required_field_ids is already a recordset of ir.model.fields. The .sudo().search(["id", "in", ...ids]) re-queries the same records for no reason. Simplify to:
for field in this.stage_id.required_field_ids:The .sudo() is also unnecessary here since you already have access to the stage through the task.
3. Falsy check fails on numeric zero
not getattr(this, field.name) treats 0 and 0.0 as empty. If someone marks an integer or float field as required and sets it to 0, the constraint will incorrectly reject it. Consider checking the field type: for relational fields check not value, for others check value is False (which is how Odoo represents "not set" for scalar fields).
4. View cache invalidation bug
@luisDIXMIT reported that removing required fields from a stage still enforces them. The _get_view_cache_key only includes field names but not which stages they belong to. If stage A requires user_ids and you remove it, but stage B also requires user_ids, the cache key is unchanged and the stale view is served. Include stage IDs in the cache key.
5. Minor items
import json as simplejson-- misleading alias, just useimport jsonandjson.dumps.- Method name
_check_stage_id_has a trailing underscore -- likely a typo. Rename to_check_required_fields_by_stagefor clarity. - Missing
"installable": Truein manifest (OCA convention).
Overall the concept is solid and the test coverage is decent. Looking forward to the next iteration.
| # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
|
||
| import ast | ||
| import json as simplejson |
There was a problem hiding this comment.
import json as simplejson is a misleading alias (simplejson was a different library). Just use import json and call json.dumps below.
| ) | ||
| for node in arch.xpath("//field[@name='%s']" % field.name): | ||
| attrs = ast.literal_eval(node.attrib.get("attrs", "{}")) | ||
| if attrs: |
There was a problem hiding this comment.
As @NL66278 suggested -- this whole if/elif/else block can be replaced with:
from odoo.osv.expression import OR
...
required_domain = attrs.get("required", [])
attrs["required"] = OR([required_domain, [("stage_id", "in", stages_with_field.ids)]])No branching needed.
There was a problem hiding this comment.
yes attrs , previously in node.attrib.get defaults to {} so it is safe to remove all branches.
|
|
||
| @api.constrains("stage_id") | ||
| def _check_stage_id_(self): | ||
| for this in self: |
There was a problem hiding this comment.
Trailing underscore on _check_stage_id_ looks like a typo. Consider _check_required_fields_by_stage which also better describes the intent.
| def _check_stage_id_(self): | ||
| for this in self: | ||
| if this.stage_id.required_field_ids: | ||
| fields = ( |
There was a problem hiding this comment.
This search is redundant -- this.stage_id.required_field_ids is already the ir.model.fields recordset you want. The .sudo().search(...) just re-fetches the same records. Replace the whole block with:
for field in this.stage_id.required_field_ids:| .search([("id", "in", this.stage_id.required_field_ids.ids)]) | ||
| ) | ||
| for field in fields: | ||
| if hasattr(this, field.name) and not getattr(this, field.name): |
There was a problem hiding this comment.
not getattr(this, field.name) is falsy for 0 and 0.0, so a numeric field set to zero would incorrectly fail validation. For scalar fields, Odoo uses False to mean "not set" -- check getattr(this, field.name) is False for non-relational types, or inspect field.ttype to pick the right emptiness check.
There was a problem hiding this comment.
Managed this in refactored is_empty function for clarity, where i check the ttype and do the appropriate check.
| self.env["project.task.type"] | ||
| .search([("required_field_ids", "!=", False)]) | ||
| .mapped("required_field_ids.name") | ||
| ) |
There was a problem hiding this comment.
Cache key only includes field names, not which stages they belong to. Removing a required field from one stage won't invalidate the cache if another stage still requires the same field name. Include stage IDs or write_date in the key to ensure proper invalidation on config changes.
3773f27 to
035ae86
Compare
|
@alexey-pelykh @NL66278 all review comments and simplifications implemented specifically in commit 4f16056 . Added a function that correctly evaluates what "not set" means for the different types of fields. Added tests to cover those code branches too and keep codeCov coverage percentage from dipping. if i had made {boolean_field} = true or false = set that would had made adding booleans to restrictions on stages effectively useless. |
067b7a3 to
b67aa25
Compare
b67aa25 to
4f16056
Compare
alexey-pelykh
left a comment
There was a problem hiding this comment.
All review comments from the previous round have been addressed in commit 4f16056:
import json as simplejson-- replaced with cleanimport json._get_viewattrs logic -- collapsed toOR([required_domain, ...])as suggested by @NL66278.- Redundant search removed -- constraint now iterates
this.stage_id.required_field_idsdirectly. - Method renamed --
_check_stage_id_is now_check_required_fields_by_stage. - Type-aware emptiness check -- new
_is_empty()function correctly differentiates byfield.ttype. The boolean interpretation (False= "not set") is reasonable for this use case. - Cache key fix --
_get_view_cache_keynow includes(stage.id, field.name)tuples, so config changes properly invalidate the cached view. "installable": Trueadded to manifest.
Tests cover the constraint, view injection for multiple field types, and the OR domain merging. Looks good.
|
This PR has the |
| ) | ||
| self.project_task_1.write( | ||
| { | ||
| "stage_id": self.project_task_type_2.id, |
There was a problem hiding this comment.
This write can be combined with the previous write, which actually would make the test stronger, as writing the required field for a stage together with the stage should succeed.
| "name": "Project Stage 1", | ||
| "project_ids": [(4, self.project_project_1.id)], | ||
| } | ||
| ) |
There was a problem hiding this comment.
Just a suggestion for more concise code:
from odoo.fields import Command
...
required_field_names = [
"planned_hours", # integer
"project_id", # m20
"user_ids", # m2m
"active", # bool
]
....
"required_fields": [
Command.link(self.env.ref("project.field_project_task__%s" % field_name).id)
for field_name in required_field_names
]
|
@gfcapalbo Please squash your own commits into one. |
|
On my way to merge this fine PR! |
|
@NL66278 I missed your comment before starting the merge of the module : / |
|
Congratulations, your PR was merged at 1486c9f. Thanks a lot for contributing to OCA. ❤️ |
reopening #1341 with original commits.
Added changes requested in that original MR-.