Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0][IMP] base_tier_validation: support computed state field #970

Open
wants to merge 1 commit into
base: 16.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions base_tier_validation/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ development.

See `purchase_tier_validation <https://github.com/OCA/purchase-workflow>`_ as an example of implementation.

Additionally, if your state field is a (stored) computed field, you need
to set ``_tier_validation_state_field_is_computed`` to ``True`` in your
model Python file, and you will want to add the dependent fields of the
compute method in ``_get_after_validation_exceptions`` and
``_get_under_validation_exceptions``.

**Table of contents**

.. contents::
Expand Down Expand Up @@ -233,6 +239,8 @@ Contributors

* Houzéfa Abbasbhay

- Stefan Rijnhart <stefan@opener.amsterdam>

Maintainers
~~~~~~~~~~~

Expand Down
48 changes: 44 additions & 4 deletions base_tier_validation/models/tier_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ast import literal_eval

from lxml import etree
from psycopg2.extensions import AsIs

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
Expand All @@ -21,6 +22,7 @@ class TierValidation(models.AbstractModel):

_tier_validation_buttons_xpath = "/form/header/button[last()]"
_tier_validation_manual_config = True
_tier_validation_state_field_is_computed = False

_state_field = "state"
_state_from = ["draft"]
Expand Down Expand Up @@ -326,6 +328,39 @@ def _get_fields_to_write_validation(self, vals, records_exception_function):
return allowed_field_names, not_allowed_field_names

def write(self, vals):
self._tier_validation_check_state_on_write(vals)
self._tier_validation_check_write_allowed(vals)
self._tier_validation_check_write_remove_reviews(vals)
return super().write(vals)

def _write(self, vals):
if self._tier_validation_state_field_is_computed:
self._tier_validation_check_state_on_write(vals)
self._tier_validation_check_write_remove_reviews(vals)
return super()._write(vals)

def _tier_validation_get_current_state_value(self):
"""Get the current value from the cache or the database.

If the field is set in a computed method, the value in the cache will
already be the updated value, so we need to revert to the raw data.
"""
self.ensure_one()
if self._tier_validation_state_field_is_computed and isinstance(self.id, int):
self.env.cr.execute(
"select %(field)s from %(table)s where id = %(res_id)s",
{
"field": AsIs(self._state_field),
"table": AsIs(self._table),
"res_id": self.id,
},
)
rows = self.env.cr.fetchall()
if rows:
return rows[0][0]
return self[self._state_field]

def _tier_validation_check_state_on_write(self, vals):
for rec in self:
if rec._check_state_conditions(vals):
if rec.need_validation:
Expand All @@ -346,6 +381,9 @@ def write(self, vals):
"one record."
)
)

def _tier_validation_check_write_allowed(self, vals):
for rec in self:
# Write under validation
if (
rec.review_ids
Expand Down Expand Up @@ -377,7 +415,7 @@ def write(self, vals):
if (
rec._get_validation_exceptions(add_base_exceptions=False)
and rec.validation_status == "validated"
and getattr(rec, self._state_field)
and rec._tier_validation_get_current_state_value()
in (self._state_to + [self._cancel_state])
and not rec._check_allow_write_after_validation(vals)
and not rec._context.get("skip_validation_check")
Expand All @@ -399,17 +437,19 @@ def write(self, vals):
"allowed_fields": "\n- ".join(allowed_fields),
}
)

def _tier_validation_check_write_remove_reviews(self, vals):
for rec in self:
if rec._allow_to_remove_reviews(vals):
rec.mapped("review_ids").unlink()
return super(TierValidation, self).write(vals)

def _allow_to_remove_reviews(self, values):
"""Method for deciding whether the elimination of revisions is necessary."""
self.ensure_one()
state_to = values.get(self._state_field)
if not state_to:
return False
state_from = self[self._state_field]
state_from = self._tier_validation_get_current_state_value()
# If you change to _cancel_state
if state_to in (self._cancel_state):
return True
Expand All @@ -421,7 +461,7 @@ def _allow_to_remove_reviews(self, values):
def _check_state_from_condition(self):
return self.env.context.get("skip_check_state_condition") or (
self._state_field in self._fields
and getattr(self, self._state_field) in self._state_from
and self._tier_validation_get_current_state_value() in self._state_from
)

def _check_state_conditions(self, vals):
Expand Down
1 change: 1 addition & 0 deletions base_tier_validation/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
* `XCG Consulting <https://xcg-consulting.fr>`_:

