[MERGE] forward port branch 11.0 up to 84210074e5
This commit is contained in:
@@ -70,7 +70,7 @@ class AccountInvoice(models.Model):
|
||||
@api.onchange('amount_total')
|
||||
def _onchange_amount_total(self):
|
||||
for inv in self:
|
||||
if inv.amount_total < 0:
|
||||
if float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1:
|
||||
raise Warning(_('You cannot validate an invoice with a negative total amount. You should create a credit note instead.'))
|
||||
|
||||
@api.model
|
||||
@@ -784,7 +784,7 @@ class AccountInvoice(models.Model):
|
||||
to_open_invoices = self.filtered(lambda inv: inv.state != 'open')
|
||||
if to_open_invoices.filtered(lambda inv: inv.state != 'draft'):
|
||||
raise UserError(_("Invoice must be in draft state in order to validate it."))
|
||||
if to_open_invoices.filtered(lambda inv: inv.amount_total < 0):
|
||||
if to_open_invoices.filtered(lambda inv: float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1):
|
||||
raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead."))
|
||||
to_open_invoices.action_date_assign()
|
||||
to_open_invoices.action_move_create()
|
||||
|
||||
@@ -868,7 +868,7 @@ var StatementModel = BasicModel.extend({
|
||||
}) : false,
|
||||
account_code: self.accounts[line.st_line.open_balance_account_id],
|
||||
};
|
||||
line.balance.type = line.balance.amount_currency ? (line.balance.amount_currency > 0 && line.st_line.partner_id ? 0 : -1) : 1;
|
||||
line.balance.type = line.balance.amount_currency ? (line.st_line.partner_id ? 0 : -1) : 1;
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
<td class="cell_action"><span class="toggle_create fa fa-play"></span></td>
|
||||
<td class="cell_account_code"><t t-esc="state.balance.account_code"/></td>
|
||||
<td class="cell_due_date"></td>
|
||||
<td class="cell_label"><t t-if="state.balance.amount_currency < 0">Create Write-off</t><t t-elif="state.st_line.partner_id">Open balance</t><t t-else="">Choose counterpart</t></td>
|
||||
<td class="cell_label"><t t-if="state.st_line.partner_id">Open balance</t><t t-else="">Choose counterpart or Create Write-off</t></td>
|
||||
<td class="cell_left"><t t-if="state.balance.amount_currency < 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
|
||||
<td class="cell_right"><t t-if="state.balance.amount_currency > 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
|
||||
<td class="cell_info_popover"></td>
|
||||
|
||||
@@ -1028,8 +1028,8 @@ QUnit.module('account', {
|
||||
widget.$('.create .create_label input').val('test1').trigger('input');
|
||||
|
||||
assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$ 200.00", "should display the value 200.00 in left column");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Create Write-off' with 25.00 in left column");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Open balance' with 25.00 in left column");
|
||||
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 3, "should have 3 created reconcile lines");
|
||||
|
||||
clientAction.destroy();
|
||||
@@ -1135,8 +1135,8 @@ QUnit.module('account', {
|
||||
$('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click');
|
||||
|
||||
assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Open balance' with 145.00 in right column");
|
||||
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
|
||||
|
||||
clientAction.destroy();
|
||||
@@ -1160,7 +1160,7 @@ QUnit.module('account', {
|
||||
assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '),
|
||||
" ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67", "should display 4 lines");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '),
|
||||
"Create Write-off229.12", "should display the 'Create Write-off' line with value in left column");
|
||||
"Open balance229.12", "should display the 'Open balance' line with value in left column");
|
||||
|
||||
widget.$('.create .create_amount input').val('100').trigger('input');
|
||||
|
||||
@@ -1168,7 +1168,7 @@ QUnit.module('account', {
|
||||
" 101120 ATOS Banque 1075.00 101120 Tax 20.00% 215.00 101130 ATOS Frais 90.91 101300 Tax 10.00% include 9.09 ",
|
||||
"should update the value of the 4 lines (because the line must have 100% of the value)");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '),
|
||||
"Create Write-off215.00", "should change the 'Create Write-off' line because the 20.00% tax is not an include tax");
|
||||
"Open balance215.00", "should change the 'Open balance' line because the 20.00% tax is not an include tax");
|
||||
|
||||
widget.$('.accounting_view tbody .cell_account_code:first').trigger('click');
|
||||
widget.$('.accounting_view tbody .cell_label:first').trigger('click');
|
||||
|
||||
@@ -11,10 +11,10 @@ Barcodes encoding
|
||||
The Barcodes module defines barcode nomenclatures whose rules identify specific type
|
||||
of items e.g. products, locations. It contains the following features:
|
||||
|
||||
- Barcode patterns to identify barcodes containing a numerical value (e.g. weight, price)
|
||||
- Patterns to identify barcodes containing a numerical value (e.g. weight, price)
|
||||
- Definitin of barcode aliases that allow to identify the same product with different barcodes
|
||||
- Unlimited barcode patterns and definitions,
|
||||
- Barcode EAN13 encoding supported.
|
||||
- Support for encodings EAN-13, EAN-8 and UPC-A
|
||||
|
||||
Barcode encodings
|
||||
-----------------
|
||||
@@ -65,7 +65,7 @@ We now want to write a barcode for 2.75kg of oranges. This barcode should be '21
|
||||
numerical content of this barcode is '02750', and the correct checksum is '6'). When scanned, this
|
||||
barcode matches the Weighted Product rule (since is starts with '21'). The numerical content is extracted,
|
||||
and replaced by a sequence of '0's. The correct checksum is then computed for the obtained barcode
|
||||
('2100001000004') and the corresponding product (oranges) qgit is retrieved from product table.
|
||||
('2100001000004') and the corresponding product (oranges) is retrieved from product table.
|
||||
|
||||
Note: the special characters '{' and '}' in patterns are used to identify numerical content. To
|
||||
explicitely specify '{' or '}' in a pattern, they must be escaped.
|
||||
@@ -86,6 +86,17 @@ matching the original, 12-digit long, barcode.
|
||||
Barcodes scanning
|
||||
==============================
|
||||
|
||||
Scanning
|
||||
--------
|
||||
|
||||
Use a USB scanner (that mimics keyboard inputs) in order to work with barcodes in Odoo.
|
||||
The scanner must be configured to use no prefix and a carriage return or tab as suffix.
|
||||
The delay between each character input must be less than or equal to 50 milliseconds.
|
||||
Most barcode scanners will work out of the box.
|
||||
However, make sure the scanner uses the same keyboard layout as the device it's plugged in.
|
||||
Either by setting the device's keyboard layout to US QWERTY (default value for most readers)
|
||||
or by changing the scanner's keyboard layout (check the manual).
|
||||
|
||||
|
||||
Barcode events
|
||||
------------------------------
|
||||
@@ -3,27 +3,6 @@
|
||||
'version': '2.0',
|
||||
'category': 'Extra Tools',
|
||||
'summary': 'Barcodes Scanning and Parsing',
|
||||
'description': """
|
||||
This module adds support for barcode scanning and parsing.
|
||||
|
||||
Scanning
|
||||
--------
|
||||
Use a USB scanner (that mimics keyboard inputs) in order to work with barcodes in Odoo.
|
||||
The scanner must be configured to use no prefix and a carriage return or tab as suffix.
|
||||
The delay between each character input must be less than or equal to 50 milliseconds.
|
||||
Most barcode scanners will work out of the box.
|
||||
However, make sure the scanner uses the same keyboard layout as the device it's plugged in.
|
||||
Either by setting the device's keyboard layout to US QWERTY (default value for most readers)
|
||||
or by changing the scanner's keyboard layout (check the manual).
|
||||
|
||||
Parsing
|
||||
-------
|
||||
The barcodes are interpreted using the rules defined by a nomenclature.
|
||||
It provides the following features:
|
||||
- Patterns to identify barcodes containing a numerical value (e.g. weight, price)
|
||||
- Definition of barcode aliases that allow to identify the same product with different barcodes
|
||||
- Support for encodings EAN-13, EAN-8 and UPC-A
|
||||
""",
|
||||
'depends': ['web'],
|
||||
'data': [
|
||||
'data/barcodes_data.xml',
|
||||
|
||||
@@ -123,9 +123,10 @@ class MailActivity(models.Model):
|
||||
|
||||
for record in self.filtered(lambda activity: activity.date_deadline):
|
||||
today = today_default
|
||||
if record.user_id.tz:
|
||||
tz = record.user_id.sudo().tz
|
||||
if tz:
|
||||
today_utc = pytz.UTC.localize(datetime.utcnow())
|
||||
today_tz = today_utc.astimezone(pytz.timezone(record.user_id.tz))
|
||||
today_tz = today_utc.astimezone(pytz.timezone(tz))
|
||||
today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day)
|
||||
|
||||
date_deadline = fields.Date.from_string(record.date_deadline)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<label for="product_uom_qty"/>
|
||||
<div class="o_row">
|
||||
<span><field name="product_uom_qty" readonly="1" nolabel="1"/></span>
|
||||
<span><field name="product_uom" readonly="1" nolabel="1"/></span>
|
||||
<span><field name="product_uom" readonly="1" force_save="1" nolabel="1"/></span>
|
||||
</div>
|
||||
<label for="quantity_done"/>
|
||||
<div class="o_row">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
|
||||
|
||||
class CrmTeam(models.Model):
|
||||
@@ -41,13 +42,16 @@ class CrmTeam(models.Model):
|
||||
""" If the type of the sales team is point of sale ('pos'), the graph will display the sales data.
|
||||
The override here is to get data from pos.order instead of sale.order.
|
||||
"""
|
||||
offset = datetime.now(pytz.timezone(self.env.user.tz or 'UTC')).utcoffset()
|
||||
min_date = fields.Datetime.to_string(datetime.combine(start_date, datetime.min.time()) - offset)
|
||||
max_date = fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()) - offset)
|
||||
if self.team_type == 'pos':
|
||||
result = []
|
||||
if self.dashboard_graph_group_pos == 'pos':
|
||||
order_data = self.env['report.pos.order'].read_group(
|
||||
domain=[
|
||||
('date', '>=', fields.Date.to_string(start_date)),
|
||||
('date', '<=', fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()))),
|
||||
('date', '>=', min_date),
|
||||
('date', '<=', max_date),
|
||||
('config_id', 'in', self.pos_config_ids.ids),
|
||||
('state', 'in', ['paid', 'done', 'invoiced'])],
|
||||
fields=['config_id', 'price_total'],
|
||||
@@ -63,8 +67,8 @@ class CrmTeam(models.Model):
|
||||
elif self.dashboard_graph_group_pos == 'user':
|
||||
order_data = self.env['report.pos.order'].read_group(
|
||||
domain=[
|
||||
('date', '>=', fields.Date.to_string(start_date)),
|
||||
('date', '<=', fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()))),
|
||||
('date', '>=', min_date),
|
||||
('date', '<=', max_date),
|
||||
('config_id', 'in', self.pos_config_ids.ids),
|
||||
('state', 'in', ['paid', 'done', 'invoiced'])],
|
||||
fields=['user_id', 'price_total'],
|
||||
@@ -78,8 +82,8 @@ class CrmTeam(models.Model):
|
||||
# /!\ do not use en_US as it's not ISO-standard and does not match datetime's library
|
||||
order_data = self.env['report.pos.order'].with_context(lang='en_GB').read_group(
|
||||
domain=[
|
||||
('date', '>=', fields.Date.to_string(start_date)),
|
||||
('date', '<=', fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()))),
|
||||
('date', '>=', min_date),
|
||||
('date', '<=', max_date),
|
||||
('config_id', 'in', self.pos_config_ids.ids),
|
||||
('state', 'in', ['paid', 'done', 'invoiced'])],
|
||||
fields=['date', 'price_total'],
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
'data': [
|
||||
'security/project_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/project_data.xml',
|
||||
'report/project_report_views.xml',
|
||||
'views/rating_views.xml',
|
||||
'views/project_views.xml',
|
||||
@@ -33,6 +32,7 @@
|
||||
'views/project_portal_templates.xml',
|
||||
'views/project_rating_templates.xml',
|
||||
'data/project_mail_template_data.xml',
|
||||
'data/project_data.xml',
|
||||
],
|
||||
'qweb': ['static/src/xml/project.xml'],
|
||||
'demo': ['data/project_demo.xml'],
|
||||
|
||||
@@ -16,7 +16,7 @@ class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
purchase_line_id = fields.Many2one('purchase.order.line',
|
||||
'Purchase Order Line', ondelete='set null', index=True, readonly=True, copy=False)
|
||||
'Purchase Order Line', ondelete='set null', index=True, readonly=True)
|
||||
created_purchase_line_id = fields.Many2one('purchase.order.line',
|
||||
'Created Purchase Order Line', ondelete='set null', readonly=True, copy=False)
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ class CrmTeam(models.Model):
|
||||
|
||||
self.ensure_one()
|
||||
values = []
|
||||
today = date.today()
|
||||
today = fields.Date.from_string(fields.Date.context_today(self))
|
||||
start_date, end_date = self._graph_get_dates(today)
|
||||
graph_data = self._graph_data(start_date, end_date)
|
||||
|
||||
|
||||
@@ -1564,6 +1564,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
|
||||
|
||||
this.$('form.o_form_binary_form').submit();
|
||||
this.$('.oe_fileupload').hide();
|
||||
ev.target.value = "";
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -814,7 +814,8 @@ var BasicModel = AbstractModel.extend({
|
||||
return this.mutex.exec(this._applyChange.bind(this, record_id, changes, options));
|
||||
},
|
||||
/**
|
||||
* Reload all data for a given resource
|
||||
* Reload all data for a given resource. At any time there is at most one
|
||||
* reload operation active.
|
||||
*
|
||||
* @param {string} id local id for a resource
|
||||
* @param {Object} [options]
|
||||
@@ -823,64 +824,7 @@ var BasicModel = AbstractModel.extend({
|
||||
* @returns {Deferred<string>} resolves to the id of the resource
|
||||
*/
|
||||
reload: function (id, options) {
|
||||
options = options || {};
|
||||
var element = this.localData[id];
|
||||
|
||||
if (element.type === 'record') {
|
||||
if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
|
||||
var params = {
|
||||
context: element.context,
|
||||
fieldsInfo: element.fieldsInfo,
|
||||
fields: element.fields,
|
||||
viewType: element.viewType,
|
||||
};
|
||||
return this._makeDefaultRecord(element.model, params);
|
||||
}
|
||||
if (!options.keepChanges) {
|
||||
this.discardChanges(id, {rollback: false});
|
||||
}
|
||||
} else if (element._changes) {
|
||||
delete element.tempLimitIncrement;
|
||||
_.each(element._changes, function (change) {
|
||||
delete change.isNew;
|
||||
});
|
||||
}
|
||||
|
||||
if (options.context !== undefined) {
|
||||
element.context = options.context;
|
||||
}
|
||||
if (options.domain !== undefined) {
|
||||
element.domain = options.domain;
|
||||
}
|
||||
if (options.groupBy !== undefined) {
|
||||
element.groupedBy = options.groupBy;
|
||||
}
|
||||
if (options.limit !== undefined) {
|
||||
element.limit = options.limit;
|
||||
}
|
||||
if (options.offset !== undefined) {
|
||||
this._setOffset(element.id, options.offset);
|
||||
}
|
||||
if (options.loadMoreOffset !== undefined) {
|
||||
element.loadMoreOffset = options.loadMoreOffset;
|
||||
} else {
|
||||
// reset if not specified
|
||||
element.loadMoreOffset = 0;
|
||||
}
|
||||
if (options.currentId !== undefined) {
|
||||
element.res_id = options.currentId;
|
||||
}
|
||||
if (options.ids !== undefined) {
|
||||
element.res_ids = options.ids;
|
||||
element.count = element.res_ids.length;
|
||||
}
|
||||
if (element.type === 'record') {
|
||||
element.offset = _.indexOf(element.res_ids, element.res_id);
|
||||
}
|
||||
var loadOptions = _.pick(options, 'fieldNames', 'viewType');
|
||||
return this._load(element, loadOptions).then(function (result) {
|
||||
return result.id;
|
||||
});
|
||||
return this.mutex.exec(this._reload.bind(this, id, options));
|
||||
},
|
||||
/**
|
||||
* In some case, we may need to remove an element from a list, without going
|
||||
@@ -2917,13 +2861,10 @@ var BasicModel = AbstractModel.extend({
|
||||
continue;
|
||||
}
|
||||
changes = this._generateChanges(relRecord, options);
|
||||
if (changes.id) {
|
||||
if (!this.isNew(relRecord.id)) {
|
||||
// the subrecord already exists in db
|
||||
commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id));
|
||||
delete changes.id;
|
||||
if (this.isNew(record.id)) {
|
||||
// if the main record is new, link the subrecord to it
|
||||
commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id));
|
||||
}
|
||||
if (!_.isEmpty(changes)) {
|
||||
commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes));
|
||||
}
|
||||
@@ -4013,6 +3954,76 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Reload all data for a given resource
|
||||
*
|
||||
* @private
|
||||
* @param {string} id local id for a resource
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.keepChanges=false] if true, doesn't discard the
|
||||
* changes on the record before reloading it
|
||||
* @returns {Deferred<string>} resolves to the id of the resource
|
||||
*/
|
||||
_reload: function (id, options) {
|
||||
options = options || {};
|
||||
var element = this.localData[id];
|
||||
|
||||
if (element.type === 'record') {
|
||||
if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
|
||||
var params = {
|
||||
context: element.context,
|
||||
fieldsInfo: element.fieldsInfo,
|
||||
fields: element.fields,
|
||||
viewType: element.viewType,
|
||||
};
|
||||
return this._makeDefaultRecord(element.model, params);
|
||||
}
|
||||
if (!options.keepChanges) {
|
||||
this.discardChanges(id, {rollback: false});
|
||||
}
|
||||
} else if (element._changes) {
|
||||
delete element.tempLimitIncrement;
|
||||
_.each(element._changes, function (change) {
|
||||
delete change.isNew;
|
||||
});
|
||||
}
|
||||
|
||||
if (options.context !== undefined) {
|
||||
element.context = options.context;
|
||||
}
|
||||
if (options.domain !== undefined) {
|
||||
element.domain = options.domain;
|
||||
}
|
||||
if (options.groupBy !== undefined) {
|
||||
element.groupedBy = options.groupBy;
|
||||
}
|
||||
if (options.limit !== undefined) {
|
||||
element.limit = options.limit;
|
||||
}
|
||||
if (options.offset !== undefined) {
|
||||
this._setOffset(element.id, options.offset);
|
||||
}
|
||||
if (options.loadMoreOffset !== undefined) {
|
||||
element.loadMoreOffset = options.loadMoreOffset;
|
||||
} else {
|
||||
// reset if not specified
|
||||
element.loadMoreOffset = 0;
|
||||
}
|
||||
if (options.currentId !== undefined) {
|
||||
element.res_id = options.currentId;
|
||||
}
|
||||
if (options.ids !== undefined) {
|
||||
element.res_ids = options.ids;
|
||||
element.count = element.res_ids.length;
|
||||
}
|
||||
if (element.type === 'record') {
|
||||
element.offset = _.indexOf(element.res_ids, element.res_id);
|
||||
}
|
||||
var loadOptions = _.pick(options, 'fieldNames', 'viewType');
|
||||
return this._load(element, loadOptions).then(function (result) {
|
||||
return result.id;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Allows to save a value in the specialData cache associated to a given
|
||||
* record and fieldName. If the value in the cache was already the given
|
||||
|
||||
@@ -3207,6 +3207,136 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('onchange for embedded one2many in a one2many with a second page', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.turtle.fields.partner_ids.type = 'one2many';
|
||||
this.data.turtle.records[0].partner_ids = [1];
|
||||
// we need a second page, so we set two records and only display one per page
|
||||
this.data.partner.records[0].turtles = [1, 2];
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
turtles: function (obj) {
|
||||
obj.turtles = [
|
||||
[5],
|
||||
[1, 1, {
|
||||
turtle_foo: "hop",
|
||||
partner_ids: [[5], [4, 1]],
|
||||
}],
|
||||
[1, 2, {
|
||||
turtle_foo: "blip",
|
||||
partner_ids: [[5], [4, 2], [4, 4]],
|
||||
}],
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom" limit="1">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'<field name="partner_ids" widget="many2many_tags"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'write') {
|
||||
var expectedResultTurtles = [
|
||||
[1, 1, {
|
||||
turtle_foo: "hop",
|
||||
}],
|
||||
[1, 2, {
|
||||
partner_ids: [[4, 2, false], [4, 4, false]],
|
||||
turtle_foo: "blip",
|
||||
}],
|
||||
];
|
||||
assert.deepEqual(args.args[1].turtles, expectedResultTurtles,
|
||||
"the right values should be written");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_data_cell').eq(1).click();
|
||||
var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
|
||||
$cell.val("hop").trigger('change');
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('onchange for embedded one2many in a one2many updated by server', function (assert) {
|
||||
// here we test that after an onchange, the embedded one2many field has
|
||||
// been updated by a new list of ids by the server response, to this new
|
||||
// list should be correctly sent back at save time
|
||||
assert.expect(3);
|
||||
|
||||
this.data.turtle.fields.partner_ids.type = 'one2many';
|
||||
this.data.partner.records[0].turtles = [2];
|
||||
this.data.turtle.records[1].partner_ids = [2];
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
turtles: function (obj) {
|
||||
obj.turtles = [
|
||||
[5],
|
||||
[1, 2, {
|
||||
turtle_foo: "hop",
|
||||
partner_ids: [[5], [4, 2], [4, 4]],
|
||||
}],
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'<field name="partner_ids" widget="many2many_tags"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/call_kw/partner/write') {
|
||||
var expectedResultTurtles = [
|
||||
[1, 2, {
|
||||
partner_ids: [[4, 2, false], [4, 4, false]],
|
||||
turtle_foo: "hop",
|
||||
}],
|
||||
];
|
||||
assert.deepEqual(args.args[1].turtles, expectedResultTurtles,
|
||||
'The right values should be written');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim(), "second record",
|
||||
"the partner_ids should be as specified at initialization");
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_data_cell').eq(1).click();
|
||||
var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
|
||||
$cell.val("hop").trigger("change");
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim().split(/\s+/),
|
||||
[ "second", "record", "aaa" ],
|
||||
'The partner_ids should have been updated');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('onchange for embedded one2many with handle widget', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -237,7 +237,6 @@ var MockServer = Class.extend({
|
||||
if (attrs) {
|
||||
attrs = pyeval.py_eval(attrs);
|
||||
_.extend(modifiers, attrs);
|
||||
node.removeAttribute('attrs');
|
||||
}
|
||||
|
||||
var states = node.getAttribute('states');
|
||||
|
||||
@@ -3582,6 +3582,56 @@ QUnit.module('Views', {
|
||||
testUtils.unpatch(mixins.ParentedMixin);
|
||||
});
|
||||
|
||||
QUnit.test('grouped kanban becomes ungrouped when clearing domain then clearing groupby', function (assert) {
|
||||
// in this test, we simulate that clearing the domain is slow, so that
|
||||
// clearing the groupby does not corrupt the data handled while
|
||||
// reloading the kanban view.
|
||||
assert.expect(4);
|
||||
|
||||
var def = $.Deferred();
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test">' +
|
||||
'<field name="bar"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates></kanban>',
|
||||
domain: [['foo', '=', 'norecord']],
|
||||
groupBy: ['bar'],
|
||||
mockRPC: function (route, args) {
|
||||
var result = this._super(route, args);
|
||||
if (args.method === 'read_group') {
|
||||
var isFirstUpdate = _.isEmpty(args.kwargs.domain) &&
|
||||
args.kwargs.groupby &&
|
||||
args.kwargs.groupby[0] === 'bar';
|
||||
if (isFirstUpdate) {
|
||||
return def.then(_.constant(result));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'),
|
||||
"the kanban view should be grouped");
|
||||
assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_ungrouped'),
|
||||
"the kanban view should not be ungrouped");
|
||||
|
||||
kanban.update({domain: []}); // 1st update on kanban view
|
||||
kanban.update({groupBy: false}); // 2n update on kanban view
|
||||
def.resolve(); // simulate slow 1st update of kanban view
|
||||
|
||||
assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'),
|
||||
"the kanban view should not longer be grouped");
|
||||
assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_ungrouped'),
|
||||
"the kanban view should have become ungrouped");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
|
||||
}),
|
||||
events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.events || {}, {
|
||||
'click > a': '_onMenuToggleClick',
|
||||
'click > #o_new_content_menu_choices': '_onBackgroundClick',
|
||||
}),
|
||||
|
||||
/**
|
||||
@@ -73,6 +74,15 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
|
||||
ev.preventDefault();
|
||||
this.$newContentMenuChoices.toggleClass('o_hidden');
|
||||
},
|
||||
/**
|
||||
* Called when a click outside the menu's options occurs -> Closes the menu
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onBackgroundClick: function (ev) {
|
||||
this.$newContentMenuChoices.addClass('o_hidden');
|
||||
},
|
||||
});
|
||||
|
||||
websiteNavbarData.websiteNavbarRegistry.add(NewContentMenu, '.o_new_content_menu');
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
India, 2018-06-27
|
||||
|
||||
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,
|
||||
|
||||
Maulik Raval maulik.raval502@gmail.com https://github.com/maulik-raval
|
||||
@@ -413,6 +413,12 @@ actual arch.
|
||||
self.clear_caches()
|
||||
return super(View, self).write(self._compute_defaults(vals))
|
||||
|
||||
def unlink(self):
|
||||
# if in uninstall mode and has children views, emulate an ondelete cascade
|
||||
if self.env.context.get('_force_unlink', False) and self.mapped('inherit_children_ids'):
|
||||
self.mapped('inherit_children_ids').unlink()
|
||||
super(View, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def toggle(self):
|
||||
""" Switches between enabled and disabled statuses
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<form string="Create Menu">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="menu_id" domain="[('parent_id','!=',False)]"/>
|
||||
<field name="menu_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="menu_create" string="Create _Menu" type="object" class="btn-primary"/>
|
||||
|
||||
+13
-4
@@ -120,7 +120,17 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
|
||||
continue
|
||||
|
||||
_logger.debug('loading module %s (%d/%d)', module_name, index, module_count)
|
||||
migrations.migrate_module(package, 'pre')
|
||||
|
||||
needs_update = (
|
||||
hasattr(package, "init")
|
||||
or hasattr(package, "update")
|
||||
or package.state in ("to install", "to upgrade")
|
||||
)
|
||||
if needs_update:
|
||||
if package.name != 'base':
|
||||
registry.setup_models(cr)
|
||||
migrations.migrate_module(package, 'pre')
|
||||
|
||||
load_openerp_module(package.name)
|
||||
|
||||
new_install = package.state == 'to install'
|
||||
@@ -133,8 +143,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
|
||||
model_names = registry.load(cr, package)
|
||||
|
||||
loaded_modules.append(package.name)
|
||||
if (hasattr(package, 'init') or hasattr(package, 'update')
|
||||
or package.state in ('to install', 'to upgrade')):
|
||||
if needs_update:
|
||||
models_updated |= set(model_names)
|
||||
models_to_check -= set(model_names)
|
||||
registry.setup_models(cr)
|
||||
@@ -154,7 +163,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
|
||||
if hasattr(package, 'init') or package.state == 'to install':
|
||||
mode = 'init'
|
||||
|
||||
if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
|
||||
if needs_update:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
# Can't put this line out of the loop: ir.module.module will be
|
||||
# registered by init_models() above.
|
||||
|
||||
Reference in New Issue
Block a user