[MERGE] forward port branch 11.0 up to 19558129f0

This commit is contained in:
Christophe Simonis
2019-01-17 20:49:36 +01:00
12 changed files with 178 additions and 26 deletions
+45 -1
View File
@@ -81,7 +81,7 @@
Your project is linked to an inactive analytic account. Change it, or reactivate the current one to timesheet on the project. Your project is linked to an inactive analytic account. Change it, or reactivate the current one to timesheet on the project.
</div> </div>
</group> </group>
<field name="timesheet_ids" attrs="{'invisible': [('allow_timesheets', '=', False)]}" context="{'default_project_id': project_id, 'default_name':'', 'default_analytic_account_id': analytic_account_id}"> <field name="timesheet_ids" mode="tree,kanban" attrs="{'invisible': [('allow_timesheets', '=', False)]}" context="{'default_project_id': project_id, 'default_name':'', 'default_analytic_account_id': analytic_account_id}">
<tree editable="bottom" string="Timesheet Activities" default_order="date"> <tree editable="bottom" string="Timesheet Activities" default_order="date">
<field name="date"/> <field name="date"/>
<field name="user_id" invisible="1"/> <field name="user_id" invisible="1"/>
@@ -90,6 +90,50 @@
<field name="unit_amount" string="Duration" widget="float_time"/> <field name="unit_amount" string="Duration" widget="float_time"/>
<field name="project_id" invisible="1"/> <field name="project_id" invisible="1"/>
</tree> </tree>
<kanban class="o_kanban_mobile">
<field name="date"/>
<field name="user_id"/>
<field name="employee_id"/>
<field name="name"/>
<field name="unit_amount"/>
<field name="project_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div class="row">
<div class="col-xs-6">
<strong><span><t t-esc="record.employee_id.value"/></span></strong>
</div>
<div class="col-xs-6 pull-right text-right">
<strong><t t-esc="record.date.value"/></strong>
</div>
</div>
<div class="row">
<div class="col-xs-6 text-muted">
<span><t t-esc="record.name.value"/></span>
</div>
<div class="col-xs-6">
<span class="pull-right text-right">
<field name="unit_amount" widget="float_time"/>
</span>
</div>
</div>
</div>
</t>
</templates>
</kanban>
<form string="Timesheet Activities">
<sheet>
<group>
<field name="date"/>
<field name="user_id" invisible="1"/>
<field name="employee_id" required="1"/>
<field name="name"/>
<field name="unit_amount" string="Duration" widget="float_time"/>
<field name="project_id" invisible="1"/>
</group>
</sheet>
</form>
</field> </field>
<group attrs="{'invisible': [('allow_timesheets', '=', False)]}"> <group attrs="{'invisible': [('allow_timesheets', '=', False)]}">
<group class="oe_subtotal_footer oe_right" name="project_hours"> <group class="oe_subtotal_footer oe_right" name="project_hours">
+1 -1
View File
@@ -18,7 +18,7 @@ function _parse_and_transform(nodes, transform_function) {
// Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url // Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
// Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match. // Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match.
var url_regexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.))[-a-z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13})\b(?:[-a-z0-9@:%_\+.~#?&'$//=]*)/gi; var url_regexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.))[-a-z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13})\b(?:[-a-z0-9@:%_\+.~#?&'$//=;]*)/gi;
function linkify(text, attrs) { function linkify(text, attrs) {
attrs = attrs || {}; attrs = attrs || {};
if (attrs.target === undefined) { if (attrs.target === undefined) {
+2 -2
View File
@@ -385,7 +385,7 @@ class ProductProduct(models.Model):
# Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
# Use `load=False` to not call `name_get` for the `product_tmpl_id` # Use `load=False` to not call `name_get` for the `product_tmpl_id`
self.sudo().read(['name', 'default_code', 'product_tmpl_id', 'attribute_value_ids'], load=False) self.sudo().read(['name', 'default_code', 'product_tmpl_id', 'attribute_value_ids', 'attribute_line_ids'], load=False)
product_template_ids = self.sudo().mapped('product_tmpl_id').ids product_template_ids = self.sudo().mapped('product_tmpl_id').ids
@@ -402,7 +402,7 @@ class ProductProduct(models.Model):
supplier_info_by_template.setdefault(r.product_tmpl_id, []).append(r) supplier_info_by_template.setdefault(r.product_tmpl_id, []).append(r)
for product in self.sudo(): for product in self.sudo():
# display only the attributes with multiple possible values on the template # display only the attributes with multiple possible values on the template
variable_attributes = product.attribute_value_ids.filtered(lambda v: len(v.attribute_id.value_ids) > 1).mapped('attribute_id') variable_attributes = product.attribute_line_ids.filtered(lambda l: len(l.value_ids) > 1).mapped('attribute_id')
variant = product.attribute_value_ids._variant_name(variable_attributes) variant = product.attribute_value_ids._variant_name(variable_attributes)
name = variant and "%s (%s)" % (product.name, variant) or product.name name = variant and "%s (%s)" % (product.name, variant) or product.name
+4
View File
@@ -251,6 +251,10 @@ class Survey(http.Controller):
def print_survey(self, survey, token=None, **post): def print_survey(self, survey, token=None, **post):
'''Display an survey in printable view; if <token> is set, it will '''Display an survey in printable view; if <token> is set, it will
grab the answers of the user_input_id that has <token>.''' grab the answers of the user_input_id that has <token>.'''
if survey.auth_required and request.env.user == request.website.user_id:
return request.render("survey.auth_required", {'survey': survey, 'token': token})
return request.render('survey.survey_print', return request.render('survey.survey_print',
{'survey': survey, {'survey': survey,
'token': token, 'token': token,
@@ -191,6 +191,10 @@ return Widget.extend({
icon: 'fa-star', icon: 'fa-star',
field: { field: {
get_context: function () { get_context: function () {
var filterContext = filter.context;
if (typeof filter.context === 'string') {
filterContext = pyeval.eval('context', filter.context);
}
var sortParsed = JSON.parse(filter.sort || "[]"); var sortParsed = JSON.parse(filter.sort || "[]");
var orderedBy = []; var orderedBy = [];
_.each(sortParsed, function (sort) { _.each(sortParsed, function (sort) {
@@ -200,7 +204,7 @@ return Widget.extend({
}); });
}); });
return _.defaults({}, filter.context, {orderedBy : orderedBy}); return _.defaults({}, filterContext, {orderedBy : orderedBy});
}, },
get_groupby: function () { return [filter.context]; }, get_groupby: function () { return [filter.context]; },
get_domain: function () { return filter.domain; } get_domain: function () { return filter.domain; }
@@ -208,7 +208,7 @@ var PivotModel = AbstractModel.extend({
return {has_data: false}; return {has_data: false};
} }
return { return {
colGroupBys: this.data.main_col.groupbys, colGroupBys: this.data.main_col.root.groupbys,
context: this.data.context, context: this.data.context,
domain: this.data.domain, domain: this.data.domain,
fields: this.fields, fields: this.fields,
@@ -217,7 +217,7 @@ var PivotModel = AbstractModel.extend({
mainColWidth: this.data.main_col.width, mainColWidth: this.data.main_col.width,
measures: this.data.measures, measures: this.data.measures,
rows: !isRaw && this._computeRows(), rows: !isRaw && this._computeRows(),
rowGroupBys: this.data.main_row.groupbys, rowGroupBys: this.data.main_row.root.groupbys,
sortedColumn: this.data.sorted_column, sortedColumn: this.data.sorted_column,
}; };
}, },
@@ -283,22 +283,19 @@ var PivotModel = AbstractModel.extend({
return this._loadData(); return this._loadData();
} }
var old_row_root = this.data.main_row.root; var old_row = this.data.main_row;
var old_col_root = this.data.main_col.root; var old_col = this.data.main_col;
return this._loadData().then(function () { return this._loadData().then(function () {
var new_groupby_length;
if (!('groupBy' in params) && !('pivot_row_groupby' in (params.context || {}))) { if (!('groupBy' in params) && !('pivot_row_groupby' in (params.context || {}))) {
// we only update the row groupbys according to the old groupbys // we only update the row groupbys according to the old groupbys
// if we don't have the key 'groupBy' in params. In that case, // if we don't have the key 'groupBy' in params. In that case,
// we want to have the full open state for the groupbys. // we want to have the full open state for the groupbys.
self._updateTree(old_row_root, self.data.main_row.root); self._updateTree(old_row.root, self.data.main_row.root);
new_groupby_length = self._getHeaderDepth(self.data.main_row.root) - 1; self._updateMainGroupBys(old_row, self.data.main_row);
self.data.main_row.groupbys = old_row_root.groupbys.slice(0, new_groupby_length);
} }
self._updateTree(old_col_root, self.data.main_col.root); self._updateTree(old_col.root, self.data.main_col.root);
new_groupby_length = self._getHeaderDepth(self.data.main_col.root) - 1; self._updateMainGroupBys(old_col, self.data.main_col);
self.data.main_row.groupbys = old_row_root.groupbys.slice(0, new_groupby_length);
}); });
}, },
/** /**
@@ -680,9 +677,6 @@ var PivotModel = AbstractModel.extend({
} }
} }
this.data.main_row.groupbys = rowGroupBys;
this.data.main_col.groupbys = colGroupBys;
main_row_header.other_root = main_col_header; main_row_header.other_root = main_col_header;
main_col_header.other_root = main_row_header; main_col_header.other_root = main_row_header;
@@ -751,6 +745,19 @@ var PivotModel = AbstractModel.extend({
this._traverseTree(root.children[i], f, arg1, arg2, arg3); this._traverseTree(root.children[i], f, arg1, arg2, arg3);
} }
}, },
/**
* Updates the groupBys that the main[row | col] holds
*
* @private
* @param {Object} old: The main abstract header before reload
* @param {Object} main: The main abstract header after reload
*
*/
_updateMainGroupBys: function (old, main) {
var new_groupby_length = this._getHeaderDepth(main.root) - 1;
var new_groupby_list = old.root.groupbys.slice(0, new_groupby_length);
main.root.groupbys = new_groupby_list;
},
/** /**
* @param {Object} old_tree * @param {Object} old_tree
* @param {Object} new_tree * @param {Object} new_tree
@@ -257,7 +257,6 @@ var ModelFieldSelector = Widget.extend({
if (this.dirty) { if (this.dirty) {
this.dirty = false; this.dirty = false;
this.pages = this.pages.slice(0, this.chain.length || 1);
this.trigger_up("field_chain_changed", {chain: this.chain}); this.trigger_up("field_chain_changed", {chain: this.chain});
} }
}, },
@@ -831,6 +831,80 @@ QUnit.module('Views', {
pivot.destroy(); pivot.destroy();
}); });
QUnit.test('correctly remove pivot_ keys from the context', function (assert) {
assert.expect(5);
this.data.partner.fields.amount = {string: "Amount", type: "float"};
// Equivalent to loading with default filter
var pivot = createView({
View: PivotView,
model: "partner",
data: this.data,
arch: '<pivot>' +
'<field name="date" interval="day" type="col"/>' +
'<field name="amount" type="measure"/>' +
'</pivot>',
viewOptions: {
context: {
pivot_measures: ['foo'],
pivot_column_groupby: ['customer'],
pivot_row_groupby: ['product_id'],
},
},
});
// Equivalent to unload the filter
var reloadParams = {
context: {},
};
pivot.reload(reloadParams);
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: ['customer'],
pivot_measures: ['foo'],
pivot_row_groupby: ['product_id'],
}, "context should be correct");
// Let's get rid of the rows groupBy
pivot.$('tbody .o_pivot_header_cell_opened').click();
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: ['customer'],
pivot_measures: ['foo'],
pivot_row_groupby: [],
}, "context should be correct");
// And now, get rid of the col groupby
pivot.$('thead .o_pivot_header_cell_opened').click();
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: [],
pivot_measures: ['foo'],
pivot_row_groupby: [],
}, "context should be correct");
pivot.$('tbody .o_pivot_header_cell_closed').click();
pivot.$('.o_pivot_field_menu li[data-field=product_id] a').click();
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: [],
pivot_measures: ['foo'],
pivot_row_groupby: ['product_id'],
}, "context should be correct");
pivot.$('thead .o_pivot_header_cell_closed').click();
pivot.$('.o_pivot_field_menu li[data-field=customer] a').click();
assert.deepEqual(pivot.getContext(), {
pivot_column_groupby: ['customer'],
pivot_measures: ['foo'],
pivot_row_groupby: ['product_id'],
}, "context should be correct");
pivot.destroy();
});
QUnit.test('correctly uses pivot_ keys from the context', function (assert) { QUnit.test('correctly uses pivot_ keys from the context', function (assert) {
assert.expect(7); assert.expect(7);
+11 -1
View File
@@ -389,7 +389,17 @@ class Web_Editor(http.Controller):
# Create a view to extend the template which adds the original file to link the new modified version instead # Create a view to extend the template which adds the original file to link the new modified version instead
IrUiView = request.env["ir.ui.view"] IrUiView = request.env["ir.ui.view"]
view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(lambda v: v.arch.find(url) >= 0)
def views_linking_url(view):
"""
Returns whether the view arch has some html link tag linked to the url.
(note: searching for the URL string is not enough as it could appear in a comment or an xpath expression.)
"""
return bool(etree.XML(view.arch).xpath("link[@href='{}']".format(url)))
view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url)
IrUiView.create(dict( IrUiView.create(dict(
name = custom_url, name = custom_url,
mode = "extension", mode = "extension",
+9
View File
@@ -0,0 +1,9 @@
Ukraine, 2019-01-09
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this declaration.
Signed,
Vyshnevska Iryna i.vyshnevska@mobilunity.com https://github.com/i-vyshnevska/
+2 -1
View File
@@ -455,7 +455,8 @@ class Module(models.Model):
""" """
modules_to_remove = self.mapped('name') modules_to_remove = self.mapped('name')
self.env['ir.model.data']._module_data_uninstall(modules_to_remove) self.env['ir.model.data']._module_data_uninstall(modules_to_remove)
self.write({'state': 'uninstalled', 'latest_version': False}) # we deactivate prefetching to not try to read a column that has been deleted
self.with_context(prefetch_fields=False).write({'state': 'uninstalled', 'latest_version': False})
return True return True
@api.multi @api.multi
@@ -127,14 +127,14 @@ class MergePartnerAutomatic(models.TransientModel):
# unique key treated # unique key treated
query = """ query = """
UPDATE "%(table)s" as ___tu UPDATE "%(table)s" as ___tu
SET %(column)s = %%s SET "%(column)s" = %%s
WHERE WHERE
%(column)s = %%s AND "%(column)s" = %%s AND
NOT EXISTS ( NOT EXISTS (
SELECT 1 SELECT 1
FROM "%(table)s" as ___tw FROM "%(table)s" as ___tw
WHERE WHERE
%(column)s = %%s AND "%(column)s" = %%s AND
___tu.%(value)s = ___tw.%(value)s ___tu.%(value)s = ___tw.%(value)s
)""" % query_dic )""" % query_dic
for partner in src_partners: for partner in src_partners:
@@ -142,7 +142,7 @@ class MergePartnerAutomatic(models.TransientModel):
else: else:
try: try:
with mute_logger('odoo.sql_db'), self._cr.savepoint(): with mute_logger('odoo.sql_db'), self._cr.savepoint():
query = 'UPDATE "%(table)s" SET %(column)s = %%s WHERE %(column)s IN %%s' % query_dic query = 'UPDATE "%(table)s" SET "%(column)s" = %%s WHERE "%(column)s" IN %%s' % query_dic
self._cr.execute(query, (dst_partner.id, tuple(src_partners.ids),)) self._cr.execute(query, (dst_partner.id, tuple(src_partners.ids),))
# handle the recursivity with parent relation # handle the recursivity with parent relation