* Houzéfa Abbasbhay
* Stefan Rijnhart <stefan@opener.amsterdam>
5 changes: 5 additions & 0 deletions base_tier_validation/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ purchase orders, sales orders, budgets, expenses...).
development.

See `purchase_tier_validation <https://github.com/OCA/purchase-workflow>`_ as an example of implementation.

Additionally, if your state field is a (stored) computed field, you need to
set `_tier_validation_state_field_is_computed` to `True` in your model Python
file, and you will want to add the dependent fields of the compute method
in `_get_after_validation_exceptions` and `_get_under_validation_exceptions`.
6 changes: 6 additions & 0 deletions base_tier_validation/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,11 @@ <h1 class="title">Base Tier Validation</h1>
<p><strong>Note:</strong> To be able to use this module in a new model you will need some
development.</p>
<p>See <a class="reference external" href="https://github.com/OCA/purchase-workflow">purchase_tier_validation</a> as an example of implementation.</p>
<p>Additionally, if your state field is a (stored) computed field, you need
to set <tt class="docutils literal">_tier_validation_state_field_is_computed</tt> to <tt class="docutils literal">True</tt> in your
model Python file, and you will want to add the dependent fields of the
compute method in <tt class="docutils literal">_get_after_validation_exceptions</tt> and
<tt class="docutils literal">_get_under_validation_exceptions</tt>.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
Expand Down Expand Up @@ -586,6 +591,7 @@ <h2><a class="toc-backref" href="#toc-entry-21">Contributors</a></h2>
<li>Houzéfa Abbasbhay</li>
</ul>
</li>
<li>Stefan Rijnhart &lt;<a class="reference external" href="mailto:stefan&#64;opener.amsterdam">stefan&#64;opener.amsterdam</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
Expand Down
101 changes: 77 additions & 24 deletions base_tier_validation/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,49 +19,54 @@ def setUpClass(cls):
TierDefinition,
TierValidationTester,
TierValidationTester2,
TierValidationTesterComputed,
)

cls.loader.update_registry(
(TierValidationTester, TierValidationTester2, TierDefinition)
(
TierValidationTester,
TierValidationTester2,
TierValidationTesterComputed,
TierDefinition,
)
)

cls.test_model = cls.env[TierValidationTester._name]
cls.test_model_2 = cls.env[TierValidationTester2._name]
cls.test_model_computed = cls.env[TierValidationTesterComputed._name]

cls.tester_model = cls.env["ir.model"].search(
[("model", "=", "tier.validation.tester")]
)
cls.tester_model_2 = cls.env["ir.model"].search(
[("model", "=", "tier.validation.tester2")]
)

