Browse Source

[11.0][IMP] base_tier_validation

fixup and extend tests

[ADD] systray icon for pending reviews

[FIX] Remove python safe_eval

[ADD] base_tier_validation_formula and migration scripts

[ADD] widget domain and python expression to define reviewer in tier definition

[ADD] auto updating of systray icon counter

[ADD] validation date field

[ADD] review widget dropdown menu
pull/69/head
Adrià Gil Sorribes 6 years ago
committed by Lois Rilo
parent
commit
731ed4d5bd
  1. 10
      base_tier_validation/__manifest__.py
  2. 16
      base_tier_validation/migrations/11.0.1.2.0/post-migrate.py
  3. 1
      base_tier_validation/models/__init__.py
  4. 38
      base_tier_validation/models/res_users.py
  5. 31
      base_tier_validation/models/tier_definition.py
  6. 2
      base_tier_validation/models/tier_review.py
  7. 34
      base_tier_validation/models/tier_validation.py
  8. 1
      base_tier_validation/readme/CONTRIBUTORS.rst
  9. 58
      base_tier_validation/static/src/js/review_widget.js
  10. 135
      base_tier_validation/static/src/js/systray.js
  11. 4
      base_tier_validation/static/src/less/review.less
  12. 113
      base_tier_validation/static/src/less/systray.less
  13. 41
      base_tier_validation/static/src/xml/systray.xml
  14. 63
      base_tier_validation/static/src/xml/tier_review_template.xml
  15. 6
      base_tier_validation/tests/common.py
  16. 84
      base_tier_validation/tests/test_tier_validation.py
  17. 21
      base_tier_validation/tests/tier_validation_tester.py
  18. 13
      base_tier_validation/views/assets_backend.xml
  19. 31
      base_tier_validation/views/tier_definition_view.xml
  20. 4
      base_tier_validation/views/tier_review_view.xml
  21. 81
      base_tier_validation_formula/README.rst
  22. 1
      base_tier_validation_formula/__init__.py
  23. 20
      base_tier_validation_formula/__manifest__.py
  24. 3
      base_tier_validation_formula/models/__init__.py
  25. 37
      base_tier_validation_formula/models/tier_definition.py
  26. 47
      base_tier_validation_formula/models/tier_review.py
  27. 25
      base_tier_validation_formula/models/tier_validation.py
  28. 2
      base_tier_validation_formula/readme/CONTRIBUTORS.rst
  29. 2
      base_tier_validation_formula/readme/DESCRIPTION.rst
  30. 2
      base_tier_validation_formula/readme/USAGE.rst
  31. BIN
      base_tier_validation_formula/static/description/icon.png
  32. 405
      base_tier_validation_formula/static/description/index.html
  33. 4
      base_tier_validation_formula/tests/__init__.py
  34. 19
      base_tier_validation_formula/tests/common.py
  35. 112
      base_tier_validation_formula/tests/test_tier_validation.py
  36. 23
      base_tier_validation_formula/tests/tier_validation_tester.py
  37. 25
      base_tier_validation_formula/views/tier_definition_view.xml

10
base_tier_validation/__manifest__.py

@ -3,7 +3,7 @@
{ {
"name": "Base Tier Validation", "name": "Base Tier Validation",
"summary": "Implement a validation process based on tiers.", "summary": "Implement a validation process based on tiers.",
"version": "12.0.1.0.0",
"version": "12.0.2.0.0",
"development_status": "Mature", "development_status": "Mature",
"maintainers": ['lreficent'], "maintainers": ['lreficent'],
"category": "Tools", "category": "Tools",
@ -13,11 +13,17 @@
"application": False, "application": False,
"installable": True, "installable": True,
"depends": [ "depends": [
"base",
"web",
"bus",
], ],
"data": [ "data": [
"security/ir.model.access.csv", "security/ir.model.access.csv",
"views/tier_definition_view.xml", "views/tier_definition_view.xml",
"views/tier_review_view.xml", "views/tier_review_view.xml",
"views/assets_backend.xml",
],
'qweb': [
'static/src/xml/systray.xml',
'static/src/xml/tier_review_template.xml',
], ],
} }

16
base_tier_validation/migrations/11.0.1.2.0/post-migrate.py

@ -0,0 +1,16 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openupgradelib.openupgrade import migrate
@migrate()
def migrate(env, version):
module_ids = env['ir.module.module'].search([
('name', '=', 'base_tier_validation_formula'),
('state', '=', 'uninstalled')
])
if module_ids:
module_ids.sudo().button_install()
cr = env.cr
cr.execute("UPDATE tier_definition SET definition_type = 'formula'")

1
base_tier_validation/models/__init__.py

@ -3,3 +3,4 @@
from . import tier_definition from . import tier_definition
from . import tier_review from . import tier_review
from . import tier_validation from . import tier_validation
from . import res_users

38
base_tier_validation/models/res_users.py

@ -0,0 +1,38 @@
# Copyright 2019 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, modules
class Users(models.Model):
_inherit = 'res.users'
review_ids = fields.Many2many(
string="Reviews", comodel_name="tier.review"
)
@api.model
def review_user_count(self):
user_reviews = {}
to_review_docs = {}
for review in self.env.user.review_ids.filtered(
lambda r: r.status == 'pending'):
record = review.env[review.model].browse(review.res_id)
if not user_reviews.get(review['model']):
user_reviews[review.model] = {
'name': record._description,
'model': review.model,
'icon': modules.module.get_module_icon(
self.env[review.model]._original_module),
'pending_count': 0
}
docs = to_review_docs.get(review.model)
if (docs and record not in docs) or not docs:
user_reviews[review.model]['pending_count'] += 1
to_review_docs.setdefault(review.model, []).append(record)
return list(user_reviews.values())
@api.model
def get_reviews(self, data):
return self.env['tier.review'].search_read(
[('id', 'in', data.get('res_ids'))])

