[MERGE] forward port branch 11.0 up to f9e0f6663f
This commit is contained in:
@@ -503,8 +503,11 @@ class AccountMoveLine(models.Model):
|
||||
#compute the default credit/debit of the next line in case of a manual entry
|
||||
balance = 0
|
||||
for line in self._context['line_ids']:
|
||||
if line[2]:
|
||||
if line[2]: # in case of command 0: add a record with values
|
||||
balance += line[2].get('debit', 0) - line[2].get('credit', 0)
|
||||
elif line[0] == 2: # line has been deleted
|
||||
line_obj = self.browse(line[1])
|
||||
balance -= line_obj.debit - line_obj.credit
|
||||
if balance < 0:
|
||||
rec.update({'debit': -balance})
|
||||
if balance > 0:
|
||||
|
||||
@@ -68,7 +68,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
partner_ids = [partner['partner_id'] for partner in partners if partner['partner_id']]
|
||||
lines = dict((partner['partner_id'] or False, []) for partner in partners)
|
||||
if not partner_ids:
|
||||
return [], [], []
|
||||
return [], [], {}
|
||||
|
||||
# This dictionary will store the not due amount of all partners
|
||||
undue_amounts = {}
|
||||
|
||||
@@ -130,6 +130,20 @@ var StatementModel = BasicModel.extend({
|
||||
var line = this.getLine(handle);
|
||||
var prop = _.clone(_.find(line.mv_lines, {'id': mv_line_id}));
|
||||
this._addProposition(line, prop);
|
||||
|
||||
// Check whether we have some propositions left
|
||||
// If we don't, it means we are on an empty page
|
||||
// so let's go back to the previous page
|
||||
// Through the offset
|
||||
var propLineIds = _.map(line.reconciliation_proposition, function(prop) {
|
||||
return prop.id;
|
||||
});
|
||||
var leftOversProps = _.filter(line.mv_lines, function(mv_line) {
|
||||
return propLineIds.indexOf(mv_line.id) === -1;
|
||||
});
|
||||
if (line.offset && !leftOversProps.length) {
|
||||
line.offset -= line.limitMoveLines;
|
||||
}
|
||||
return $.when(this._computeLine(line), this._performMoveLine(handle));
|
||||
},
|
||||
/**
|
||||
@@ -665,7 +679,7 @@ var StatementModel = BasicModel.extend({
|
||||
return !isNaN(prop.id) && prop.already_paid;
|
||||
}), 'id'),
|
||||
"new_aml_dicts": _.map(_.filter(props, function (prop) {
|
||||
return isNaN(prop.id);
|
||||
return isNaN(prop.id) && prop.display;
|
||||
}), self._formatToProcessReconciliation.bind(self, line)),
|
||||
};
|
||||
|
||||
@@ -1268,7 +1282,7 @@ var ManualModel = StatementModel.extend({
|
||||
});
|
||||
} else {
|
||||
var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id');
|
||||
var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id);}), self._formatToProcessReconciliation.bind(self, line));
|
||||
var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id) && prop.display;}), self._formatToProcessReconciliation.bind(self, line));
|
||||
process_reconciliations.push({
|
||||
id: null,
|
||||
type: null,
|
||||
|
||||
@@ -405,7 +405,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
|
||||
// mv_lines
|
||||
var $mv_lines = this.$('.match table tbody').empty();
|
||||
_.each(state.mv_lines.slice(0, state.limitMoveLines), function (line) {
|
||||
var stateMvLines = state.mv_lines || [];
|
||||
_.each(stateMvLines.slice(0, state.limitMoveLines), function (line) {
|
||||
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
|
||||
if (!isNaN(line.id)) {
|
||||
$('<span class="line_info_button fa fa-info-circle"/>')
|
||||
@@ -414,9 +415,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
}
|
||||
$mv_lines.append($line);
|
||||
});
|
||||
this.$('.match .fa-chevron-right').toggleClass('disabled', state.mv_lines.length <= state.limitMoveLines);
|
||||
this.$('.match .fa-chevron-right').toggleClass('disabled', stateMvLines.length <= state.limitMoveLines);
|
||||
this.$('.match .fa-chevron-left').toggleClass('disabled', !state.offset);
|
||||
this.$('.match').css('max-height', !state.mv_lines.length && !state.filter.length ? '0px' : '');
|
||||
this.$('.match').css('max-height', !stateMvLines.length && !state.filter.length ? '0px' : '');
|
||||
|
||||
// balance
|
||||
this.$('.popover').remove();
|
||||
|
||||
@@ -1701,5 +1701,165 @@ QUnit.module('account', {
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Manual Reconciliation: remove a prop to attain balance and reconcile', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
// tweak the data to fit our needs
|
||||
this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'] = _.extend({}, this.params.data_for_manual_reconciliation_widget['[null,null]']);
|
||||
this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'].accounts[0].reconciliation_proposition = [
|
||||
{account_id: 283, account_type: "other", amount_currency_str: "", currency_id: false, date_maturity: "2017-03-18", date: "2017-02-16",
|
||||
total_amount_str: "$ 500.00", partner_id: 8, account_name: "101000 Current Assets", name: "INV/2017/0987", partner_name: "Agrolait",
|
||||
total_amount_currency_str: "", id: 999, credit: 0.0, journal_id: [1, "Customer Invoices"], amount_str: "$ 500.00", debit: 500.0,
|
||||
account_code: "101000", ref: "", already_paid: false}
|
||||
];
|
||||
|
||||
var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
|
||||
testUtils.addMockEnvironment(clientAction, {
|
||||
data: this.params.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'process_reconciliations') {
|
||||
assert.deepEqual(args.args,
|
||||
[
|
||||
[{id: null, type: null,
|
||||
mv_line_ids: [399, 402],
|
||||
new_mv_line_dicts: []}
|
||||
]
|
||||
], "should call process_reconciliations without the new mv line dict");
|
||||
}
|
||||
|
||||
return this._super(route, args);
|
||||
},
|
||||
session: {
|
||||
currencies: {
|
||||
3: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$"
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
clientAction.appendTo($('#qunit-fixture'));
|
||||
|
||||
// The first reconciliation "line" is where it happens
|
||||
var widget = clientAction.widgets[0];
|
||||
|
||||
// Add first prop
|
||||
widget.$('.match .cell_account_code:first').trigger('click');
|
||||
assert.notOk( widget.$('.cell_right .line_info_button').length,
|
||||
"should not display the partial reconciliation alert");
|
||||
|
||||
// Add second prop
|
||||
widget.$('.match .cell_account_code:first').trigger('click');
|
||||
|
||||
// Check that a create form is here
|
||||
var writeOffCreate = widget.$('div.create');
|
||||
|
||||
assert.equal(writeOffCreate.length, 1,
|
||||
'A write-off creation should be present');
|
||||
|
||||
assert.equal(writeOffCreate.find('input[name=amount]').val(), 500,
|
||||
'The right amount should be proposed for the write-off');
|
||||
|
||||
// remove the first line, the other two will balance one another
|
||||
widget.$('tr[data-line-id="999"] td:first').click()
|
||||
|
||||
var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)');
|
||||
assert.equal($buttonReconcile.length, 1,
|
||||
'The reconcile button must be visible');
|
||||
|
||||
$buttonReconcile.click();
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Manual Reconciliation: No lines for account', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
|
||||
testUtils.addMockEnvironment(clientAction, {
|
||||
data: this.params.data,
|
||||
session: {
|
||||
currencies: {
|
||||
3: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$"
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
clientAction.appendTo($('#qunit-fixture'));
|
||||
|
||||
// The second reconciliation "line" is where it happens
|
||||
var widget = clientAction.widgets[1];
|
||||
|
||||
var emptyLine = widget.$('tr.mv_line');
|
||||
|
||||
assert.notOk('data-line-id' in emptyLine.getAttributes(),
|
||||
'Empty line should be empty');
|
||||
|
||||
emptyLine.find('td:first').click();
|
||||
|
||||
// Check that a create form is here
|
||||
var writeOffCreate = widget.$('div.create .create_account_id');
|
||||
|
||||
assert.equal(writeOffCreate.length, 1,
|
||||
'A write-off creation should be present');
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Automatic Reconciliation: Don\'t loose pager when adding last line of a page', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
function standard_partner (list, partner_id) {
|
||||
_.each(list, function (item) {
|
||||
item.partner_id = partner_id;
|
||||
});
|
||||
};
|
||||
|
||||
this.params.options.params.limitMoveLines = 1;
|
||||
standard_partner(this.params.mv_lines['[5,"",0,6]'], 8);
|
||||
|
||||
// Overlap of those requests is normal
|
||||
// the pager is implemented as:
|
||||
// if there is at least 1 more line than my limit, the pager is active
|
||||
this.params.mv_lines['[5,"",0,2]'] = this.params.mv_lines['[5,"",0,6]'].slice(0,2);
|
||||
this.params.mv_lines['[5,"",1,2]'] = this.params.mv_lines['[5,"",0,6]'].slice(1,2);
|
||||
|
||||
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
|
||||
testUtils.addMockEnvironment(clientAction, {
|
||||
data: this.params.data,
|
||||
session: {
|
||||
currencies: {
|
||||
3: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$"
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
clientAction.appendTo($('#qunit-fixture'));
|
||||
|
||||
var widget = clientAction.widgets[0];
|
||||
|
||||
var $nextPage = widget.$('.match .match_controls .fa-chevron-right:not(disabled)');
|
||||
assert.ok($nextPage.length,
|
||||
'We should have a next page');
|
||||
$nextPage.click();
|
||||
|
||||
// Add the second's page record
|
||||
widget.$('.match tr[data-line-id=112] td:first').click();
|
||||
|
||||
assert.ok(widget.$('.match tr[data-line-id=109]').length,
|
||||
'the record of the first page must be proposed');
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('im_livechat.im_livechat', function (require) {
|
||||
"use strict";
|
||||
|
||||
var bus = require('bus.bus').bus;
|
||||
var concurrency = require('web.concurrency');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
@@ -306,6 +307,7 @@ var Feedback = Widget.extend({
|
||||
this.channel_uuid = channel_uuid;
|
||||
this.server_origin = session.origin;
|
||||
this.rating = undefined;
|
||||
this.dp = new concurrency.DropPrevious();
|
||||
},
|
||||
|
||||
on_click_smiley: function (ev) {
|
||||
@@ -315,7 +317,7 @@ var Feedback = Widget.extend({
|
||||
|
||||
// only display textearea if bad smiley selected
|
||||
var close_chat = false;
|
||||
if (this.rating === 0) {
|
||||
if (this.rating === 1) {
|
||||
this.$('.o_livechat_rating_reason').show();
|
||||
} else {
|
||||
this.$('.o_livechat_rating_reason').hide();
|
||||
@@ -341,7 +343,7 @@ var Feedback = Widget.extend({
|
||||
rate: this.rating,
|
||||
reason : options.reason
|
||||
};
|
||||
return session.rpc('/im_livechat/feedback', args).then(function () {
|
||||
this.dp.add(session.rpc('/im_livechat/feedback', args)).then(function () {
|
||||
if (options.close) {
|
||||
var content = _.str.sprintf(_t("Rating: :rating_%d"), self.rating);
|
||||
if (options.reason) {
|
||||
|
||||
@@ -192,6 +192,6 @@ class PushedFlow(models.Model):
|
||||
_inherit = "stock.location.path"
|
||||
|
||||
def _prepare_move_copy_values(self, move_to_copy, new_date):
|
||||
res = super()._prepare_move_copy_values(move_to_copy, new_date)
|
||||
res = super(PushedFlow, self)._prepare_move_copy_values(move_to_copy, new_date)
|
||||
res['purchase_line_id'] = None
|
||||
return res
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<group string="Logistics">
|
||||
<field name="route_ids" widget="many2many_tags" groups="stock.group_adv_location"/>
|
||||
<div colspan="2" attrs="{'invisible': [('parent_id', '=', False)]}">
|
||||
The following routes will apply to the products in this category taking into account parent categories:
|
||||
<field name="total_route_ids" nolabel="1" widget="many2many_tags"/>
|
||||
</div>
|
||||
<field name="removal_strategy_id" options="{'no_create': True}"/>
|
||||
|
||||
@@ -240,8 +240,8 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id" attrs="{'invisible': [('picking_type_code', '=', 'internal')]}"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/>
|
||||
<field name="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/>
|
||||
</group>
|
||||
<group>
|
||||
|
||||
@@ -548,10 +548,11 @@ function dragAndDrop($el, $to, options) {
|
||||
var toOffset = $to.offset();
|
||||
toOffset.top += $to.outerHeight()/2;
|
||||
toOffset.left += $to.outerWidth()/2;
|
||||
var vertical_offset = (toOffset.top < elementCenter.top) ? -1 : 1;
|
||||
if (position === 'top') {
|
||||
toOffset.top -= $to.outerHeight()/2;
|
||||
toOffset.top -= $to.outerHeight()/2 + vertical_offset;
|
||||
} else if (position === 'bottom') {
|
||||
toOffset.top += $to.outerHeight()/2;
|
||||
toOffset.top += $to.outerHeight()/2 - vertical_offset;
|
||||
} else if (position === 'left') {
|
||||
toOffset.left -= $to.outerWidth()/2;
|
||||
} else if (position === 'right') {
|
||||
|
||||
@@ -60,8 +60,8 @@ class Website(models.Model):
|
||||
|
||||
if not pricelists: # no pricelist for this country, or no GeoIP
|
||||
pricelists |= all_pl.filtered(lambda pl: not show_visible or pl.selectable or pl.id in (current_pl, order_pl))
|
||||
else:
|
||||
pricelists |= all_pl.filtered(lambda pl: not show_visible and pl.sudo().code)
|
||||
if not show_visible and not country_code:
|
||||
pricelists |= all_pl.filtered(lambda pl: pl.sudo().code)
|
||||
|
||||
# This method is cached, must not return records! See also #8795
|
||||
return pricelists.ids
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<del class='text-danger'><t t-esc='wish.price' t-options="{'widget': 'monetary', 'display_currency': wish.currency_id}"/></del>
|
||||
<br/>
|
||||
</t>
|
||||
<t t-esc='wish.price_new' t-options="{'widget': 'monetary', 'display_currency': wish.currency_id}"/>
|
||||
<t t-esc="wish.product_id.website_price" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}"/>
|
||||
</td>
|
||||
<td class='text-center td-wish-btn'>
|
||||
<input name="product_id" t-att-value="wish.product_id.id" type="hidden"/>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
India, 2018-07-31
|
||||
|
||||
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,
|
||||
|
||||
Jothimani Rajagopal jothimani1991@gmail.com https://github.com/jothimani-r
|
||||
@@ -695,7 +695,7 @@ class UsersImplied(models.Model):
|
||||
for user in self.with_context({}):
|
||||
gs = set(concat(g.trans_implied_ids for g in user.groups_id))
|
||||
vals = {'groups_id': [(4, g.id) for g in gs]}
|
||||
super(UsersImplied, self).write(vals)
|
||||
super(UsersImplied, user).write(vals)
|
||||
return res
|
||||
|
||||
#
|
||||
|
||||
@@ -33,3 +33,4 @@ from . import test_xmlrpc
|
||||
from . import test_res_partner_bank
|
||||
from . import test_reports
|
||||
from . import test_tests_tags
|
||||
from . import test_non_regression
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Non-Regression Tests
|
||||
"""
|
||||
|
||||
from openerp.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestNR(TransactionCase):
|
||||
def test_issue26036(self):
|
||||
U = self.env["res.users"]
|
||||
G = self.env["res.groups"]
|
||||
|
||||
group_A = G.create({"name": "A"})
|
||||
group_AA = G.create({"name": "AA", "implied_ids": [(6, 0, [group_A.id])]})
|
||||
group_B = G.create({"name": "B"})
|
||||
group_BB = G.create({"name": "BB", "implied_ids": [(6, 0, [group_B.id])]})
|
||||
group_C = G.create({"name": "C"})
|
||||
|
||||
user_a = U.create({"name": "a", "login": "a", "groups_id": [(6, 0, [group_AA.id])]})
|
||||
user_b = U.create({"name": "b", "login": "b", "groups_id": [(6, 0, [group_BB.id])]})
|
||||
|
||||
(user_a + user_b).write({"groups_id": [(4, group_C.id)]})
|
||||
|
||||
self.assertEqual(user_a.groups_id, (group_AA + group_A + group_C))
|
||||
self.assertEqual(user_b.groups_id, (group_BB + group_B + group_C))
|
||||
@@ -5,7 +5,6 @@ import collections
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from lxml import etree
|
||||
|
||||
@@ -481,12 +480,16 @@ class TestQWebNS(TransactionCase):
|
||||
</t>
|
||||
"""
|
||||
})
|
||||
error_msg = "Can't convert 'int' object to str implicitly"
|
||||
if sys.version_info >= (3, 6):
|
||||
error_msg = "must be str, not int"
|
||||
with self.assertRaisesRegexp(QWebException, error_msg):
|
||||
|
||||
try:
|
||||
"" + 0
|
||||
except TypeError as e:
|
||||
error_msg = e.args[0]
|
||||
|
||||
with self.assertRaises(QWebException, msg=error_msg):
|
||||
view1.render()
|
||||
|
||||
|
||||
from copy import deepcopy
|
||||
class FileSystemLoader(object):
|
||||
def __init__(self, path):
|
||||
|
||||
@@ -139,10 +139,14 @@ class TestOnChange(common.TransactionCase):
|
||||
self.env.cache.invalidate()
|
||||
result = self.Discussion.onchange(values, 'name', field_onchange)
|
||||
self.assertIn('messages', result['value'])
|
||||
self.assertEqual(result['value']['messages'], [
|
||||
self.assertItemsEqual(result['value']['messages'], [
|
||||
(5,),
|
||||
(1, message.id, {
|
||||
'name': "[%s] %s" % ("Foo", USER.name),
|
||||
'body': message.body,
|
||||
'author': message.author.name_get()[0],
|
||||
'size': message.size,
|
||||
'important': message.important,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': "[%s] %s" % ("Foo", USER.name),
|
||||
@@ -252,7 +256,8 @@ class TestOnChange(common.TransactionCase):
|
||||
'lines': [
|
||||
(5,),
|
||||
(1, line1.id, {'name': partner2.name,
|
||||
'partner': (partner2.id, partner2.name)}),
|
||||
'partner': (partner2.id, partner2.name),
|
||||
'tags': [(5,)]}),
|
||||
(0, 0, {'name': partner2.name,
|
||||
'partner': (partner2.id, partner2.name),
|
||||
'tags': [(5,)]}),
|
||||
@@ -260,17 +265,6 @@ class TestOnChange(common.TransactionCase):
|
||||
})
|
||||
|
||||
# do it again, but this time with a new tag on the second line
|
||||
expected_new_tag_result = {
|
||||
'name': partner2.name,
|
||||
'lines': [
|
||||
(5,),
|
||||
(1, line1.id, {'name': partner2.name,
|
||||
'partner': (partner2.id, partner2.name)}),
|
||||
(0, 0, {'name': partner2.name,
|
||||
'partner': (partner2.id, partner2.name),
|
||||
'tags': [(5,), (0, 0, {'name': 'Tag'})]}),
|
||||
],
|
||||
}
|
||||
values = {
|
||||
'name': partner1.name,
|
||||
'partner': partner2.id, # this one just changed
|
||||
@@ -282,13 +276,18 @@ class TestOnChange(common.TransactionCase):
|
||||
self.env.cache.invalidate()
|
||||
|
||||
result = multi.onchange(values, 'partner', field_onchange)
|
||||
self.assertEqual(result['value'], expected_new_tag_result)
|
||||
|
||||
# ensure ID is not returned when asked and a many2many record is set to be created
|
||||
self.env.cache.invalidate()
|
||||
|
||||
result = multi.onchange(values, 'partner', dict(field_onchange, **{'lines.tags.id': None}))
|
||||
self.assertEqual(result['value'], expected_new_tag_result)
|
||||
self.assertEqual(result['value'], {
|
||||
'name': partner2.name,
|
||||
'lines': [
|
||||
(5,),
|
||||
(1, line1.id, {'name': partner2.name,
|
||||
'partner': (partner2.id, partner2.name),
|
||||
'tags': [(5,)]}),
|
||||
(0, 0, {'name': partner2.name,
|
||||
'partner': (partner2.id, partner2.name),
|
||||
'tags': [(5,), (0, 0, {'name': 'Tag'})]}),
|
||||
],
|
||||
})
|
||||
|
||||
def test_onchange_specific(self):
|
||||
""" test the effect of field-specific onchange method """
|
||||
@@ -320,7 +319,8 @@ class TestOnChange(common.TransactionCase):
|
||||
self.assertIn('participants', result['value'])
|
||||
self.assertItemsEqual(
|
||||
result['value']['participants'],
|
||||
[(5,)] + [(4, user.id) for user in discussion.participants + demo],
|
||||
[(5,)] + [(1, user.id, {'display_name': user.display_name})
|
||||
for user in discussion.participants + demo],
|
||||
)
|
||||
|
||||
def test_onchange_default(self):
|
||||
@@ -356,8 +356,6 @@ class TestOnChange(common.TransactionCase):
|
||||
self.assertEqual(len(discussion.messages), 3)
|
||||
messages = [(4, msg.id) for msg in discussion.messages]
|
||||
messages[0] = (1, messages[0][1], {'body': 'test onchange'})
|
||||
lines = ["%s:%s" % (m.name, m.body) for m in discussion.messages]
|
||||
lines[0] = "%s:%s" % (discussion.messages[0].name, 'test onchange')
|
||||
values = {
|
||||
'name': discussion.name,
|
||||
'moderator': demo.id,
|
||||
@@ -368,7 +366,8 @@ class TestOnChange(common.TransactionCase):
|
||||
}
|
||||
result = discussion.onchange(values, 'messages', field_onchange)
|
||||
self.assertIn('message_concat', result['value'])
|
||||
self.assertEqual(result['value']['message_concat'], "\n".join(lines))
|
||||
self.assertEqual(result['value']['message_concat'],
|
||||
"\n".join(["%s:%s" % (m.name, m.body) for m in discussion.messages]))
|
||||
|
||||
def test_onchange_one2many_with_domain_on_related_field(self):
|
||||
""" test the value of the one2many field when defined with a domain on a related field"""
|
||||
@@ -414,15 +413,28 @@ class TestOnChange(common.TransactionCase):
|
||||
'categories': [(4, cat.id) for cat in discussion.categories],
|
||||
'messages': [(4, msg.id) for msg in discussion.messages],
|
||||
'participants': [(4, usr.id) for usr in discussion.participants],
|
||||
'message_changes': 0,
|
||||
'important_messages': [(4, msg.id) for msg in discussion.important_messages],
|
||||
'important_emails': [(4, eml.id) for eml in discussion.important_emails],
|
||||
}
|
||||
self.env.cache.invalidate()
|
||||
result = discussion.onchange(values, 'name', field_onchange)
|
||||
|
||||
# When one2many domain contains non-computed field, things are ok
|
||||
self.assertEqual(result['value']['important_messages'],
|
||||
[(5,)] + [(4, msg.id) for msg in discussion.important_messages])
|
||||
|
||||
# But here with commit 5676d81, we get value of: [(2, email.id)]
|
||||
self.assertEqual(
|
||||
result['value']['important_emails'],
|
||||
[(5,), (1, email.id, {'name': u'[Foo Bar] %s' % USER.name})],
|
||||
[(5,),
|
||||
(1, email.id, {
|
||||
'name': u'[Foo Bar] %s' % USER.name,
|
||||
'body': email.body,
|
||||
'author': USER.name_get()[0],
|
||||
'important': True,
|
||||
'email_to': demo.email,
|
||||
'size': email.size,
|
||||
})]
|
||||
)
|
||||
|
||||
def test_onchange_related(self):
|
||||
|
||||
+29
-106
@@ -5051,114 +5051,28 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
if not all(name in self._fields for name in names):
|
||||
return {}
|
||||
|
||||
class PrefixTree(OrderedDict):
|
||||
""" A prefix tree for sequences of field names. The tree is a
|
||||
dictionary that associates each given field name to its
|
||||
corresponding subtree (in fields order)::
|
||||
|
||||
# tree corresponding to dotnames
|
||||
# ['name', 'line_ids.product_id', 'line_ids.tags_ids.name']
|
||||
{
|
||||
'name': {},
|
||||
'line_ids': {
|
||||
'product_id': {},
|
||||
'tags_ids': {
|
||||
'name': {},
|
||||
},
|
||||
},
|
||||
}
|
||||
"""
|
||||
def __init__(self, model, dotnames):
|
||||
super(PrefixTree, self).__init__()
|
||||
if not dotnames:
|
||||
return
|
||||
# group dotnames by prefix
|
||||
suffixes = defaultdict(list)
|
||||
for dotname in dotnames:
|
||||
names = dotname.split('.', 1)
|
||||
name_suffixes = suffixes[names[0]]
|
||||
if len(names) > 1:
|
||||
name_suffixes.append(names[1])
|
||||
# fill in self in fields order
|
||||
for name in model._fields:
|
||||
if name in suffixes:
|
||||
self[name] = PrefixTree(model[name], suffixes[name])
|
||||
|
||||
def dotnames(self):
|
||||
""" Iterate over the sequences of field names. """
|
||||
for name, subnames in self.items():
|
||||
yield name
|
||||
for dotname in subnames.dotnames():
|
||||
yield "%s.%s" % (name, dotname)
|
||||
|
||||
nametree = PrefixTree(self.browse(), field_onchange)
|
||||
dotnames = list(nametree.dotnames())
|
||||
|
||||
def snapshot(record, tree=nametree):
|
||||
""" Return a dict with the values of record, following nametree. """
|
||||
vals = {}
|
||||
for name, subnames in tree.items():
|
||||
if subnames:
|
||||
# x2many fields as {line: snapshot(line), ...}
|
||||
vals[name] = OrderedDict(
|
||||
(line, snapshot(line, subnames))
|
||||
for line in record[name]
|
||||
)
|
||||
else:
|
||||
vals[name] = record[name]
|
||||
return vals
|
||||
|
||||
def diff(record, old, new, tree=nametree):
|
||||
""" Return the values that differ between snapshots.
|
||||
The snapshot ``old`` may be empty (for new records).
|
||||
"""
|
||||
result = {}
|
||||
for name, subnames in tree.items():
|
||||
if name == 'id':
|
||||
continue
|
||||
if old and old[name] == new[name]:
|
||||
continue
|
||||
field = record._fields[name]
|
||||
if not subnames:
|
||||
result[name] = field.convert_to_onchange(new[name], record, {})
|
||||
continue
|
||||
# x2many fields: serialize value as commands
|
||||
result[name] = commands = [(5,)]
|
||||
old_val = old.get(name) or {}
|
||||
for line, vals in new[name].items():
|
||||
vals0 = (old_val.get(line) or snapshot(line, subnames)) if line.id else {}
|
||||
line_diff = diff(line, vals0, vals, subnames)
|
||||
if not line.id:
|
||||
commands.append((0, line.id.ref or 0, line_diff))
|
||||
elif line_diff:
|
||||
commands.append((1, line.id, line_diff))
|
||||
else:
|
||||
commands.append((4, line.id))
|
||||
return result
|
||||
|
||||
# prefetch x2many lines without data (for the initial snapshot)
|
||||
for name, subnames in nametree.items():
|
||||
if subnames and values.get(name):
|
||||
# retrieve all ids in commands, and read the expected fields
|
||||
line_ids = []
|
||||
for cmd in values[name]:
|
||||
if cmd[0] in (1, 4):
|
||||
line_ids.append(cmd[1])
|
||||
elif cmd[0] == 6:
|
||||
line_ids.extend(cmd[2])
|
||||
lines = self.browse()[name].browse(line_ids)
|
||||
lines.read(list(subnames), load='_classic_write')
|
||||
# filter out keys in field_onchange that do not refer to actual fields
|
||||
dotnames = []
|
||||
for dotname in field_onchange:
|
||||
try:
|
||||
model = self.browse()
|
||||
for name in dotname.split('.'):
|
||||
model = model[name]
|
||||
dotnames.append(dotname)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# create a new record with values, and attach ``self`` to it
|
||||
with env.do_in_onchange():
|
||||
record = self.new(values)
|
||||
values = {name: record[name] for name in nametree}
|
||||
values = {name: record[name] for name in record._cache}
|
||||
# attach ``self`` with a different context (for cache consistency)
|
||||
record._origin = self.with_context(__onchange=True)
|
||||
|
||||
# make a snapshot based on the initial values of record
|
||||
# load fields on secondary records, to avoid false changes
|
||||
with env.do_in_onchange():
|
||||
before = snapshot(record)
|
||||
for dotname in dotnames:
|
||||
record.mapped(dotname)
|
||||
|
||||
# determine which field(s) should be triggered an onchange
|
||||
todo = list(names) or list(values)
|
||||
@@ -5177,6 +5091,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
record[name] = value
|
||||
|
||||
result = {}
|
||||
dirty = set()
|
||||
|
||||
# process names in order (or the keys of values if no name given)
|
||||
while todo:
|
||||
@@ -5202,14 +5117,22 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
field.type in ('one2many', 'many2many') and newval._is_dirty()
|
||||
):
|
||||
todo.append(name)
|
||||
dirty.add(name)
|
||||
|
||||
# make a snapshot based on the final values of record
|
||||
# determine subfields for field.convert_to_onchange() below
|
||||
Tree = lambda: defaultdict(Tree)
|
||||
subnames = Tree()
|
||||
for dotname in dotnames:
|
||||
subtree = subnames
|
||||
for name in dotname.split('.'):
|
||||
subtree = subtree[name]
|
||||
|
||||
# collect values from dirty fields
|
||||
with env.do_in_onchange():
|
||||
after = snapshot(record)
|
||||
|
||||
# determine values that have changed by comparing snapshots
|
||||
self.invalidate_cache()
|
||||
result['value'] = diff(record, before, after)
|
||||
result['value'] = {
|
||||
name: self._fields[name].convert_to_onchange(record[name], record, subnames[name])
|
||||
for name in dirty
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user