# Access record:
cls.env["ir.model.access"].create(
{
"name": "access.tester",
"model_id": cls.tester_model.id,
"perm_read": 1,
"perm_write": 1,
"perm_create": 1,
"perm_unlink": 1,
}
cls.tester_model_computed = cls.env["ir.model"].search(
[("model", "=", "tier.validation.tester.computed")]
)
cls.env["ir.model.access"].create(
{
"name": "access.tester2",
"model_id": cls.tester_model_2.id,
"perm_read": 1,
"perm_write": 1,
"perm_create": 1,
"perm_unlink": 1,
}

models = (
cls.tester_model,
cls.tester_model_2,
cls.tester_model_computed,
)
for model in models:
# Access record:
cls.env["ir.model.access"].create(
{
"name": f"access {model.name}",
"model_id": model.id,
"perm_read": 1,
"perm_write": 1,
"perm_create": 1,
"perm_unlink": 1,
}
)

# Define views to avoid automatic views with all fields.
for model in cls.test_model._name, cls.test_model_2._name:
# Define views to avoid automatic views with all fields.
cls.env["ir.ui.view"].create(
{
"model": model,
"model": model.model,
"name": f"Demo view for {model}",
"arch": """<form>
<header>
Expand Down Expand Up @@ -103,6 +108,54 @@ def setUpClass(cls):

cls.test_record = cls.test_model.create({"test_field": 2.5})
cls.test_record_2 = cls.test_model_2.create({"test_field": 2.5})
cls.test_record_computed = cls.test_model_computed.create({"test_field": 2.5})

cls.tier_def_obj.create(
{
"model_id": cls.tester_model.id,
"review_type": "individual",
"reviewer_id": cls.test_user_1.id,
"definition_domain": "[('test_field', '>', 3.0)]",
"approve_sequence": True,
"sequence": 20,
"name": "Definition for test 19 - sequence - user 1",
}
)
cls.tier_def_obj.create(
{
"model_id": cls.tester_model.id,
"review_type": "individual",
"reviewer_id": cls.test_user_2.id,
"definition_domain": "[('test_field', '>', 3.0)]",
"approve_sequence": True,
"sequence": 10,
"name": "Definition for test 19 - sequence - user 2",
}
)
# Create definition for test 20
cls.tier_def_obj.create(
{
"model_id": cls.tester_model.id,
"review_type": "individual",
"reviewer_id": cls.test_user_1.id,
"definition_domain": "[('test_field', '=', 0.9)]",
"approve_sequence": False,
"sequence": 10,
"name": "Definition for test 20 - no sequence - user 1 - no sequence",
}
)

cls.tier_def_obj.create(
{
"model_id": cls.tester_model_computed.id,
"review_type": "individual",
"reviewer_id": cls.test_user_1.id,
"definition_domain": "[]",
"approve_sequence": True,
"sequence": 20,
"name": "Definition for computed model",
}
)

@classmethod
def tearDownClass(cls):
Expand Down
29 changes: 29 additions & 0 deletions base_tier_validation/tests/test_tier_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,35 @@ def test_25_change_field_exception_validation(self):
)
self.assertEqual(self.test_record.test_validation_field, 4)

def test_26_computed_state_field(self):
"""Test the regular flow on a model where state is a computed field"""
# The record cannot be confirmed without validation
with self.assertRaisesRegex(
ValidationError,
"This action needs to be validated",
):
with self.env.cr.savepoint():
self.test_record_computed.action_confirm()
# Flush manually to trigger the _write
self.test_record_computed.flush_recordset()
self.assertEqual(self.test_record_computed.state, "draft")
# The validation is performed
self.test_record_computed.request_validation()
self.test_record_computed.invalidate_recordset()
self.assertEqual(self.test_record_computed.review_ids.status, "pending")
self.test_record_computed.with_user(self.test_user_1).validate_tier()
self.test_record_computed.invalidate_recordset()
self.assertEqual(self.test_record_computed.review_ids.status, "approved")
# After validation, the record can be confirmed
self.test_record_computed.action_confirm()
self.test_record_computed.flush_recordset()
self.assertEqual(self.test_record_computed.state, "confirmed")
# After cancelling, the reviews are removed
self.test_record_computed.action_cancel()
self.test_record_computed.flush_recordset()
self.assertFalse(self.test_record_computed.review_ids)
self.test_record_computed.invalidate_recordset()


@tagged("at_install")
class TierTierValidationView(CommonTierValidation):
Expand Down
54 changes: 54 additions & 0 deletions base_tier_validation/tests/tier_validation_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,59 @@
self.write({"state": "confirmed"})


class TierValidationTesterComputed(models.Model):
_name = "tier.validation.tester.computed"
_description = "Tier Validation Tester Computed"
_inherit = ["tier.validation"]
_tier_validation_manual_config = False
_tier_validation_state_field_is_computed = True

confirmed = fields.Boolean()
cancelled = fields.Boolean()
state = fields.Selection(
selection=[
("draft", "Draft"),
("confirmed", "Confirmed"),
("cancel", "Cancel"),
],
compute="_compute_state",
store=True,
)
test_field = fields.Float()
test_validation_field = fields.Float()
user_id = fields.Many2one(string="Assigned to:", comodel_name="res.users")

@api.model
def _get_after_validation_exceptions(self):
return super()._get_after_validation_exceptions() + [

Check warning on line 75 in base_tier_validation/tests/tier_validation_tester.py

View check run for this annotation

Codecov / codecov/patch

base_tier_validation/tests/tier_validation_tester.py#L75

Added line #L75 was not covered by tests
"confirmed",
"cancelled",
]

@api.model
def _get_under_validation_exceptions(self):
return super()._get_under_validation_exceptions() + [
"confirmed",
"cancelled",
]

@api.depends("confirmed", "cancelled")
def _compute_state(self):
for rec in self:
if rec.cancelled:
rec.state = "cancel"
elif rec.confirmed:
rec.state = "confirmed"
else:
rec.state = "draft"

def action_confirm(self):
self.write({"confirmed": True})

def action_cancel(self):
self.write({"cancelled": True})


class TierDefinition(models.Model):
_inherit = "tier.definition"

Expand All @@ -56,4 +109,5 @@
res = super()._get_tier_validation_model_names()
res.append("tier.validation.tester")
res.append("tier.validation.tester2")
res.append("tier.validation.tester.computed")
return res
Loading
Loading