31
base_tier_validation/models/tier_definition.py

@ -7,13 +7,18 @@ from odoo import api, fields, models
class TierDefinition(models.Model): class TierDefinition(models.Model):
_name = "tier.definition" _name = "tier.definition"
_description = "Tier Definition" _description = "Tier Definition"
_rec_name = "model_id"
@api.model
def _get_default_name(self):
return "New Tier Validation"
@api.model @api.model
def _get_tier_validation_model_names(self): def _get_tier_validation_model_names(self):
res = [] res = []
return res return res
name = fields.Char(
'Description', required=True, default=_get_default_name)
model_id = fields.Many2one( model_id = fields.Many2one(
comodel_name="ir.model", comodel_name="ir.model",
string="Referenced Model", string="Referenced Model",
@ -23,8 +28,10 @@ class TierDefinition(models.Model):
) )
review_type = fields.Selection( review_type = fields.Selection(
string="Validated by", default="individual", string="Validated by", default="individual",
selection=[("individual", "Specific user"),
("group", "Any user in a specific group.")]
selection=[
("individual", "Specific user"),
("group", "Any user in a specific group."),
]
) )
reviewer_id = fields.Many2one( reviewer_id = fields.Many2one(
comodel_name="res.users", string="Reviewer", comodel_name="res.users", string="Reviewer",
@ -32,13 +39,14 @@ class TierDefinition(models.Model):
reviewer_group_id = fields.Many2one( reviewer_group_id = fields.Many2one(
comodel_name="res.groups", string="Reviewer group", comodel_name="res.groups", string="Reviewer group",
) )
python_code = fields.Text(
string='Tier Definition Expression',
help="Write Python code that defines when this tier confirmation "
"will be needed. The result of executing the expresion must be "
"a boolean.",
default="""# Available locals:\n# - rec: current record""",
definition_type = fields.Selection(
string="Definition",
selection=[
('domain', 'Domain'),
],
default='domain',
) )
definition_domain = fields.Char()
active = fields.Boolean(default=True) active = fields.Boolean(default=True)
sequence = fields.Integer(default=30) sequence = fields.Integer(default=30)
company_id = fields.Many2one( company_id = fields.Many2one(
@ -52,3 +60,8 @@ class TierDefinition(models.Model):
return {'domain': { return {'domain': {
'model_id': [ 'model_id': [
('model', 'in', self._get_tier_validation_model_names())]}} ('model', 'in', self._get_tier_validation_model_names())]}}
@api.onchange('review_type')
def onchange_review_type(self):
self.reviewer_id = None
self.reviewer_group_id = None

2
base_tier_validation/models/tier_review.py

@ -8,6 +8,7 @@ class TierReview(models.Model):
_name = "tier.review" _name = "tier.review"
_description = "Tier Review" _description = "Tier Review"
name = fields.Char(related="definition_id.name", readonly=True)
status = fields.Selection( status = fields.Selection(
selection=[("pending", "Pending"), selection=[("pending", "Pending"),
("rejected", "Rejected"), ("rejected", "Rejected"),
@ -39,6 +40,7 @@ class TierReview(models.Model):
requested_by = fields.Many2one( requested_by = fields.Many2one(
comodel_name="res.users", comodel_name="res.users",
) )
reviewed_date = fields.Datetime(string='Validation Date')
@api.multi @api.multi
@api.depends('reviewer_id', 'reviewer_group_id', 'reviewer_group_id.users') @api.depends('reviewer_id', 'reviewer_group_id', 'reviewer_group_id.users')

34
base_tier_validation/models/tier_validation.py

@ -2,8 +2,8 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _ from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError
from odoo.tools.safe_eval import safe_eval
from odoo.exceptions import ValidationError
from ast import literal_eval
class TierValidation(models.AbstractModel): class TierValidation(models.AbstractModel):
@ -23,6 +23,10 @@ class TierValidation(models.AbstractModel):
domain=lambda self: [('model', '=', self._name)], domain=lambda self: [('model', '=', self._name)],
auto_join=True, auto_join=True,
) )
review_ids_dropdown = fields.One2many(
related='review_ids',
help="Field needed to display the dropdown menu correctly"
)
validated = fields.Boolean( validated = fields.Boolean(
compute="_compute_validated_rejected", compute="_compute_validated_rejected",
search="_search_validated", search="_search_validated",
@ -94,12 +98,10 @@ class TierValidation(models.AbstractModel):
@api.multi @api.multi
def evaluate_tier(self, tier): def evaluate_tier(self, tier):
try:
res = safe_eval(tier.python_code, globals_dict={'rec': self})
except Exception as error:
raise UserError(_(
"Error evaluating tier validation conditions.\n %s") % error)
return res
domain = []
if tier.definition_domain:
domain = literal_eval(tier.definition_domain)
return self.search([('id', '=', self.id)] + domain)
@api.model @api.model
def _get_under_validation_exceptions(self): def _get_under_validation_exceptions(self):
@ -147,12 +149,13 @@ class TierValidation(models.AbstractModel):
tier_reviews = tiers or self.review_ids tier_reviews = tiers or self.review_ids
user_reviews = tier_reviews.filtered( user_reviews = tier_reviews.filtered(
lambda r: r.status in ('pending', 'rejected') and lambda r: r.status in ('pending', 'rejected') and
(r.reviewer_id == self.env.user or
r.reviewer_group_id in self.env.user.groups_id))
(self.env.user in r.reviewer_ids))
user_reviews.write({ user_reviews.write({
'status': 'approved', 'status': 'approved',
'done_by': self.env.user.id, 'done_by': self.env.user.id,
'reviewed_date': fields.Datetime.now(),
}) })
# TODO: add message_post
@api.multi @api.multi
def validate_tier(self): def validate_tier(self):
@ -169,7 +172,9 @@ class TierValidation(models.AbstractModel):
user_reviews.write({ user_reviews.write({
'status': 'rejected', 'status': 'rejected',
'done_by': self.env.user.id, 'done_by': self.env.user.id,
'reviewed_date': fields.Datetime.now(),
}) })
# TODO: Add Message_post
@api.multi @api.multi
def request_validation(self): def request_validation(self):
@ -191,7 +196,7 @@ class TierValidation(models.AbstractModel):
'sequence': sequence, 'sequence': sequence,
'requested_by': self.env.uid, 'requested_by': self.env.uid,
}) })
# TODO: notify? post some msg in chatter?
self._update_counter()
return created_trs return created_trs
@api.multi @api.multi
@ -199,3 +204,10 @@ class TierValidation(models.AbstractModel):
for rec in self: for rec in self:
if getattr(rec, self._state_field) in self._state_from: if getattr(rec, self._state_field) in self._state_from:
rec.mapped('review_ids').unlink() rec.mapped('review_ids').unlink()
self._update_counter()
def _update_counter(self):
notifications = []
channel = 'base.tier.validation'
notifications.append([channel, {}])
self.env['bus.bus'].sendmany(notifications)

1
base_tier_validation/readme/CONTRIBUTORS.rst

@ -1,2 +1,3 @@
* Lois Rilo <lois.rilo@eficent.com> * Lois Rilo <lois.rilo@eficent.com>
* Naglis Jonaitis <naglis@versada.eu> * Naglis Jonaitis <naglis@versada.eu>
* Adrià Gil Sorribes <adria.gil@eficent.com>

58
base_tier_validation/static/src/js/review_widget.js

@ -0,0 +1,58 @@
odoo.define('base_tier_validation.ReviewField', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
var core = require('web.core');
var session = require('web.session');
var field_registry = require('web.field_registry');
var Widget = require('web.Widget');
var _t = core._t;
var QWeb = core.qweb;
var ReviewField = AbstractField.extend({
template: 'tier.review.ReviewPopUp',
events: {
'click .o_info_btn': '_onButtonClicked',
},
start: function () {
var self = this;
console.log(self)
},
/**
* Make RPC and get current user's activity details
* @private
*/
_getReviewData: function(res_ids){
var self = this;
return self._rpc({
model: 'res.users',
method: 'get_reviews',
args: [res_ids],
}).then(function (data) {
self.reviews = data;
});
},
_renderDropdown: function () {
var self = this;
return this._getReviewData(self.value).then(function (){
self.$('.o_review').html(QWeb.render("tier.review.ReviewDropDown", {
reviews : self.reviews
}));
});
},
_onButtonClicked: function (event) {
event.preventDefault();
if (!this.$el.hasClass('open')) {
this._renderDropdown();
}
},
});
field_registry.add('review_popup', ReviewField);
return ReviewField;
});

135
base_tier_validation/static/src/js/systray.js

@ -0,0 +1,135 @@
odoo.define('tier_validation.systray', function (require) {
"use strict";
var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var Widget = require('web.Widget');
var bus = require('bus.bus').bus;
var chat_manager = require('mail.chat_manager');
var QWeb = core.qweb;
var ReviewMenu = Widget.extend({
template:'tier.validation.ReviewMenu',
events: {
"click": "_onReviewMenuClick",
"click .o_mail_channel_preview": "_onReviewFilterClick",
},
start: function () {
this.$reviews_preview = this.$('.o_mail_navbar_dropdown_channels');
this._updateReviewPreview();
var channel = 'base.tier.validation';
bus.add_channel(channel);
bus.on('notification', this, this._updateReviewPreview);
return this._super();
},
// Private
/**
* Make RPC and get current user's activity details
* @private
*/
_getReviewData: function(){
var self = this;
return self._rpc({
model: 'res.users',
method: 'review_user_count',
kwargs: {
context: session.user_context,
},
}).then(function (data) {
self.reviews = data;
self.reviewCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.pending_count; }, 0);
self.$('.o_notification_counter').text(self.reviewCounter);
self.$el.toggleClass('o_no_notification', !self.reviewCounter);
});
},
/**
* Check wether activity systray dropdown is open or not
* @private
* @returns {boolean}
*/
_isOpen: function () {
return this.$el.hasClass('open');
},
/**
* Update(render) activity system tray view on activity updation.
* @private
*/
_updateReviewPreview: function () {
var self = this;
self._getReviewData().then(function (){
self.$reviews_preview.html(QWeb.render('tier.validation.ReviewMenuPreview', {
reviews : self.reviews
}));
});
},
/**
* update counter based on activity status(created or Done)
* @private
* @param {Object} [data] key, value to decide activity created or deleted
* @param {String} [data.type] notification type
* @param {Boolean} [data.activity_deleted] when activity deleted
* @param {Boolean} [data.activity_created] when activity created
*/
_updateCounter: function (data) {
if (data) {
if (data.review_created) {
this.reviewCounter ++;
}
if (data.review_deleted && this.reviewCounter > 0) {
this.reviewCounter --;
}
this.$('.o_notification_counter').text(this.reviewCounter);
this.$el.toggleClass('o_no_notification', !this.reviewCounter);
}
},
// Handlers
/**
* Redirect to particular model view
* @private
* @param {MouseEvent} event
*/
_onReviewFilterClick: function (event) {
// fetch the data from the button otherwise fetch the ones from the parent (.o_tier_channel_preview).
var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data());
var context = {};
this.do_action({
type: 'ir.actions.act_window',
name: data.model_name,
res_model: data.res_model,
views: [[false, 'list'], [false, 'form']],
search_view_id: [false],
domain: [['review_ids.reviewer_ids', '=', session.uid],
['review_ids.status', '=', 'pending']],
context:context,
});
},
/**
* When menu clicked update activity preview if counter updated
* @private
* @param {MouseEvent} event
*/
_onReviewMenuClick: function () {
if (!this._isOpen()) {
this._updateReviewPreview();
}
},
});
SystrayMenu.Items.push(ReviewMenu);
// to test activity menu in qunit test cases we need it
return {
ReviewMenu: ReviewMenu,
};
});

