[MERGE] forward port branch saas-11.3 up to 691dd65e0e
This commit is contained in:
@@ -646,8 +646,11 @@ class AccountMoveLine(models.Model):
|
|||||||
#compute the default credit/debit of the next line in case of a manual entry
|
#compute the default credit/debit of the next line in case of a manual entry
|
||||||
balance = 0
|
balance = 0
|
||||||
for line in self._context['line_ids']:
|
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)
|
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:
|
if balance < 0:
|
||||||
rec.update({'debit': -balance})
|
rec.update({'debit': -balance})
|
||||||
if balance > 0:
|
if balance > 0:
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ class account_register_payments(models.TransientModel):
|
|||||||
'currency_id': self.currency_id.id,
|
'currency_id': self.currency_id.id,
|
||||||
'partner_id': invoices[0].commercial_partner_id.id,
|
'partner_id': invoices[0].commercial_partner_id.id,
|
||||||
'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type],
|
'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type],
|
||||||
|
'multi': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
|||||||
partner_ids = [partner['partner_id'] for partner in partners if partner['partner_id']]
|
partner_ids = [partner['partner_id'] for partner in partners if partner['partner_id']]
|
||||||
lines = dict((partner['partner_id'] or False, []) for partner in partners)
|
lines = dict((partner['partner_id'] or False, []) for partner in partners)
|
||||||
if not partner_ids:
|
if not partner_ids:
|
||||||
return [], [], []
|
return [], [], {}
|
||||||
|
|
||||||
# This dictionary will store the not due amount of all partners
|
# This dictionary will store the not due amount of all partners
|
||||||
undue_amounts = {}
|
undue_amounts = {}
|
||||||
|
|||||||
@@ -130,6 +130,20 @@ var StatementModel = BasicModel.extend({
|
|||||||
var line = this.getLine(handle);
|
var line = this.getLine(handle);
|
||||||
var prop = _.clone(_.find(line.mv_lines, {'id': mv_line_id}));
|
var prop = _.clone(_.find(line.mv_lines, {'id': mv_line_id}));
|
||||||
this._addProposition(line, prop);
|
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));
|
return $.when(this._computeLine(line), this._performMoveLine(handle));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -665,7 +679,7 @@ var StatementModel = BasicModel.extend({
|
|||||||
return !isNaN(prop.id) && prop.already_paid;
|
return !isNaN(prop.id) && prop.already_paid;
|
||||||
}), 'id'),
|
}), 'id'),
|
||||||
"new_aml_dicts": _.map(_.filter(props, function (prop) {
|
"new_aml_dicts": _.map(_.filter(props, function (prop) {
|
||||||
return isNaN(prop.id);
|
return isNaN(prop.id) && prop.display;
|
||||||
}), self._formatToProcessReconciliation.bind(self, line)),
|
}), self._formatToProcessReconciliation.bind(self, line)),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1262,7 +1276,7 @@ var ManualModel = StatementModel.extend({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id');
|
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({
|
process_reconciliations.push({
|
||||||
id: null,
|
id: null,
|
||||||
type: null,
|
type: null,
|
||||||
|
|||||||
@@ -423,7 +423,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
|||||||
|
|
||||||
// mv_lines
|
// mv_lines
|
||||||
var $mv_lines = this.$('.match table tbody').empty();
|
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}));
|
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
|
||||||
if (!isNaN(line.id)) {
|
if (!isNaN(line.id)) {
|
||||||
$('<span class="line_info_button fa fa-info-circle"/>')
|
$('<span class="line_info_button fa fa-info-circle"/>')
|
||||||
@@ -432,9 +433,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
|||||||
}
|
}
|
||||||
$mv_lines.append($line);
|
$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 .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
|
// balance
|
||||||
this.$('.popover').remove();
|
this.$('.popover').remove();
|
||||||
|
|||||||
@@ -1720,5 +1720,165 @@ QUnit.module('account', {
|
|||||||
|
|
||||||
clientAction.destroy();
|
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_move_lines') {
|
||||||
|
assert.deepEqual(args.args,
|
||||||
|
[
|
||||||
|
[{id: null, type: null,
|
||||||
|
mv_line_ids: [399, 402],
|
||||||
|
new_mv_line_dicts: []}
|
||||||
|
]
|
||||||
|
], "should call process_move_lines 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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -320,7 +320,7 @@
|
|||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="company_partner_id" invisible="1"/>
|
<field name="company_partner_id" invisible="1"/>
|
||||||
<field name="bank_account_id"/>
|
<field name="bank_account_id" context="{'default_partner_id': company_partner_id, 'form_view_ref': 'account.view_company_partner_bank_form'}"/>
|
||||||
<field name="bank_id"/>
|
<field name="bank_id"/>
|
||||||
</group>
|
</group>
|
||||||
<group >
|
<group >
|
||||||
@@ -364,6 +364,18 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="view_company_partner_bank_form" model="ir.ui.view">
|
||||||
|
<field name="name">company.res.partner.bank.form</field>
|
||||||
|
<field name="model">res.partner.bank</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_bank_form"/>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='partner_id']" position="attributes">
|
||||||
|
<attribute name="invisible">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record id="view_account_bank_journal_tree" model="ir.ui.view">
|
<record id="view_account_bank_journal_tree" model="ir.ui.view">
|
||||||
<field name="name">account.bank.journal.tree</field>
|
<field name="name">account.bank.journal.tree</field>
|
||||||
<field name="model">account.journal</field>
|
<field name="model">account.journal</field>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ odoo.define('im_livechat.im_livechat', function (require) {
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var bus = require('bus.bus').bus;
|
var bus = require('bus.bus').bus;
|
||||||
|
var concurrency = require('web.concurrency');
|
||||||
var config = require('web.config');
|
var config = require('web.config');
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var session = require('web.session');
|
var session = require('web.session');
|
||||||
@@ -313,6 +314,7 @@ var Feedback = Widget.extend({
|
|||||||
this.channel_uuid = channel_uuid;
|
this.channel_uuid = channel_uuid;
|
||||||
this.server_origin = session.origin;
|
this.server_origin = session.origin;
|
||||||
this.rating = undefined;
|
this.rating = undefined;
|
||||||
|
this.dp = new concurrency.DropPrevious();
|
||||||
},
|
},
|
||||||
|
|
||||||
on_click_smiley: function (ev) {
|
on_click_smiley: function (ev) {
|
||||||
@@ -348,7 +350,7 @@ var Feedback = Widget.extend({
|
|||||||
rate: this.rating,
|
rate: this.rating,
|
||||||
reason : options.reason
|
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) {
|
if (options.close) {
|
||||||
var emoji = RATING_TO_EMOJI[self.rating] || "??" ;
|
var emoji = RATING_TO_EMOJI[self.rating] || "??" ;
|
||||||
var content = _.str.sprintf(_t("Rating: %s"), emoji);
|
var content = _.str.sprintf(_t("Rating: %s"), emoji);
|
||||||
|
|||||||
@@ -215,6 +215,6 @@ class PushedFlow(models.Model):
|
|||||||
_inherit = "stock.location.path"
|
_inherit = "stock.location.path"
|
||||||
|
|
||||||
def _prepare_move_copy_values(self, move_to_copy, new_date):
|
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
|
res['purchase_line_id'] = None
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
<group string="Logistics">
|
<group string="Logistics">
|
||||||
<field name="route_ids" widget="many2many_tags" groups="stock.group_adv_location"/>
|
<field name="route_ids" widget="many2many_tags" groups="stock.group_adv_location"/>
|
||||||
<div colspan="2" attrs="{'invisible': [('parent_id', '=', False)]}">
|
<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"/>
|
<field name="total_route_ids" nolabel="1" widget="many2many_tags"/>
|
||||||
</div>
|
</div>
|
||||||
<field name="removal_strategy_id" options="{'no_create': True}"/>
|
<field name="removal_strategy_id" options="{'no_create': True}"/>
|
||||||
|
|||||||
@@ -547,10 +547,11 @@ function dragAndDrop($el, $to, options) {
|
|||||||
var toOffset = $to.offset();
|
var toOffset = $to.offset();
|
||||||
toOffset.top += $to.outerHeight()/2;
|
toOffset.top += $to.outerHeight()/2;
|
||||||
toOffset.left += $to.outerWidth()/2;
|
toOffset.left += $to.outerWidth()/2;
|
||||||
|
var vertical_offset = (toOffset.top < elementCenter.top) ? -1 : 1;
|
||||||
if (position === 'top') {
|
if (position === 'top') {
|
||||||
toOffset.top -= $to.outerHeight()/2;
|
toOffset.top -= $to.outerHeight()/2 + vertical_offset;
|
||||||
} else if (position === 'bottom') {
|
} else if (position === 'bottom') {
|
||||||
toOffset.top += $to.outerHeight()/2;
|
toOffset.top += $to.outerHeight()/2 - vertical_offset;
|
||||||
} else if (position === 'left') {
|
} else if (position === 'left') {
|
||||||
toOffset.left -= $to.outerWidth()/2;
|
toOffset.left -= $to.outerWidth()/2;
|
||||||
} else if (position === 'right') {
|
} else if (position === 'right') {
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ class Website(models.Model):
|
|||||||
|
|
||||||
if not pricelists: # no pricelist for this country, or no GeoIP
|
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))
|
pricelists |= all_pl.filtered(lambda pl: not show_visible or pl.selectable or pl.id in (current_pl, order_pl))
|
||||||
else:
|
if not show_visible and not country_code:
|
||||||
pricelists |= all_pl.filtered(lambda pl: not show_visible and pl.sudo().code)
|
pricelists |= all_pl.filtered(lambda pl: pl.sudo().code)
|
||||||
|
|
||||||
# This method is cached, must not return records! See also #8795
|
# This method is cached, must not return records! See also #8795
|
||||||
return pricelists.ids
|
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>
|
<del class='text-danger'><t t-esc='wish.price' t-options="{'widget': 'monetary', 'display_currency': wish.currency_id}"/></del>
|
||||||
<br/>
|
<br/>
|
||||||
</t>
|
</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>
|
||||||
<td class='text-center td-wish-btn'>
|
<td class='text-center td-wish-btn'>
|
||||||
<input name="product_id" t-att-value="wish.product_id.id" type="hidden"/>
|
<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
|
||||||
@@ -883,7 +883,7 @@ class UsersImplied(models.Model):
|
|||||||
for user in self.with_context({}):
|
for user in self.with_context({}):
|
||||||
gs = set(concat(g.trans_implied_ids for g in user.groups_id))
|
gs = set(concat(g.trans_implied_ids for g in user.groups_id))
|
||||||
vals = {'groups_id': [(4, g.id) for g in gs]}
|
vals = {'groups_id': [(4, g.id) for g in gs]}
|
||||||
super(UsersImplied, self).write(vals)
|
super(UsersImplied, user).write(vals)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
|||||||
@@ -33,3 +33,4 @@ from . import test_xmlrpc
|
|||||||
from . import test_res_partner_bank
|
from . import test_res_partner_bank
|
||||||
from . import test_reports
|
from . import test_reports
|
||||||
from . import test_tests_tags
|
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 json
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
@@ -481,12 +480,16 @@ class TestQWebNS(TransactionCase):
|
|||||||
</t>
|
</t>
|
||||||
"""
|
"""
|
||||||
})
|
})
|
||||||
error_msg = "Can't convert 'int' object to str implicitly"
|
|
||||||
if sys.version_info >= (3, 6):
|
try:
|
||||||
error_msg = "must be str, not int"
|
"" + 0
|
||||||
with self.assertRaisesRegexp(QWebException, error_msg):
|
except TypeError as e:
|
||||||
|
error_msg = e.args[0]
|
||||||
|
|
||||||
|
with self.assertRaises(QWebException, msg=error_msg):
|
||||||
view1.render()
|
view1.render()
|
||||||
|
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
class FileSystemLoader(object):
|
class FileSystemLoader(object):
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
|
|||||||
@@ -139,10 +139,14 @@ class TestOnChange(common.TransactionCase):
|
|||||||
self.env.cache.invalidate()
|
self.env.cache.invalidate()
|
||||||
result = self.Discussion.onchange(values, 'name', field_onchange)
|
result = self.Discussion.onchange(values, 'name', field_onchange)
|
||||||
self.assertIn('messages', result['value'])
|
self.assertIn('messages', result['value'])
|
||||||
self.assertEqual(result['value']['messages'], [
|
self.assertItemsEqual(result['value']['messages'], [
|
||||||
(5,),
|
(5,),
|
||||||
(1, message.id, {
|
(1, message.id, {
|
||||||
'name': "[%s] %s" % ("Foo", USER.name),
|
'name': "[%s] %s" % ("Foo", USER.name),
|
||||||
|
'body': message.body,
|
||||||
|
'author': message.author.name_get()[0],
|
||||||
|
'size': message.size,
|
||||||
|
'important': message.important,
|
||||||
}),
|
}),
|
||||||
(0, 0, {
|
(0, 0, {
|
||||||
'name': "[%s] %s" % ("Foo", USER.name),
|
'name': "[%s] %s" % ("Foo", USER.name),
|
||||||
@@ -252,7 +256,8 @@ class TestOnChange(common.TransactionCase):
|
|||||||
'lines': [
|
'lines': [
|
||||||
(5,),
|
(5,),
|
||||||
(1, line1.id, {'name': partner2.name,
|
(1, line1.id, {'name': partner2.name,
|
||||||
'partner': (partner2.id, partner2.name)}),
|
'partner': (partner2.id, partner2.name),
|
||||||
|
'tags': [(5,)]}),
|
||||||
(0, 0, {'name': partner2.name,
|
(0, 0, {'name': partner2.name,
|
||||||
'partner': (partner2.id, partner2.name),
|
'partner': (partner2.id, partner2.name),
|
||||||
'tags': [(5,)]}),
|
'tags': [(5,)]}),
|
||||||
@@ -260,17 +265,6 @@ class TestOnChange(common.TransactionCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# do it again, but this time with a new tag on the second line
|
# 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 = {
|
values = {
|
||||||
'name': partner1.name,
|
'name': partner1.name,
|
||||||
'partner': partner2.id, # this one just changed
|
'partner': partner2.id, # this one just changed
|
||||||
@@ -282,13 +276,18 @@ class TestOnChange(common.TransactionCase):
|
|||||||
self.env.cache.invalidate()
|
self.env.cache.invalidate()
|
||||||
|
|
||||||
result = multi.onchange(values, 'partner', field_onchange)
|
result = multi.onchange(values, 'partner', field_onchange)
|
||||||
self.assertEqual(result['value'], expected_new_tag_result)
|
self.assertEqual(result['value'], {
|
||||||
|
'name': partner2.name,
|
||||||
# ensure ID is not returned when asked and a many2many record is set to be created
|
'lines': [
|
||||||
self.env.cache.invalidate()
|
(5,),
|
||||||
|
(1, line1.id, {'name': partner2.name,
|
||||||
result = multi.onchange(values, 'partner', dict(field_onchange, **{'lines.tags.id': None}))
|
'partner': (partner2.id, partner2.name),
|
||||||
self.assertEqual(result['value'], expected_new_tag_result)
|
'tags': [(5,)]}),
|
||||||
|
(0, 0, {'name': partner2.name,
|
||||||
|
'partner': (partner2.id, partner2.name),
|
||||||
|
'tags': [(5,), (0, 0, {'name': 'Tag'})]}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
def test_onchange_specific(self):
|
def test_onchange_specific(self):
|
||||||
""" test the effect of field-specific onchange method """
|
""" test the effect of field-specific onchange method """
|
||||||
@@ -320,7 +319,8 @@ class TestOnChange(common.TransactionCase):
|
|||||||
self.assertIn('participants', result['value'])
|
self.assertIn('participants', result['value'])
|
||||||
self.assertItemsEqual(
|
self.assertItemsEqual(
|
||||||
result['value']['participants'],
|
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):
|
def test_onchange_default(self):
|
||||||
@@ -356,8 +356,6 @@ class TestOnChange(common.TransactionCase):
|
|||||||
self.assertEqual(len(discussion.messages), 3)
|
self.assertEqual(len(discussion.messages), 3)
|
||||||
messages = [(4, msg.id) for msg in discussion.messages]
|
messages = [(4, msg.id) for msg in discussion.messages]
|
||||||
messages[0] = (1, messages[0][1], {'body': 'test onchange'})
|
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 = {
|
values = {
|
||||||
'name': discussion.name,
|
'name': discussion.name,
|
||||||
'moderator': demo.id,
|
'moderator': demo.id,
|
||||||
@@ -368,7 +366,8 @@ class TestOnChange(common.TransactionCase):
|
|||||||
}
|
}
|
||||||
result = discussion.onchange(values, 'messages', field_onchange)
|
result = discussion.onchange(values, 'messages', field_onchange)
|
||||||
self.assertIn('message_concat', result['value'])
|
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):
|
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"""
|
""" 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],
|
'categories': [(4, cat.id) for cat in discussion.categories],
|
||||||
'messages': [(4, msg.id) for msg in discussion.messages],
|
'messages': [(4, msg.id) for msg in discussion.messages],
|
||||||
'participants': [(4, usr.id) for usr in discussion.participants],
|
'participants': [(4, usr.id) for usr in discussion.participants],
|
||||||
|
'message_changes': 0,
|
||||||
'important_messages': [(4, msg.id) for msg in discussion.important_messages],
|
'important_messages': [(4, msg.id) for msg in discussion.important_messages],
|
||||||
'important_emails': [(4, eml.id) for eml in discussion.important_emails],
|
'important_emails': [(4, eml.id) for eml in discussion.important_emails],
|
||||||
}
|
}
|
||||||
self.env.cache.invalidate()
|
|
||||||
result = discussion.onchange(values, 'name', field_onchange)
|
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(
|
self.assertEqual(
|
||||||
result['value']['important_emails'],
|
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):
|
def test_onchange_related(self):
|
||||||
|
|||||||
+29
-106
@@ -4982,114 +4982,28 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
if not all(name in self._fields for name in names):
|
if not all(name in self._fields for name in names):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
class PrefixTree(OrderedDict):
|
# filter out keys in field_onchange that do not refer to actual fields
|
||||||
""" A prefix tree for sequences of field names. The tree is a
|
dotnames = []
|
||||||
dictionary that associates each given field name to its
|
for dotname in field_onchange:
|
||||||
corresponding subtree (in fields order)::
|
try:
|
||||||
|
model = self.browse()
|
||||||
# tree corresponding to dotnames
|
for name in dotname.split('.'):
|
||||||
# ['name', 'line_ids.product_id', 'line_ids.tags_ids.name']
|
model = model[name]
|
||||||
{
|
dotnames.append(dotname)
|
||||||
'name': {},
|
except Exception:
|
||||||
'line_ids': {
|
pass
|
||||||
'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')
|
|
||||||
|
|
||||||
# create a new record with values, and attach ``self`` to it
|
# create a new record with values, and attach ``self`` to it
|
||||||
with env.do_in_onchange():
|
with env.do_in_onchange():
|
||||||
record = self.new(values)
|
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)
|
# attach ``self`` with a different context (for cache consistency)
|
||||||
record._origin = self.with_context(__onchange=True)
|
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():
|
with env.do_in_onchange():
|
||||||
before = snapshot(record)
|
for dotname in dotnames:
|
||||||
|
record.mapped(dotname)
|
||||||
|
|
||||||
# determine which field(s) should be triggered an onchange
|
# determine which field(s) should be triggered an onchange
|
||||||
todo = list(names) or list(values)
|
todo = list(names) or list(values)
|
||||||
@@ -5108,6 +5022,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
record[name] = value
|
record[name] = value
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
|
dirty = set()
|
||||||
|
|
||||||
# process names in order (or the keys of values if no name given)
|
# process names in order (or the keys of values if no name given)
|
||||||
while todo:
|
while todo:
|
||||||
@@ -5133,14 +5048,22 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
field.type in ('one2many', 'many2many') and newval._is_dirty()
|
field.type in ('one2many', 'many2many') and newval._is_dirty()
|
||||||
):
|
):
|
||||||
todo.append(name)
|
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():
|
with env.do_in_onchange():
|
||||||
after = snapshot(record)
|
result['value'] = {
|
||||||
|
name: self._fields[name].convert_to_onchange(record[name], record, subnames[name])
|
||||||
# determine values that have changed by comparing snapshots
|
for name in dirty
|
||||||
self.invalidate_cache()
|
}
|
||||||
result['value'] = diff(record, before, after)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user