4
base_tier_validation/static/src/less/review.less

@ -0,0 +1,4 @@
ul.o_review {
min-width: 600px;
max-width: 800px
}

113
base_tier_validation/static/src/less/systray.less

@ -0,0 +1,113 @@
// Navbar icon and dropdown
.o_tier_navbar_item {
> a {
opacity: 1;
> i {
font-size: larger;
}
}
&.o_no_notification > a {
opacity: 0.5;
> i {
.o-transform(translateY(0px));
}
.o_notification_counter {
display: none;
}
}
&.open .o_tier_navbar_dropdown {
.o-flex-display();
.o-flex-flow(column, nowrap);
}
.o_notification_counter {
.o-position-absolute(@top: 20%, @right: 1px);
background: @odoo-brand-optional;
color: white;
padding: 0em 0.3em;
font-size: 0.7em;
}
.o_tier_navbar_dropdown {
width: 350px;
padding: 0;
.o_spinner {
.o-flex-display();
.o-align-items(center);
.o-justify-content(center);
color: @odoo-main-text-color;
height: 50px;
}
.o_tier_navbar_dropdown_channels {
.o-flex(0, 1, auto);
max-height: 400px;
min-height: 50px;
overflow-y: auto;
@media (min-width: @screen-sm-min) {
.o_tier_channel_preview {
height: 50px;
padding: 5px;
.o_tier_channel_image {
width: 40px;
}
.o_channel_info {
margin-left: 10px;
.o_channel_title {
.o_last_message_date {
padding-top: 2px;
font-size: x-small;
margin-left: 10px;
}
}
}
}
}
}
.o_no_review {
cursor: initial;
.o-align-items(center);
color: grey;
opacity: 0.5;
padding: 3px;
}
}
}
.o_no_chat_window .o_tier_navbar_dropdown .o_new_message {
display: none; // hide 'new message' button if chat windows are disabled
}
// Mobile rules
// Goal: mock the design of Discuss in mobile
@media (max-width: @screen-xs-max) {
.o_tier_navbar_item {
.o_notification_counter {
top: 10%;
}
.o_tier_navbar_dropdown {
position: relative;
.o_tier_navbar_dropdown_top {
padding: 5px;
}
.o_tier_navbar_mobile_header {
padding: 5px;
height: 44px;
border-bottom: 1px solid #ebebeb;
box-shadow: 0 0 2px @gray-lighter-darker;
}
.o_tier_navbar_dropdown_channels {
max-height: none;
padding-bottom: 52px; // leave space for tabs
}
.o_tier_mobile_tabs {
position: fixed;
bottom: 0px;
left: 0px;
right: 0px;
background-color: white;
color: @odoo-main-text-color;
}
}
}
}

41
base_tier_validation/static/src/xml/systray.xml

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="tier.validation.ReviewMenuPreview">
<t t-if="_.isEmpty(reviews)">
<li class="text-center o_no_review">
<span>No reviews to do.</span>
</li>
</t>
<t t-foreach="reviews" t-as="review">
<div class="o_mail_channel_preview" t-att-data-res_model="review.model" t-att-data-model_name="review.name">
<div class="o_mail_channel_image o_mail_channel_app">
<img t-att-src="review.icon"/>
</div>
<div class="o_channel_info">
<div class="o_channel_title">
<span class="o_channel_name">
<t t-esc="review.name"/>
</span>
</div>
<div>
<button t-if="review.pending_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="review.model" t-att-data-model_name="review.name" data-filter='pending_count'><t t-esc="review.pending_count"/> Pending </button>
<span t-if="!review.pending_count" class="o_no_review mr16">0 Pending </span>
</div>
</div>
</div>
</t>
</t>
<t t-name="tier.validation.ReviewMenu">
<li class="o_mail_navbar_item">
<a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" title="Reviews" href="#">
<i class="fa fa-pencil-square-o"/> <span class="o_notification_counter badge"/>
</a>
<ul class="o_mail_navbar_dropdown dropdown-menu" role="menu">
<li class="o_mail_navbar_dropdown_channels"/>
</ul>
</li>
</t>
</templates>

63
base_tier_validation/static/src/xml/tier_review_template.xml

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="tier.review.ReviewPopUp">
<div class="dropdown btn btn-sm oe_stat_button">
<div class="dropdown-toggle o_info_btn" data-toggle="dropdown" style="height:100%;width:100%;display:table">
<div style="display:table-cell;vertical-align:middle">
<div class="fa fa-fw o_button_icon fa-pencil-square-o"/>
<div class="o_field_widget o_stat_info o_readonly_modifier">
<span class="o_stat_text">Reviews</span>
</div>
</div>
</div>
<ul class="dropdown-menu o_review" role="menu" style="right:0;left: auto">
</ul>
</div>
</t>
<t t-name="tier.review.ReviewDropDown">
<table class="oe_mt32 table table-condensed">
<thead>
<tr>
<th class="text-center">Sequence</th>
<th class="text-left">Requested by</th>
<th class="text-right">Description</th>
<th class="text-right">Status</th>
<th class="text-right">Done by</th>
<th class="text-right">Validation Date</th>
</tr>
</thead>
<tbody class="sale_tbody">
<t t-foreach="reviews" t-as="review">
<t t-if="review.status == 'pending'" t-set="status_class" t-value=""/>
<t t-if="review.status == 'approved'" t-set="status_class" t-value="'alert-success'"/>
<t t-if="review.status == 'rejected'" t-set="status_class" t-value="'alert-danger'"/>
<tr t-att-class="status_class">
<td class="text-center">
<span t-esc="review.sequence"/>
</td>
<td class="text-left">
<span t-esc="review.requested_by[1]"/>
</td>
<td class="text-right">
<span t-esc="review.name"/>
</td>
<td class="text-right">
<span t-esc="review.status"/>
</td>
<td class="text-right">
<span t-esc="review.done_by[1]"/>
</td>
<td class="text-right">
<t t-if="review.reviewed_date">
<span t-esc="review.reviewed_date"/>
</t>
</td>
</tr>
</t>
</tbody>
</table>
</t>
</templates>

6
base_tier_validation/tests/common.py

@ -11,3 +11,9 @@ def setup_test_model(env, model_clses):
env.cr, [model_cls._name for model_cls in model_clses], env.cr, [model_cls._name for model_cls in model_clses],
dict(env.context, update_custom_fields=True) dict(env.context, update_custom_fields=True)
) )
def teardown_test_model(env, model_clses):
for model_cls in model_clses:
del env.registry.models[model_cls._name]
env.registry.setup_models(env.cr)

84
base_tier_validation/tests/test_tier_validation.py

@ -2,9 +2,9 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo.tests import common from odoo.tests import common
from odoo.exceptions import ValidationError, UserError
from .common import setup_test_model
from .tier_validation_tester import TierValidationTester
from odoo.exceptions import ValidationError
from .common import setup_test_model, teardown_test_model
from .tier_validation_tester import TierValidationTester, TierValidationTester2
@common.at_install(False) @common.at_install(False)
@ -15,12 +15,16 @@ class TierTierValidation(common.SavepointCase):
def setUpClass(cls): def setUpClass(cls):
super(TierTierValidation, cls).setUpClass() super(TierTierValidation, cls).setUpClass()
setup_test_model(cls.env, [TierValidationTester])
setup_test_model(cls.env,
[TierValidationTester, TierValidationTester2])
cls.test_model = cls.env[TierValidationTester._name] cls.test_model = cls.env[TierValidationTester._name]
cls.test_model_2 = cls.env[TierValidationTester2._name]
cls.tester_model = cls.env['ir.model'].search([ cls.tester_model = cls.env['ir.model'].search([
('model', '=', 'tier.validation.tester')]) ('model', '=', 'tier.validation.tester')])
cls.tester_model_2 = cls.env['ir.model'].search([
('model', '=', 'tier.validation.tester2')])
# Access record: # Access record:
cls.env["ir.model.access"].create({ cls.env["ir.model.access"].create({
@ -31,6 +35,14 @@ class TierTierValidation(common.SavepointCase):
'perm_create': 1, 'perm_create': 1,
'perm_unlink': 1, 'perm_unlink': 1,
}) })
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,
})
# Create users: # Create users:
group_ids = cls.env.ref('base.group_system').ids group_ids = cls.env.ref('base.group_system').ids
@ -44,18 +56,27 @@ class TierTierValidation(common.SavepointCase):
'login': 'test2', 'login': 'test2',
}) })
# Create tier definition:
# Create tier definitions:
cls.tier_def_obj = cls.env['tier.definition'] cls.tier_def_obj = cls.env['tier.definition']
cls.tier_def_obj.create({ cls.tier_def_obj.create({
'model_id': cls.tester_model.id, 'model_id': cls.tester_model.id,
'review_type': 'individual', 'review_type': 'individual',
'reviewer_id': cls.test_user_1.id, 'reviewer_id': cls.test_user_1.id,
'python_code': 'rec.test_field > 1.0',
'definition_domain': "[('test_field', '>', 1.0)]",
}) })
cls.test_record = cls.test_model.create({ cls.test_record = cls.test_model.create({
'test_field': 2.5, 'test_field': 2.5,
}) })
cls.test_record_2 = cls.test_model_2.create({
'test_field': 2.5,
})
@classmethod
def tearDownClass(cls):
teardown_test_model(cls.env,
[TierValidationTester, TierValidationTester2])
super(TierTierValidation, cls).tearDownClass()
def test_01_auto_validation(self): def test_01_auto_validation(self):
"""When the user can validate all future reviews, it is not needed """When the user can validate all future reviews, it is not needed
@ -130,21 +151,46 @@ class TierTierValidation(common.SavepointCase):
[('validated', '=', False)]) [('validated', '=', False)])
self.assertTrue(res) self.assertTrue(res)
def test_09_wrong_tier_definition(self):
"""Error should raise with incorrect python expresions on
tier definitions."""
self.tier_def_obj.create({
'model_id': self.tester_model.id,
'review_type': 'individual',
'reviewer_id': self.test_user_1.id,
'python_code': 'rec.not_existing_field > 1.0',
})
with self.assertRaises(UserError):
self.test_record.sudo(self.test_user_1.id).action_confirm()
def test_10_dummy_tier_definition(self):
def test_09_dummy_tier_definition(self):
"""Test tier.definition methods.""" """Test tier.definition methods."""
res = self.tier_def_obj._get_tier_validation_model_names() res = self.tier_def_obj._get_tier_validation_model_names()
self.assertEqual(res, []) self.assertEqual(res, [])
res = self.tier_def_obj.onchange_model_id() res = self.tier_def_obj.onchange_model_id()
self.assertTrue(res) self.assertTrue(res)
def test_10_systray_counter(self):
# Create new test record
test_record = self.test_model.create({
'test_field': 2.5,
})
# Create tier definitions for both tester models
self.tier_def_obj.create({
'model_id': self.tester_model.id,
'review_type': 'individual',
'reviewer_id': self.test_user_1.id,
'definition_domain': "[('test_field', '>', 1.0)]",
})
self.tier_def_obj.create({
'model_id': self.tester_model.id,
'review_type': 'individual',
'reviewer_id': self.test_user_1.id,
'definition_domain': "[('test_field', '>', 1.0)]",
})
self.tier_def_obj.create({
'model_id': self.tester_model_2.id,
'review_type': 'individual',
'reviewer_id': self.test_user_1.id,
'definition_domain': "[('test_field', '>', 1.0)]",
})
# Request validation
self.test_record.sudo(self.test_user_2.id).request_validation()
test_record.sudo(self.test_user_2.id).request_validation()
self.test_record_2.sudo(self.test_user_2.id).request_validation()
# Get review user count as systray icon would do and check count value
docs = self.test_user_1.sudo(
self.test_user_1).review_user_count()
for doc in docs:
if doc.get('name') == 'tier.validation.tester2':
self.assertEqual(doc.get('pending_count'), 1)
else:
self.assertEqual(doc.get('pending_count'), 2)

21
base_tier_validation/tests/tier_validation_tester.py

@ -15,6 +15,27 @@ class TierValidationTester(models.Model):
default='draft', default='draft',
) )
test_field = fields.Float() test_field = fields.Float()
user_id = fields.Many2one(string="Assigned to:",
comodel_name="res.users")
@api.multi
def action_confirm(self):
self.write({'state': 'confirmed'})
class TierValidationTester2(models.Model):
_name = 'tier.validation.tester2'
_inherit = ['tier.validation']
state = fields.Selection(
selection=[('draft', 'Draft'),
('confirmed', 'Confirmed'),
('cancel', 'Cancel')],
default='draft',
)
test_field = fields.Float()
user_id = fields.Many2one(string="Assigned to:",
comodel_name="res.users")
@api.multi @api.multi
def action_confirm(self): def action_confirm(self):

13
base_tier_validation/views/assets_backend.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend" name="mail assets"
inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/base_tier_validation/static/src/js/systray.js"/>
<script type="text/javascript" src="/base_tier_validation/static/src/js/review_widget.js"/>
<link rel="stylesheet" href="/base_tier_validation/static/src/less/systray.less" type="text/less"/>
<link rel="stylesheet" href="/base_tier_validation/static/src/less/review.less" type="text/less"/>
</xpath>
</template>
</odoo>

31
base_tier_validation/views/tier_definition_view.xml

@ -9,6 +9,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Tier Definition"> <tree string="Tier Definition">
<field name="model_id"/> <field name="model_id"/>
<field name="name"/>
<field name="review_type"/> <field name="review_type"/>
<field name="reviewer_id"/> <field name="reviewer_id"/>
<field name="reviewer_group_id"/> <field name="reviewer_group_id"/>
@ -26,13 +27,23 @@
<form string="Tier Definition"> <form string="Tier Definition">
<sheet> <sheet>
<div class="oe_button_box" name="button_box"> <div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<button name="toggle_active" type="object"
class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"/> <field name="active" widget="boolean_button"/>
</button> </button>
</div> </div>
<div class="oe_title">
<span class="oe_edit_only">Name</span>
<h1>
<field name="name" required="1"
placeholder="e.g. Tier Validation for..."/>
</h1>
</div>
<group> <group>
<group name="left"> <group name="left">
<field name="model_id" options="{'no_create': True}"/>
<field name="model_id"
options="{'no_create': True}"/>
<field name="model" invisible="1"/>
<field name="review_type"/> <field name="review_type"/>
<field name="reviewer_id" <field name="reviewer_id"
attrs="{'invisible': [('review_type', '!=', 'individual')]}"/> attrs="{'invisible': [('review_type', '!=', 'individual')]}"/>
@ -40,12 +51,16 @@
attrs="{'invisible': [('review_type', '!=', 'group')]}"/> attrs="{'invisible': [('review_type', '!=', 'group')]}"/>
</group> </group>
<group name="right"> <group name="right">
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="company_id"
groups="base.group_multi_company"
options="{'no_create': True}"/>
<field name="sequence"/> <field name="sequence"/>
</group> </group>
</group> </group>
<group col="4" name="bottom">
<field name="python_code" colspan="4"/>
<group name="bottom">
<field name="definition_type"/>
<field name="definition_domain" widget="domain" options="{'model': 'model'}"
attrs="{'invisible': [('definition_type', '!=', 'domain')]}"/>
</group> </group>
</sheet> </sheet>
</form> </form>
@ -62,9 +77,11 @@
<field name="reviewer_group_id"/> <field name="reviewer_group_id"/>
<field name="active"/> <field name="active"/>
<separator/> <separator/>
<filter string="All" name="all" domain="['|', ('active', '=', False), ('active', '=', True)]" />
<filter string="All" name="all"
domain="['|', ('active', '=', False), ('active', '=', True)]"/>
<group expand="0" string="Group By"> <group expand="0" string="Group By">
<filter string="Model" name="model_id" domain="[]" context="{'group_by':'model_id'}"/>
<filter string="Model" name="model_id" domain="[]"
context="{'group_by':'model_id'}"/>
</group> </group>
</search> </search>
</field> </field>

4
base_tier_validation/views/tier_review_view.xml

@ -13,10 +13,10 @@
<field name="sequence"/> <field name="sequence"/>
<field name="requested_by"/> <field name="requested_by"/>
<field name="review_type"/> <field name="review_type"/>
<field name="reviewer_id"/>
<field name="reviewer_group_id"/>
<field name="name"/>
<field name="status"/> <field name="status"/>
<field name="done_by"/> <field name="done_by"/>
<field name="reviewed_date"/>
</tree> </tree>
</field> </field>
</record> </record>

81
base_tier_validation_formula/README.rst

@ -0,0 +1,81 @@
============================
Base Tier Validation Formula
============================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--ux-lightgray.png?logo=github
:target: https://github.com/OCA/server-ux/tree/11.0/base_tier_validation_formula
:alt: OCA/server-ux
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-ux-11-0/server-ux-11-0-base_tier_validation_formula
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/250/11.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module includes the ability to define the tier definition domain
and the tier reviewers using python code.
**Table of contents**
.. contents::
:local:
Usage
=====
To define the domain by python code choose the Formula option in the Definition field.
To define the reviewers by python code choose Python Expression option in the Validated by field.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-ux/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-ux/issues/new?body=module:%20base_tier_validation_formula%0Aversion:%2011.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Creu Blanca
Contributors
~~~~~~~~~~~~
* Enric Tobella <etobella@creublanca.es>
* Adrià Gil Sorribes <adria.gil@eficent.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/server-ux <https://github.com/OCA/server-ux/tree/11.0/base_tier_validation_formula>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

1
base_tier_validation_formula/__init__.py

@ -0,0 +1 @@
from . import models

20
base_tier_validation_formula/__manifest__.py

@ -0,0 +1,20 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Base Tier Validation Formula',
'summary': """
Formulas for Base tier validation""",
'version': '11.0.1.0.0',
'license': 'AGPL-3',
'author': 'Creu Blanca,Odoo Community Association (OCA)',
'website': 'www.creublanca.es',
'depends': [
'base_tier_validation'
],
'data': [
'views/tier_definition_view.xml',
],
'demo': [
],
}

3
base_tier_validation_formula/models/__init__.py

@ -0,0 +1,3 @@
from . import tier_definition
from . import tier_validation
from . import tier_review

37
base_tier_validation_formula/models/tier_definition.py

@ -0,0 +1,37 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, api, models
class TierDefinition(models.Model):
_inherit = 'tier.definition'
python_code = fields.Text(
string='Tier Definition Expression',
help="Write Python code that defines when this tier confirmation "
"will be needed. The result of executing the expresion must be "
"a boolean.",
default="""# Available locals:\n# - rec: current record""",
)
definition_type = fields.Selection(
selection_add=[('formula', 'Formula')]
)
reviewer_expression = fields.Text(
string='Review Expression',
help="Write Python code that defines the reviewer. "
"The result of executing the expression must be a res.users "
"recordset.",
default="# Available locals:\n# - rec: current record\n"
"# - Expects a recordset of res.users",
)
review_type = fields.Selection(
selection_add=[("expression", "Python Expression")]
)
@api.onchange('review_type')
def onchange_review_type(self):
super(TierDefinition, self).onchange_review_type()
self.reviewer_expression = "# Available locals:\n" \
"# - rec: current record\n" \
"# - Expects a recordset of res.users"

47
base_tier_validation_formula/models/tier_review.py

@ -0,0 +1,47 @@
# Copyright 2019 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval
class TierReview(models.Model):
_inherit = "tier.review"
python_reviewer_ids = fields.Many2many(
string="Reviewers from Python expression", comodel_name="res.users",
compute="_compute_python_reviewer_ids", store=True
)
@api.depends('reviewer_id', 'reviewer_group_id', 'reviewer_group_id.users',
'python_reviewer_ids')
def _compute_reviewer_ids(self):
super(TierReview, self)._compute_reviewer_ids()
for rec in self:
rec.reviewer_ids = rec.reviewer_id + rec.reviewer_group_id.users \
+ rec.python_reviewer_ids
@api.multi
@api.depends('definition_id.reviewer_expression',
'review_type', 'model', 'res_id')
def _compute_python_reviewer_ids(self):
for rec in self:
if rec.review_type == 'expression':
record = rec.env[rec.model].browse(rec.res_id).exists()
try:
reviewer_ids = safe_eval(
rec.definition_id.reviewer_expression,
globals_dict={'rec': record})
except Exception as error:
raise UserError(_(
"Error evaluating tier validation "
"conditions.\n %s") % error)
# Check if python expression returns 'res.users' recordset
if not isinstance(reviewer_ids, models.Model) or \
reviewer_ids._name != 'res.users':
raise UserError(_(
"Reviewer python expression must return a "
"res.users recordset."))
else:
rec.python_reviewer_ids = reviewer_ids

25
base_tier_validation_formula/models/tier_validation.py

@ -0,0 +1,25 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models, _
from odoo.tools.safe_eval import safe_eval
from odoo.exceptions import UserError
class TierValidation(models.AbstractModel):
_inherit = 'tier.validation'
@api.multi
def evaluate_formula_tier(self, tier):
try:
res = safe_eval(tier.python_code, globals_dict={'rec': self})
except Exception as error:
raise UserError(_(
"Error evaluating tier validation conditions.\n %s") % error)
return res
@api.multi
def evaluate_tier(self, tier):
if tier.definition_type == 'formula':
return self.evaluate_formula_tier(tier)
return super().evaluate_tier(tier)

2
base_tier_validation_formula/readme/CONTRIBUTORS.rst

@ -0,0 +1,2 @@
* Enric Tobella <etobella@creublanca.es>
* Adrià Gil Sorribes <adria.gil@eficent.com>

2
base_tier_validation_formula/readme/DESCRIPTION.rst

@ -0,0 +1,2 @@
This module includes the ability to define the tier definition domain
and the tier reviewers using python code.

2
base_tier_validation_formula/readme/USAGE.rst

@ -0,0 +1,2 @@
To define the domain by python code choose the Formula option in the Definition field.
To define the reviewers by python code choose Python Expression option in the Validated by field.

BIN
base_tier_validation_formula/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

405
base_tier_validation_formula/static/description/index.html

@ -0,0 +1,405 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.12: http://docutils.sourceforge.net/" />
<title>Base Tier Validation Formula</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7614 2013-02-21 15:55:51Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="base-tier-validation-formula">
<h1 class="title">Base Tier Validation Formula</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/server-ux/tree/11.0/base_tier_validation_formula"><img alt="OCA/server-ux" src="https://img.shields.io/badge/github-OCA%2Fserver--ux-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/server-ux-11-0/server-ux-11-0-base_tier_validation_formula"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/250/11.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module includes the ability to define the tier definition domain
and the tier reviewers using python code.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>To define the domain by python code choose the Formula option in the Definition field.
To define the reviewers by python code choose Python Expression option in the Validated by field.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-ux/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/server-ux/issues/new?body=module:%20base_tier_validation_formula%0Aversion:%2011.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id4">Authors</a></h2>
<ul class="simple">
<li>Creu Blanca</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id5">Contributors</a></h2>
<ul class="simple">
<li>Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
<li>Adrià Gil Sorribes &lt;<a class="reference external" href="mailto:adria.gil&#64;eficent.com">adria.gil&#64;eficent.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-ux/tree/11.0/base_tier_validation_formula">OCA/server-ux</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

4
base_tier_validation_formula/tests/__init__.py

@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import common
from . import test_tier_validation

19
base_tier_validation_formula/tests/common.py

@ -0,0 +1,19 @@
# Copyright 2018 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
def setup_test_model(env, model_clses):
for model_cls in model_clses:
model_cls._build_model(env.registry, env.cr)
env.registry.setup_models(env.cr)
env.registry.init_models(
env.cr, [model_cls._name for model_cls in model_clses],
dict(env.context, update_custom_fields=True)
)
def teardown_test_model(env, model_clses):
for model_cls in model_clses:
del env.registry.models[model_cls._name]
env.registry.setup_models(env.cr)

112
base_tier_validation_formula/tests/test_tier_validation.py

@ -0,0 +1,112 @@
# Copyright 2018 Eficent Business and IT Consulting Services S.L.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo.tests import common
from odoo.exceptions import UserError
from .common import setup_test_model, teardown_test_model
from .tier_validation_tester import TierValidationTester
@common.at_install(False)
@common.post_install(True)
class TierTierValidation(common.SavepointCase):
@classmethod
def setUpClass(cls):
super(TierTierValidation, cls).setUpClass()
setup_test_model(cls.env, [TierValidationTester])
cls.test_model = cls.env[TierValidationTester._name]
cls.tester_model = cls.env['ir.model'].search([
('model', '=', 'tier.validation.tester')])
# 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,
})
# Create users:
group_ids = cls.env.ref('base.group_system').ids
cls.test_user_1 = cls.env['res.users'].create({
'name': 'John',
'login': 'test1',
'groups_id': [(6, 0, group_ids)],
})
cls.test_user_2 = cls.env['res.users'].create({
'name': 'Mike',
'login': 'test2',
})
cls.test_user_3 = cls.env['res.users'].create({
'name': 'Mary',
'login': 'test3',
})
# Create tier definitions:
cls.tier_def_obj = cls.env['tier.definition']
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', '>', 1.0)]",
})
cls.test_record = cls.test_model.create({
'test_field': 2.5,
})
@classmethod
def tearDownClass(cls):
teardown_test_model(cls.env,
[TierValidationTester])
super(TierTierValidation, cls).tearDownClass()
def test_01_reviewer_from_python_expression(self):
tier_definition = self.tier_def_obj.create({
'model_id': self.tester_model.id,
'review_type': 'individual',
'reviewer_id': self.test_user_1.id,
'definition_type': 'formula',
'python_code': 'rec.test_field > 1.0',
})
tier_definition.write({
'model_id': self.tester_model.id,
'review_type': 'expression',
'python_code': 'rec.test_field > 3.0',
})
tier_definition.onchange_review_type()
tier_definition.write({
'reviewer_expression': 'rec.user_id',
})
self.test_record.write({
'test_field': 3.5,
'user_id': self.test_user_2.id,
})
reviews = self.test_record.sudo(
self.test_user_3.id).request_validation()
self.assertTrue(reviews)
self.assertEqual(len(reviews), 2)
record = self.test_record.sudo(self.test_user_1.id)
self.assertIn(self.test_user_1, record.reviewer_ids)
self.assertIn(self.test_user_2, record.reviewer_ids)
res = self.test_model.search(
[('reviewer_ids', 'in', self.test_user_2.id)])
self.assertTrue(res)
def test_02_wrong_reviewer_expression(self):
"""Error should raise with incorrect python expresions on
tier definitions."""
self.tier_def_obj.create({
'model_id': self.tester_model.id,
'review_type': 'expression',
'reviewer_expression': 'rec.test_field',
'python_code': 'rec.test_field > 1.0',
})
with self.assertRaises(UserError):
self.test_record.sudo(self.test_user_3.id).request_validation()

23
base_tier_validation_formula/tests/tier_validation_tester.py

@ -0,0 +1,23 @@
# Copyright 2018 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class TierValidationTester(models.Model):
_name = 'tier.validation.tester'
_inherit = ['tier.validation']
state = fields.Selection(
selection=[('draft', 'Draft'),
('confirmed', 'Confirmed'),
('cancel', 'Cancel')],
default='draft',
)
test_field = fields.Float()
user_id = fields.Many2one(string="Assigned to:",
comodel_name="res.users")
@api.multi
def action_confirm(self):
self.write({'state': 'confirmed'})

25
base_tier_validation_formula/views/tier_definition_view.xml

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="tier_definition_view_form" model="ir.ui.view">
<field name="name">tier.definition.form</field>
<field name="model">tier.definition</field>
<field name="inherit_id"
ref="base_tier_validation.tier_definition_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form/sheet/group" position="after">
<group col="4">
<field name="reviewer_expression" colspan="4"
attrs="{'invisible': [('review_type', '!=', 'expression')]}"/>
</group>
</xpath>
<field name="definition_domain" position="after">
<field name="python_code"
attrs="{'invisible': [('definition_type', '!=', 'formula')]}"/>
</field>
</field>
</record>
</odoo>
Loading…
Cancel
Save