summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/controllers/portal.py20
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_action.js6
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js9
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js4
-rw-r--r--addons/account/static/src/xml/account_reconciliation.xml8
-rw-r--r--addons/account/static/tests/reconciliation_tests.js26
-rw-r--r--addons/account/views/account_invoice_view.xml2
-rw-r--r--addons/hr/views/hr_views.xml2
-rw-r--r--addons/l10n_fr_fec/wizard/account_fr_fec.py18
-rw-r--r--addons/mail/static/src/js/many2many_tags_email.js2
-rw-r--r--addons/mail/static/src/xml/activity.xml2
-rw-r--r--addons/mail/static/tests/chatter_tests.js2
-rw-r--r--addons/mass_mailing/static/src/less/themes/theme_default.less1
-rw-r--r--addons/mrp/models/stock_move.py4
-rw-r--r--addons/mrp/views/mrp_routing_views.xml2
-rw-r--r--addons/phone_validation/__manifest__.py1
-rw-r--r--addons/phone_validation/models/res_company.py2
-rw-r--r--addons/phone_validation/views/res_company_view.xml17
-rw-r--r--addons/product/views/product_template_views.xml2
-rw-r--r--addons/sale_stock/__manifest__.py2
-rw-r--r--addons/stock/models/procurement.py1
-rw-r--r--addons/stock/models/stock_location.py4
-rw-r--r--addons/web/controllers/main.py16
-rw-r--r--addons/web/static/src/js/core/utils.js18
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js5
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js21
-rw-r--r--addons/web/static/src/js/fields/field_registry.js1
-rw-r--r--addons/web/static/src/js/fields/field_utils.js10
-rw-r--r--addons/web/static/src/js/services/data_manager.js4
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js45
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js11
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js19
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js12
-rw-r--r--addons/web/static/src/xml/base.xml3
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js34
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js41
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js1
-rw-r--r--addons/web/static/tests/views/form_tests.js35
-rw-r--r--addons/web/static/tests/views/kanban_tests.js95
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js5
-rw-r--r--addons/web_tour/static/src/js/tour_manager.js16
-rw-r--r--addons/website/controllers/main.py10
-rw-r--r--addons/website/models/ir_http.py2
-rw-r--r--addons/website/models/website.py14
-rw-r--r--addons/website/views/website_templates.xml4
-rw-r--r--addons/website_blog/controllers/main.py4
-rw-r--r--addons/website_blog/models/website_blog.py2
-rw-r--r--addons/website_blog/views/website_blog_templates.xml4
-rw-r--r--addons/website_event_track/controllers/main.py4
-rw-r--r--addons/website_sale/static/src/js/website_sale.js2
-rw-r--r--addons/website_sale_options/static/src/js/website_sale.js3
-rw-r--r--doc/_extensions/autojsdoc/ext/directives.py1
-rw-r--r--doc/_extensions/autojsdoc/parser/jsdoc.py10
-rw-r--r--doc/cla/corporate/meta-it.md2
-rw-r--r--doc/cla/individual/rcastro-tyc.md11
-rw-r--r--doc/reference/testing.rst4
-rw-r--r--odoo/addons/base/models/ir_actions.py9
-rw-r--r--odoo/addons/base/models/ir_cron.py2
-rw-r--r--odoo/addons/base/models/ir_ui_view.py2
-rw-r--r--odoo/api.py13
-rw-r--r--odoo/fields.py3
-rw-r--r--odoo/service/db.py6
62 files changed, 462 insertions, 179 deletions
diff --git a/addons/account/controllers/portal.py b/addons/account/controllers/portal.py
index f67ae00286e..4303f972f93 100644
--- a/addons/account/controllers/portal.py
+++ b/addons/account/controllers/portal.py
@@ -9,17 +9,19 @@ from odoo.tools import consteq
class PortalAccount(CustomerPortal):
-
- def _prepare_portal_layout_values(self):
- values = super(PortalAccount, self)._prepare_portal_layout_values()
+
+ def _get_account_invoice_domain(self):
partner = request.env.user.partner_id
-
- invoice_count = request.env['account.invoice'].search_count([
+ domain = [
('type', 'in', ['out_invoice', 'out_refund']),
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('state', 'in', ['open', 'paid', 'cancel'])
- ])
+ ]
+ return domain
+ def _prepare_portal_layout_values(self):
+ values = super(PortalAccount, self)._prepare_portal_layout_values()
+ invoice_count = request.env['account.invoice'].search_count(self._get_account_invoice_domain())
values['invoice_count'] = invoice_count
return values
@@ -62,11 +64,7 @@ class PortalAccount(CustomerPortal):
partner = request.env.user.partner_id
AccountInvoice = request.env['account.invoice']
- domain = [
- ('type', 'in', ['out_invoice', 'out_refund']),
- ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
- ('state', 'in', ['open', 'paid', 'cancelled'])
- ]
+ domain = self._get_account_invoice_domain()
searchbar_sortings = {
'date': {'label': _('Invoice Date'), 'order': 'date_invoice desc'},
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js
index bfd2e48972e..79724846cbc 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_action.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js
@@ -359,7 +359,11 @@ var ManualAction = StatementAction.extend({
title: self.title,
time: Date.now()-self.time,
});
- self._openFirstLine();
+ if(!_.any(result.updated, function (handle) {
+ return self.model.getLine(handle).mode !== 'inactive';
+ })) {
+ self._openFirstLine();
+ }
});
},
});
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index b77882323a3..249e8b20862 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -501,7 +501,7 @@ var StatementModel = BasicModel.extend({
var amount = line.balance.amount;
var amount_str = _.str.sprintf('%.2f', Math.abs(amount));
amount_str = (amount > '0' ? '-' : '+') + amount_str;
- if (line.balance.amount_currency) {
+ if (line.balance.currency_id && line.balance.amount_currency) {
var amount_currency = line.balance.amount_currency;
var amount_currency_str = _.str.sprintf('%.2f', Math.abs(amount_currency));
amount_str += '|' + (amount_currency > '0' ? '-' : '+') + amount_currency_str;
@@ -821,14 +821,13 @@ var StatementModel = BasicModel.extend({
amount: total,
amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions),
currency_id: isOtherCurrencyId,
- amount_currency: isOtherCurrencyId ? amount_currency : false,
+ amount_currency: isOtherCurrencyId ? amount_currency : total,
amount_currency_str: isOtherCurrencyId ? field_utils.format.monetary(Math.abs(amount_currency), {}, {
currency_id: isOtherCurrencyId
}) : false,
account_code: self.accounts[line.st_line.open_balance_account_id],
};
- line.balance.type = (isOtherCurrencyId && amount_currency || line.balance.amount) ?
- (line.balance.amount > 0 && line.st_line.partner_id ? 0 : -1) : 1;
+ line.balance.type = line.balance.amount_currency ? (line.balance.amount_currency > 0 && line.st_line.partner_id ? 0 : -1) : 1;
});
},
/**
@@ -1295,7 +1294,7 @@ var ManualModel = StatementModel.extend({
return this._super(line).then(function () {
var props = _.reject(line.reconciliation_proposition, 'invalid');
line.balance.type = -1;
- if (!line.balance.amount && props.length) {
+ if (!line.balance.amount_currency && props.length) {
line.balance.type = 1;
} else if(_.any(props, function (prop) {return prop.amount > 0;}) &&
_.any(props, function (prop) {return prop.amount < 0;})) {
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index bea6f5873d9..91139aa0926 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -363,7 +363,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
}
- if ((state.balance.amount !== 0 || line.partial_reconcile) && props.length === 1 &&
+ if ((state.balance.amount_currency !== 0 || line.partial_reconcile) && props.length === 1 &&
line.already_paid === false &&
(
(state.st_line.amount > 0 && state.st_line.amount < props[0].amount) ||
@@ -425,7 +425,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}
});
}
- this.$('.create .add_line').toggle(!!state.balance.amount);
+ this.$('.create .add_line').toggle(!!state.balance.amount_currency);
},
//--------------------------------------------------------------------------
diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml
index 20ef80c45c8..94bef90a6f5 100644
--- a/addons/account/static/src/xml/account_reconciliation.xml
+++ b/addons/account/static/src/xml/account_reconciliation.xml
@@ -124,13 +124,13 @@
</t>
<t t-name="reconciliation.line.balance">
- <tr t-if="state.balance.amount &amp;&amp; !(state.reconciliation_proposition[0] || {}).partial_reconcile">
+ <tr t-if="state.balance.amount_currency &amp;&amp; !(state.reconciliation_proposition[0] || {}).partial_reconcile">
<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 &lt; 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_left"><t t-if="state.balance.amount &lt; 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 &gt; 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_label"><t t-if="state.balance.amount_currency &lt; 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_left"><t t-if="state.balance.amount_currency &lt; 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 &gt; 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>
</tr>
</t>
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index d292c83a8df..93e4a2f365b 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -448,7 +448,10 @@ var move_lines_for_manual_reconciliation = {
'[7,12,"",0,6]': [
{'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-08", 'date': "2017-02-08", 'total_amount_str': "$ 11,000.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0004: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 17, 'credit': 11000.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 11,000.00", 'debit': 0.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
{'account_type': "receivable", 'account_id': [7, "101200 Account Receivable"], 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-09", 'date': "2017-02-09", 'total_amount_str': "$ 1,000.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0005: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 19, 'credit': 1000.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 1,000.00", 'debit': 0.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
- {'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-09", 'date': "2017-02-09", 'total_amount_str': "$ 180.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "BILL/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 21, 'credit': 180.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 180.00", 'debit': 0.0, 'account_code': "101200", 'ref': "fddfgfdgfdgsdfg", 'already_paid': false},
+ {'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency': 100, 'amount_currency_str': "100.00 €", 'currency_id': 1, 'date_maturity': "2017-02-09", 'date': "2017-02-09", 'total_amount_str': "$ 170.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 21, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 170.00", 'debit': 170.0, 'account_code': "101200", 'ref': "INV fddfgfdgfdgsdfg", 'already_paid': false},
+ {'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency': 100, 'amount_currency_str': "100.00 €", 'currency_id': 1, 'date_maturity': "2017-02-10", 'date': "2017-02-10", 'total_amount_str': "$ 180.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "BILL/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 22, 'credit': 180.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 180.00", 'debit': 0.0, 'account_code': "101200", 'ref': "fddfgfdgfdgsdfg", 'already_paid': false},
+ {'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency': 170, 'amount_currency_str': "170.00 €", 'currency_id': 1, 'date_maturity': "2017-02-09", 'date': "2017-02-09", 'total_amount_str': "$ 100.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 23, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 100.00", 'debit': 100.0, 'account_code': "101200", 'ref': "INV fddfgfdgfdgsdfg", 'already_paid': false},
+ {'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency': 180, 'amount_currency_str': "180.00 €", 'currency_id': 1, 'date_maturity': "2017-02-10", 'date': "2017-02-10", 'total_amount_str': "$ 100.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "BILL/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 24, 'credit': 100.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 100.00", 'debit': 0.0, 'account_code': "101200", 'ref': "fddfgfdgfdgsdfg", 'already_paid': false},
],
'[284,8,"",0,6]': [
{'account_type': "receivable", 'account_id': [284, "111100 Account Payable"], 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-08", 'date': "2017-02-08", 'total_amount_str': "$ 11,000.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0004: Customer Payment", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 17, 'credit': 11000.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 11,000.00", 'debit': 0.0, 'account_code': "111100", 'ref': "", 'already_paid': false},
@@ -1117,7 +1120,7 @@ QUnit.module('account', {
testUtils.addMockEnvironment(clientAction, {
'data': this.params.data,
});
- clientAction.appendTo($('body'));
+ clientAction.appendTo($('#qunit-fixture'));
clientAction.$('button.o_automatic_reconciliation').trigger('click');
@@ -1141,7 +1144,7 @@ QUnit.module('account', {
});
QUnit.test('Reconciliation manual', function (assert) {
- assert.expect(8);
+ assert.expect(13);
var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
@@ -1171,6 +1174,23 @@ QUnit.module('account', {
assert.strictEqual(clientAction.$('.accounting_view:first .o_no_valid:visible').length, 1, "should display the skip button");
+ clientAction.$('.accounting_view:eq(1) thead td:first').trigger('click');
+ clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="21"] .cell_label').trigger('click');
+ clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="22"] .cell_label').trigger('click');
+
+ assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) tfoot tr').length, 0, "should not display the 'Write-off' line because the balance is null in Euro");
+ assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) .o_reconcile:visible').length, 1, "should display 'Reconcile' button in green");
+
+ clientAction.$('.o_reconciliation_line:eq(1) .o_reconcile:visible').trigger('click');
+
+ assert.strictEqual(clientAction.$('.o_reconciliation_line[data-mode!="inactive"]').length, 1, "should have only one line open");
+
+ clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="23"] .cell_label').trigger('click');
+ clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="24"] .cell_label').trigger('click');
+
+ assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) tfoot tr').length, 1, "should display the 'Write-off' line because the balance is not null in Euro");
+ assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) .o_validate:visible').length, 1, "should display 'Reconcile' button");
+
clientAction.destroy();
});
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 92c5e5db026..e34beadc322 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -252,7 +252,7 @@
<group>
<field name="origin" attrs="{'invisible': [('origin', '=', False)]}"/>
<field name="date_invoice" string="Bill Date"/>
- <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}"/>
+ <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/>
<field name="move_name" invisible="1"/>
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
<field name="company_currency_id" invisible="1"/>
diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml
index aa84a081a01..fc75dbe232e 100644
--- a/addons/hr/views/hr_views.xml
+++ b/addons/hr/views/hr_views.xml
@@ -297,7 +297,7 @@
<field name="domain">[('id','in',active_ids)]</field>
<field name="view_type">tree</field>
<field name="view_id" ref="view_partner_tree2"/>
- <field name="binding_model_id" ref="base.model_res_partner"/>
+ <field name="binding_model_id" ref="hr.model_hr_employee"/>
</record>
<!-- Employee Category -->
diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py
index 3f2a6471c86..ac778e6be8f 100644
--- a/addons/l10n_fr_fec/wizard/account_fr_fec.py
+++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py
@@ -43,8 +43,8 @@ class AccountFrFec(models.TransientModel):
'-' AS PieceRef,
%s AS PieceDate,
'/' AS EcritureLib,
- replace(CASE WHEN COALESCE(sum(aml.balance), 0) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Debit,
- replace(CASE WHEN COALESCE(sum(aml.balance), 0) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Credit,
+ replace(CASE WHEN COALESCE(sum(aml.balance), 0) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
+ replace(CASE WHEN COALESCE(sum(aml.balance), 0) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
'' AS EcritureLet,
'' AS DateLet,
%s AS ValidDate,
@@ -142,8 +142,8 @@ class AccountFrFec(models.TransientModel):
'-' AS PieceRef,
%s AS PieceDate,
'/' AS EcritureLib,
- replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Debit,
- replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Credit,
+ replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
+ replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
'' AS EcritureLet,
'' AS DateLet,
%s AS ValidDate,
@@ -227,8 +227,8 @@ class AccountFrFec(models.TransientModel):
'-' AS PieceRef,
%s AS PieceDate,
'/' AS EcritureLib,
- replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Debit,
- replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Credit,
+ replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
+ replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
'' AS EcritureLet,
'' AS DateLet,
%s AS ValidDate,
@@ -289,14 +289,14 @@ class AccountFrFec(models.TransientModel):
AS PieceRef,
TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
CASE WHEN aml.name IS NULL THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib,
- replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '999999999999999D99') END, '.', ',') AS Debit,
- replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '999999999999999D99') END, '.', ',') AS Credit,
+ replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit,
+ replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit,
CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
CASE WHEN aml.full_reconcile_id IS NULL THEN '' ELSE TO_CHAR(rec.create_date, 'YYYYMMDD') END AS DateLet,
TO_CHAR(am.date, 'YYYYMMDD') AS ValidDate,
CASE
WHEN aml.amount_currency IS NULL OR aml.amount_currency = 0 THEN ''
- ELSE replace(to_char(aml.amount_currency, '999999999999999D99'), '.', ',')
+ ELSE replace(to_char(aml.amount_currency, '000000000000000D99'), '.', ',')
END AS Montantdevise,
CASE WHEN aml.currency_id IS NULL THEN '' ELSE rc.name END AS Idevise
FROM
diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js
index f10d7d76701..7e1fb260a0c 100644
--- a/addons/mail/static/src/js/many2many_tags_email.js
+++ b/addons/mail/static/src/js/many2many_tags_email.js
@@ -48,7 +48,7 @@ BasicModel.include({
def = self._rpc({
model: list.model,
method: 'name_get',
- args: list.res_ids,
+ args: [list.res_ids],
context: record.getContext({fieldName: fieldName}),
});
}
diff --git a/addons/mail/static/src/xml/activity.xml b/addons/mail/static/src/xml/activity.xml
index 6b5aaabe5e8..da3e63772a6 100644
--- a/addons/mail/static/src/xml/activity.xml
+++ b/addons/mail/static/src/xml/activity.xml
@@ -17,7 +17,7 @@
<div class="o_thread_message" style="margin-bottom: 10px">
<div class="o_thread_message_sidebar">
<div class="o_avatar_stack">
- <img t-attf-src="/web/image#{activity.user_id[0] >= 0 ? ('/res.users/' + activity.user_id[0] + '/image_small') : ''}" class="o_thread_message_avatar img-circle mb8" t-att-title="activity.create_uid[1]"/>
+ <img t-attf-src="/web/image#{activity.user_id[0] >= 0 ? ('/res.users/' + activity.user_id[0] + '/image_small') : ''}" class="o_thread_message_avatar img-circle mb8" t-att-title="activity.user_id[1]"/>
<i t-att-class="'o_avatar_icon fa ' + activity.icon + ' bg-' + (activity.state == 'planned'? 'success' : (activity.state == 'today'? 'warning' : 'danger')) + '-full'"
t-att-title="activity.activity_type_id[1]"/>
</div>
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index f4043e8b174..9cf98987778 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -1273,7 +1273,7 @@ QUnit.test('fieldmany2many tags email', function (assert) {
},
mockRPC: function (route, args) {
if (route === "/web/dataset/call_kw/partner_type/name_get") {
- assert.deepEqual(args.args, nameGottenIds.shift(),
+ assert.deepEqual(args.args[0], nameGottenIds.shift(),
"partner with email should be name_get'ed");
}
else if (args.method ==='read' && args.model === 'partner_type') {
diff --git a/addons/mass_mailing/static/src/less/themes/theme_default.less b/addons/mass_mailing/static/src/less/themes/theme_default.less
index cfd1264e0c7..adfa4bba5ce 100644
--- a/addons/mass_mailing/static/src/less/themes/theme_default.less
+++ b/addons/mass_mailing/static/src/less/themes/theme_default.less
@@ -50,6 +50,7 @@
// ===== Layout =====
.o_layout {
overflow: hidden;
+ box-sizing: content-box;
width: 94%;
min-height: 100%;
background-color: @o_mm_def_color_alpha;
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 10d8a7311a6..f9436f66870 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -13,7 +13,9 @@ class StockMoveLine(models.Model):
workorder_id = fields.Many2one('mrp.workorder', 'Work Order')
production_id = fields.Many2one('mrp.production', 'Production Order')
lot_produced_id = fields.Many2one('stock.production.lot', 'Finished Lot')
- lot_produced_qty = fields.Float('Quantity Finished Product', help="Informative, not used in matching")
+ lot_produced_qty = fields.Float(
+ 'Quantity Finished Product', digits=dp.get_precision('Product Unit of Measure'),
+ help="Informative, not used in matching")
done_wo = fields.Boolean('Done for Work Order', default=True, help="Technical Field which is False when temporarily filled in in work order") # TDE FIXME: naming
done_move = fields.Boolean('Move Done', related='move_id.is_done', store=True) # TDE FIXME: naming
diff --git a/addons/mrp/views/mrp_routing_views.xml b/addons/mrp/views/mrp_routing_views.xml
index 190ff768e96..e3425b66201 100644
--- a/addons/mrp/views/mrp_routing_views.xml
+++ b/addons/mrp/views/mrp_routing_views.xml
@@ -85,7 +85,7 @@
<field name="name"/>
</group>
<group>
- <field name="location_id" groups="stock.group_adv_location"/>
+ <field name="location_id" groups="stock.group_adv_location" domain="[('usage', '!=', 'view')]"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
</group>
diff --git a/addons/phone_validation/__manifest__.py b/addons/phone_validation/__manifest__.py
index dc21476150f..2ebc99f3ead 100644
--- a/addons/phone_validation/__manifest__.py
+++ b/addons/phone_validation/__manifest__.py
@@ -16,6 +16,7 @@ formatting.
This module applies this feature to Leads and Contacts.""",
'data': [
+ 'views/res_company_view.xml'
],
'depends': ['base'],
}
diff --git a/addons/phone_validation/models/res_company.py b/addons/phone_validation/models/res_company.py
index 66d68225246..2ee2c18d7bd 100644
--- a/addons/phone_validation/models/res_company.py
+++ b/addons/phone_validation/models/res_company.py
@@ -10,7 +10,7 @@ class ResCompany(models.Model):
phone_international_format = fields.Selection([
('no_prefix', 'No prefix'),
('prefix', 'Add international prefix'),
- ],string="Local Numbers", default="no_prefix",
+ ], string="Local Numbers", default="prefix",
help="Always encode phone numbers using international format. Otherwise "
"numbers coming from the company's country are nationaly formatted. "
"International numbers are always using international format."
diff --git a/addons/phone_validation/views/res_company_view.xml b/addons/phone_validation/views/res_company_view.xml
new file mode 100644
index 00000000000..d788e5aa7fe
--- /dev/null
+++ b/addons/phone_validation/views/res_company_view.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ The purpose of this view is to move the VAT field at the top of the
+ company form. That way, the user will fill it first
+-->
+<odoo>
+ <record id="res_company_view_form_phone_validation" model="ir.ui.view">
+ <field name="name">res.company.view.form.phone_validation</field>
+ <field name="model">res.company</field>
+ <field name="inherit_id" ref="base.view_company_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='phone']" position="after">
+ <field name="phone_international_format" groups="base.group_no_one"/>
+ </xpath>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/product/views/product_template_views.xml b/addons/product/views/product_template_views.xml
index 1cd7f1a166c..ed4e2ca54af 100644
--- a/addons/product/views/product_template_views.xml
+++ b/addons/product/views/product_template_views.xml
@@ -89,7 +89,7 @@
</div>
<div name="tags"/>
<ul>
- <li>Price: <field name="lst_price" widget="monetary"></field></li>
+ <li>Price: <field name="lst_price" widget="monetary" options="{'currency_field': 'currency_id', 'field_digits': True}"></field></li>
</ul>
<div name="tags"/>
</div>
diff --git a/addons/sale_stock/__manifest__.py b/addons/sale_stock/__manifest__.py
index bc8f0d777d1..3e09974e3f5 100644
--- a/addons/sale_stock/__manifest__.py
+++ b/addons/sale_stock/__manifest__.py
@@ -25,7 +25,7 @@ You can choose flexible invoicing methods:
* *Before Delivery*: A Draft invoice is created and must be paid before delivery
""",
'website': 'https://www.odoo.com/page/warehouse',
- 'depends': ['sale', 'stock_account'],
+ 'depends': ['sale_management', 'stock_account'],
'data': [
'security/sale_stock_security.xml',
'security/ir.model.access.csv',
diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py
index cb1072efe50..38b9af72a2b 100644
--- a/addons/stock/models/procurement.py
+++ b/addons/stock/models/procurement.py
@@ -78,7 +78,6 @@ class ProcurementRule(models.Model):
data = self._get_stock_move_values(product_id, product_qty, product_uom, location_id, name, origin, values, group_id)
# Since action_confirm launch following procurement_group we should activate it.
move = self.env['stock.move'].sudo().create(data)
- move._assign_picking()
move._action_confirm()
return True
diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py
index 05e4bb2b60d..21e41caa10f 100644
--- a/addons/stock/models/stock_location.py
+++ b/addons/stock/models/stock_location.py
@@ -183,6 +183,10 @@ class Route(models.Model):
'domain': [('route_ids', 'in', self.ids)],
}
+ @api.onchange('warehouse_selectable')
+ def _onchange_warehouse_selectable(self):
+ if not self.warehouse_selectable:
+ self.warehouse_ids = [(5, 0, 0)]
class PushedFlow(models.Model):
_name = "stock.location.path"
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index a8a32347882..b0cf820005b 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -17,6 +17,7 @@ import operator
import os
import re
import sys
+import tempfile
import time
import zlib
@@ -24,6 +25,7 @@ import werkzeug
import werkzeug.utils
import werkzeug.wrappers
import werkzeug.wsgi
+from collections import OrderedDict
from werkzeug.urls import url_decode, iri_to_uri
from xml.etree import ElementTree
@@ -41,6 +43,7 @@ from odoo.http import content_disposition, dispatch_rpc, request, \
serialize_exception as _serialize_exception, Response
from odoo.exceptions import AccessError, UserError
from odoo.models import check_method_name
+from odoo.service import db
_logger = logging.getLogger(__name__)
@@ -155,10 +158,10 @@ def module_installed(environment):
# TODO The following code should move to ir.module.module.list_installed_modules()
Modules = environment['ir.module.module']
domain = [('state','=','installed'), ('name','in', loadable)]
- modules = {
- module.name: module.dependencies_id.mapped('name')
+ modules = OrderedDict(
+ (module.name, module.dependencies_id.mapped('name'))
for module in Modules.search(domain)
- }
+ )
sorted_modules = topological_sort(modules)
return sorted_modules
@@ -740,12 +743,15 @@ class Database(http.Controller):
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
def restore(self, master_pwd, backup_file, name, copy=False):
try:
- data = base64.b64encode(backup_file.read())
- dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)])
+ with tempfile.NamedTemporaryFile(delete=False) as data_file:
+ backup_file.save(data_file)
+ db.restore_db(name, data_file.name, str2bool(copy))
return http.local_redirect('/web/database/manager')
except Exception as e:
error = "Database restore error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
+ finally:
+ os.unlink(data_file.name)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
def change_password(self, master_pwd, master_pwd_new):
diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js
index 99534708a94..1bdd9e70a69 100644
--- a/addons/web/static/src/js/core/utils.js
+++ b/addons/web/static/src/js/core/utils.js
@@ -24,6 +24,22 @@ var utils = {
}
},
/**
+ * Check if the value is a bin_size or not.
+ * If not, compute an approximate size out of the base64 encoded string.
+ *
+ * @param {string} value original format
+ * @return {string} bin_size (human-readable)
+ */
+ binaryToBinsize: function (value) {
+ if (!this.is_bin_size(value)) {
+ // Computing approximate size out of base64 encoded string
+ // http://en.wikipedia.org/wiki/Base64#MIME
+ return this.human_size(value.length / 1.37);
+ }
+ // already bin_size
+ return value;
+ },
+ /**
* Confines a value inside an interval
*
* @param {number} [val] the value to confine
@@ -132,7 +148,7 @@ var utils = {
size /= 1024;
++i;
}
- return size.toFixed(2) + ' ' + units[i];
+ return size.toFixed(2) + ' ' + units[i].trim();
},
/**
* Insert "thousands" separators in the provided number (which is actually
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 2534f8ae90a..8bb9afca065 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -45,6 +45,11 @@ var AbstractField = Widget.extend({
},
/**
+ * fields can extend the context, e.g. binary fields add {bin_size: true}
+ */
+ context: null,
+
+ /**
* An object representing fields to be fetched by the model eventhough not present in the view
* This object contains "field name" as key and an object as value.
* That value object must contain the key "type"
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index d0662fe7cef..f3e66cee421 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1094,6 +1094,7 @@ var AbstractFieldBinary = AbstractField.extend({
},
'click .o_clear_file_button': 'on_clear',
}),
+ context: { bin_size: true },
init: function (parent, name, record) {
this._super.apply(this, arguments);
this.fields = record.fields;
@@ -1312,25 +1313,6 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
},
});
-var FieldBinaryDownloadLink = AbstractFieldBinary.extend({
- template: 'FieldBinaryDownloadLink',
- events: _.extend({}, AbstractFieldBinary.prototype.events, {
- 'click': function (ev) {
- ev.stopPropagation();
- },
- }),
- supportedFieldTypes: ['binary'],
- init: function () {
- this._super.apply(this, arguments);
- this.filename = _t("Binary File");
- if (this.nodeOptions.filename && this.recordData[this.nodeOptions.filename]) {
- this.filename = this.recordData[this.nodeOptions.filename];
- }
- this.url = "data:application/octet-stream;base64," + this.value;
- this.text = _t("Download");
- },
-});
-
var PriorityWidget = AbstractField.extend({
// the current implementation of this widget makes it
// only usable for fields of type selection
@@ -2561,7 +2543,6 @@ return {
TranslatableFieldMixin: TranslatableFieldMixin,
DebouncedField: DebouncedField,
FieldEmail: FieldEmail,
- FieldBinaryDownloadLink: FieldBinaryDownloadLink,
FieldBinaryFile: FieldBinaryFile,
FieldBinaryImage: FieldBinaryImage,
FieldBoolean: FieldBoolean,
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 85b4fc1187f..6692663d5f9 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -33,7 +33,6 @@ registry
.add('email', basic_fields.FieldEmail)
.add('phone', basic_fields.FieldPhone)
.add('url', basic_fields.UrlWidget)
- .add('download_link', basic_fields.FieldBinaryDownloadLink)
.add('image', basic_fields.FieldBinaryImage)
.add('binary', basic_fields.FieldBinaryFile)
.add('monetary', basic_fields.FieldMonetary)
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 812a7f380f7..5ac180b1de1 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -28,17 +28,17 @@ var _t = core._t;
//------------------------------------------------------------------------------
/**
+ * Convert binary to bin_size
*
- * @param {string} value base64 representation of the binary
+ * @param {string} value base64 representation of the binary (might be already a bin_size!)
* @param {Object} [field]
* a description of the field (note: this parameter is ignored)
* @param {Object} [options] additional options (note: this parameter is ignored)
+ *
+ * @returns {string} bin_size (which is human-readable)
*/
function formatBinary(value, field, options) {
- if (!value) {
- return '';
- }
- return utils.human_size(value.length / 1.37); // base64 estimated size: http://en.wikipedia.org/wiki/Base64#MIME
+ return utils.binaryToBinsize(value);
}
/**
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 72ec816efdd..948d3a1c73a 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -387,6 +387,10 @@ return core.Class.extend({
attrs.fieldDependencies = attrs.Widget.prototype.fieldDependencies;
}
+ if (attrs.Widget.prototype.context) {
+ attrs.context = attrs.Widget.prototype.context;
+ }
+
return attrs;
},
/**
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 3621b7c1227..a6979776c99 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -33,7 +33,7 @@ odoo.define('web.BasicModel', function (require) {
* isOpen: {boolean},
* loadMoreOffset: {integer},
* limit: {integer},
- * model: {string,
+ * model: {string},
* offset: {integer},
* openGroupByDefault: {boolean},
* orderedBy: {Object[]},
@@ -1854,7 +1854,7 @@ var BasicModel = AbstractModel.extend({
model: record.model,
method: 'read',
args: [[record.res_id], fieldNames],
- context: _.extend({}, record.getContext(), {bin_size: true}),
+ context: record.getContext(),
})
.then(function (result) {
if (result.length === 0) {
@@ -2374,7 +2374,7 @@ var BasicModel = AbstractModel.extend({
});
record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) {
- var def = self._readUngroupedList(list).then(function () {
+ var def = self._readUngroupedList(list).then(function (list) {
return $.when(
self._fetchX2ManysBatched(list),
self._fetchReferencesBatched(list)
@@ -3040,10 +3040,33 @@ var BasicModel = AbstractModel.extend({
* the resource in the localData object.
*
* @param {Object} params
+ * @param {Object} [params.aggregateValues={}]
+ * @param {Object} [params.context={}] context of the action
+ * @param {integer} [params.count=0] number of record being manipulated
+ * @param {Object|Object[]} [params.data={}|[]] data of the record
+ * @param {*[]} [params.domain=[]]
+ * @param {Object} params.fields contains the description of each field
* @param {Object} [params.fieldsInfo={}] contains the fieldInfo of each field
- * @param {Array} [params.fieldNames] the name of fields to load, the list
+ * @param {Object[]} [params.fieldNames] the name of fields to load, the list
* of all fields by default
- * @param {Object} params.fields contains the description of each field
+ * @param {string[]} [params.groupedBy=[]]
+ * @param {boolean} [params.isOpen]
+ * @param {integer} params.limit max number of records shown on screen (pager size)
+ * @param {string} params.modelName
+ * @param {integer} [params.offset]
+ * @param {boolean} [params.openGroupByDefault]
+ * @param {Object[]} [params.orderedBy=[]]
+ * @param {integer[]} [params.orderedResIDs]
+ * @param {string} [params.parentID] model name ID of the parent model
+ * @param {Object} [params.rawContext]
+ * @param {[type]} [params.ref]
+ * @param {string} [params.relationField]
+ * @param {integer|null} [params.res_id] actual id of record in the server
+ * @param {integer[]} [params.res_ids] context in which the data point is used, from a list of res_id
+ * @param {boolean} [params.static=false]
+ * @param {string} [params.type='record'|'list']
+ * @param {[type]} [params.value]
+ * @param {string} [params.viewType] the type of the view, e.g. 'list' or 'form'
* @returns {Object} the resource created
*/
_makeDataPoint: function (params) {
@@ -3069,13 +3092,23 @@ var BasicModel = AbstractModel.extend({
id: {type: 'integer'},
}, params.fields);
+ // datapoint context is extended by fields widgets
+ var context = _.extend({}, params.context);
+ if (params.fieldsInfo && params.viewType) {
+ _.each(params.fieldsInfo[params.viewType], function (field) {
+ if (field.context) {
+ _.extend(context, field.context);
+ }
+ });
+ }
+
var dataPoint = {
_cache: type === 'list' ? {} : undefined,
_changes: null,
_domains: {},
_rawChanges: {},
aggregateValues: params.aggregateValues || {},
- context: params.context || {},
+ context: context,
count: params.count || res_ids.length,
data: data,
domain: params.domain || [],
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index 9409a51b7e0..779121de3e9 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -693,6 +693,7 @@ var FormRenderer = BasicRenderer.extend({
var self = this;
var $headers = $('<ul class="nav nav-tabs">');
var $pages = $('<div class="tab-content nav nav-tabs">');
+ var autofocusTab = -1;
// renderedTabs is used to aggregate the generated $headers and $pages
// alongside their node, so that their modifiers can be registered once
// all tabs have been rendered, to ensure that the first visible tab
@@ -701,9 +702,8 @@ var FormRenderer = BasicRenderer.extend({
var pageID = _.uniqueId('notebook_page_');
var $header = self._renderTabHeader(child, pageID);
var $page = self._renderTabPage(child, pageID);
- if (index === 0) {
- $header.addClass('active');
- $page.addClass('active');
+ if (autofocusTab === -1 && child.attrs.autofocus === 'autofocus') {
+ autofocusTab = index;
}
self._handleAttributes($header, child);
$headers.append($header);
@@ -714,6 +714,11 @@ var FormRenderer = BasicRenderer.extend({
node: child,
};
});
+ if (renderedTabs.length) {
+ var tabToFocus = renderedTabs[Math.max(0, autofocusTab)];
+ tabToFocus.$header.addClass('active');
+ tabToFocus.$page.addClass('active');
+ }
// register the modifiers for each tab
_.each(renderedTabs, function (tab) {
self._registerModifiers(tab.node, self.state, tab.$header, {
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 2f6f566649e..9e97ec40686 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -239,15 +239,16 @@ var KanbanController = BasicController.extend({
* @private
*/
_onButtonNew: function () {
- var data = this.model.get(this.handle, {raw: true});
- if (data.groupedBy.length > 0 && data.count > 0 && this.on_create === 'quick_create') {
+ var state = this.model.get(this.handle, {raw: true});
+ var hasColumns = state.groupedBy.length > 0 && state.data.length > 0;
+ if (hasColumns && this.on_create === 'quick_create') {
// Activate the quick create in the first column
this.renderer.addQuickCreate();
} else if (this.on_create && this.on_create !== 'quick_create') {
// Execute the given action
this.do_action(this.on_create, {
on_close: this.reload.bind(this),
- additional_context: data.context,
+ additional_context: state.context,
});
} else {
// Open the form view
@@ -355,11 +356,13 @@ var KanbanController = BasicController.extend({
self._updateEnv();
var columnState = self.model.getColumn(db_id);
- return self.renderer.updateColumn(columnState.id, columnState).then(function () {
- if (event.data.openRecord) {
- self.trigger_up('open_record', {id: db_id, mode: 'edit'});
- }
- });
+ return self.renderer
+ .updateColumn(columnState.id, columnState, {openQuickCreate: true})
+ .then(function () {
+ if (event.data.openRecord) {
+ self.trigger_up('open_record', {id: db_id, mode: 'edit'});
+ }
+ });
});
}
},
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index 0f91fb2ae14..e4e37095afa 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -141,15 +141,23 @@ var KanbanRenderer = BasicRenderer.extend({
*
* @param {string} localID the column id
* @param {Object} columnState
+ * @param {Object} [options]
+ * @param {boolean} [options.openQuickCreate] if true, directly opens the
+ * QuickCreate widget in the updated column
*
* @returns {Deferred}
*/
- updateColumn: function (localID, columnState) {
+ updateColumn: function (localID, columnState, options) {
var newColumn = new KanbanColumn(this, columnState, this.columnOptions, this.recordOptions);
var index = _.findIndex(this.widgets, {db_id: localID});
var column = this.widgets[index];
this.widgets[index] = newColumn;
- return newColumn.insertAfter(column.$el).then(column.destroy.bind(column));
+ return newColumn.insertAfter(column.$el).then(function () {
+ if (options && options.openQuickCreate) {
+ newColumn.addQuickCreate();
+ }
+ column.destroy();
+ });
},
/**
* Updates a given record with its new state.
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 8ee6f643c51..509c03fdf4b 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -906,9 +906,6 @@
</t>
</div>
</t>
-<t t-name="FieldBinaryDownloadLink">
- <a t-att-download="widget.filename" t-att-href="widget.url"><t t-esc="widget.text"/></a>
-</t>
<t t-name="HiddenInputFile">
<div t-attf-class="o_hidden_input_file #{fileupload_class or ''}" t-att-style="fileupload_style">
<form class="o_form_binary_form" t-att-target="fileupload_id"
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 97601c8e6d0..4fd9d6ee84e 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -1518,40 +1518,6 @@ QUnit.module('basic_fields', {
session.get_file = oldGetFile;
});
- QUnit.test('download_link widget in list view', function (assert) {
- assert.expect(5);
-
- this.data.partner.fields.filename = { string: "Filename", type: "char" };
- this.data.partner.records[0].filename = "document.txt";
-
- var form = createView({
- View: ListView,
- model: 'partner',
- data: this.data,
- arch: '<tree string="Partners">' +
- '<field name="document" widget="download_link" options="{\'filename\': \'filename\'}"/>' +
- '<field name="filename"/>' +
- '</tree>',
- res_id: 1,
- });
-
- assert.ok(form.$('.o_data_cell').first().hasClass("o_download_link_cell"),
- "this should be a download link cell");
- assert.ok(form.$('.o_data_cell').first().find('a').eq(0),
- "there should be a link in the cell");
- assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).attr('download'),
- "document.txt",
- "it should download a file named 'document.txt'");
- assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).attr('href').trim(),
- "data:application/octet-stream;base64,coucou==",
- "it should have the appropriate link of a binary");
- assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).text(),
- "Download",
- "the text of the link should be 'Download'");
-
- form.destroy();
- });
-
QUnit.test('text field rendering in list view', function (assert) {
assert.expect(1);
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index c149b5c3dba..ef5139511fe 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -6916,7 +6916,6 @@ QUnit.module('relational_fields', {
if (args.method === 'read' && args.model === 'partner') {
assert.deepEqual(args.kwargs.context, {
active_field: 2,
- bin_size: true,
someKey: 'some value',
}, "sent context should be correct");
}
@@ -7600,6 +7599,46 @@ QUnit.module('relational_fields', {
form.destroy();
});
+
+ QUnit.test('one2many with multiple pages and sequence field', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [3, 2, 1];
+ this.data.partner.onchanges.turtles = function () {};
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="2">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" invisible="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({value: { turtles: [
+ [5],
+ [1, 1, {turtle_foo: "from onchange", partner_ids: [[5]]}],
+ ]}});
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ form.$('span[name="delete"]').first().click();
+ assert.strictEqual(form.$('.o_data_row').text(), 'from onchange',
+ 'onchange has been properly applied');
+ form.destroy();
+ });
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 492dfd4224c..72061f326e0 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -74,7 +74,6 @@ QUnit.module('Views', {
mockRPC: function (route, args) {
assert.deepEqual(args.kwargs.context, {
active_field: 2,
- bin_size: true,
someKey: 'some value',
}, "should have sent the correct context");
return this._super.apply(this, arguments);
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 1f5e57b2202..9f2a9b68698 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -199,7 +199,7 @@ QUnit.module('Views', {
});
QUnit.test('only necessary fields are fetched with correct context', function (assert) {
- assert.expect(2);
+ assert.expect(1);
var form = createView({
View: FormView,
@@ -214,8 +214,6 @@ QUnit.module('Views', {
// field, not sure why. Maybe this test should be modified.
assert.deepEqual(args.args[1], ["foo", "display_name"],
"should only fetch requested fields");
- assert.deepEqual(args.kwargs.context, {bin_size: true},
- "bin_size should always be in the context");
return this._super(route, args);
}
});
@@ -483,6 +481,37 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('autofocus on second notebook page', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="product_id"/>' +
+ '<notebook>' +
+ '<page string="Choucroute">' +
+ '<field name="foo"/>' +
+ '</page>' +
+ '<page string="Cassoulet" autofocus="autofocus">' +
+ '<field name="bar"/>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.notOk(form.$('.o_notebook .nav li:first()').hasClass('active'),
+ 'first tab should not active');
+ assert.ok(form.$('.o_notebook .nav li:nth(1)').hasClass('active'),
+ 'second tab should be active');
+
+ form.destroy();
+ });
+
QUnit.test('invisible attrs on group are re-evaluated on field change', function (assert) {
assert.expect(2);
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 9c8361e63ae..9e6d6e9e30f 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -493,6 +493,58 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('quick create several records in a row', function (assert) {
+ assert.expect(6);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column should contain one record");
+
+ // click to add an element, fill the input and press ENTER
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "the quick create should be open");
+
+ kanban.$('.o_kanban_quick_create input')
+ .val('new partner 1')
+ .trigger($.Event('keypress', {
+ which: $.ui.keyCode.ENTER,
+ keyCode: $.ui.keyCode.ENTER,
+ }));
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "first column should now contain two records");
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "the quick create should still be open");
+
+ // create a second element in a row
+ kanban.$('.o_kanban_quick_create input')
+ .val('new partner 2')
+ .trigger($.Event('keypress', {
+ which: $.ui.keyCode.ENTER,
+ keyCode: $.ui.keyCode.ENTER,
+ }));
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3,
+ "first column should now contain three records");
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "the quick create should still be open");
+
+ kanban.destroy();
+ });
+
QUnit.test('quick create fail in grouped', function (assert) {
assert.expect(7);
@@ -524,7 +576,6 @@ QUnit.module('Views', {
return this._super.apply(this, arguments);
},
});
- kanban.renderButtons();
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
"there should be 2 records in first column");
@@ -556,6 +607,48 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('quick create record in empty grouped kanban', function (assert) {
+ assert.expect(3);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ // override read_group to return empty groups, as this is
+ // the case for several models (e.g. project.task grouped
+ // by stage_id)
+ var result = [
+ {__domain: [['product_id', '=', 3]], product_id_count: 0},
+ {__domain: [['product_id', '=', 5]], product_id_count: 0},
+ ];
+ return $.when(result);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 2,
+ "there should be 2 columns");
+ assert.strictEqual(kanban.$('.o_kanban_record').length, 0,
+ "both columns should be empty");
+
+ kanban.$buttons.find('.o-kanban-button-new').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_quick_create').length, 1,
+ "should have opened the quick create in the first column");
+
+ kanban.destroy();
+ });
+
QUnit.test('many2many_tags in kanban views', function (assert) {
assert.expect(12);
diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js
index 7f82df2cb28..fb1ce3e12b4 100644
--- a/addons/web_editor/static/src/js/editor/rte.summernote.js
+++ b/addons/web_editor/static/src/js/editor/rte.summernote.js
@@ -388,9 +388,12 @@ eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
if (r.sc.tagName && r.sc.childNodes.length) {
r.sc = r.sc.childNodes[r.so];
}
+ var media = $(r.sc).parents().addBack().filter(function (i, el) {
+ return dom.isImg(el);
+ })[0];
core.bus.trigger('media_dialog_demand', {
$editable: $editable,
- media: dom.isImg(r.sc) ? r.sc : null,
+ media: media,
});
return new $.Deferred().reject();
};
diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js
index 4b7dde7696b..1b076783fbd 100644
--- a/addons/web_tour/static/src/js/tour_manager.js
+++ b/addons/web_tour/static/src/js/tour_manager.js
@@ -172,18 +172,18 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
/**
* Registers a tour described by the following arguments *in order*
*
- * @param [string] name - tour's name
- * @param [Object] - options (optional), available options are:
- * @param [boolean] - true if this is only for tests
- * @param [boolean]
+ * @param {string} name - tour's name
+ * @param {Object} [options] - options (optional), available options are:
+ * @param {boolean} [options.test=false] - true if this is only for tests
+ * @param {boolean} [options.skip_enabled=false]
* true to add a link in its tips to consume the whole tour
- * @param [string]
+ * @param {string} [options.url]
* the url to load when manually running the tour
- * @param [boolean]
+ * @param {boolean} [options.rainbowMan=true]
* whether or not the rainbowman must be shown at the end of the tour
- * @param [Deferred]
+ * @param {Deferred} [options.wait_for]
* indicates when the tour can be started
- * @param [Object] steps - steps' descriptions, each step being an object
+ * @param {Object[]} steps - steps' descriptions, each step being an object
* containing a tip description
*/
register: function() {
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index c8c80c433c1..40d3c32fc87 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -66,17 +66,17 @@ class Website(Home):
@http.route('/', type='http', auth="public", website=True)
def index(self, **kw):
homepage = request.website.homepage_id
- if homepage and homepage.url != '/':
+ if homepage and (homepage.sudo().is_visible or request.env.user.has_group('base.group_user')) and homepage.url != '/':
return request.env['ir.http'].reroute(homepage.url)
website_page = request.env['ir.http']._serve_page()
if website_page:
return website_page
else:
- top_menu = request.website.sudo().menu_id
- first_menu = top_menu and top_menu.child_id and top_menu.child_id[0]
- if first_menu and first_menu.url != '/' and (not (first_menu.url.startswith(('/?', '/#')))):
- return request.redirect(first_menu.url)
+ top_menu = request.website.menu_id
+ first_menu = top_menu and top_menu.child_id and top_menu.child_id.filtered(lambda menu: menu.is_visible)
+ if first_menu and first_menu[0].url != '/' and (not (first_menu[0].url.startswith(('/?', '/#')))):
+ return request.redirect(first_menu[0].url)
raise request.not_found()
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 65c514487a2..84813d7eca5 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -62,7 +62,7 @@ class Http(models.AbstractModel):
if not request.session.uid:
env = api.Environment(request.cr, SUPERUSER_ID, request.context)
website = env['website'].get_current_website()
- if website:
+ if website and website.user_id:
request.uid = website.user_id.id
if not request.uid:
super(Http, cls)._auth_method_public()
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index a4848313a9c..d6deb298eb2 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -63,7 +63,7 @@ class Website(models.Model):
google_management_client_id = fields.Char('Google Client ID')
google_management_client_secret = fields.Char('Google Client Secret')
- user_id = fields.Many2one('res.users', string='Public User', default=lambda self: self.env.ref('base.public_user').id)
+ user_id = fields.Many2one('res.users', string='Public User', required=True, default=lambda self: self.env.ref('base.public_user').id)
cdn_activated = fields.Boolean('Activate CDN for assets')
cdn_url = fields.Char('CDN Base URL', default='')
cdn_filters = fields.Text('CDN Filters', default=lambda s: '\n'.join(DEFAULT_CDN_FILTERS), help="URL matching those filters will be rewritten using the CDN Base URL")
@@ -791,7 +791,7 @@ class Menu(models.Model):
_parent_store = True
_parent_order = 'sequence'
- _order = "sequence"
+ _order = "sequence, id"
def _default_sequence(self):
menu = self.search([], limit=1, order="sequence DESC")
@@ -807,6 +807,14 @@ class Menu(models.Model):
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
parent_left = fields.Integer('Parent Left', index=True)
parent_right = fields.Integer('Parent Rigth', index=True)
+ is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible')
+
+ @api.one
+ def _compute_visible(self):
+ visible = True
+ if self.page_id and not self.page_id.sudo().is_visible and not self.user_has_groups('base.group_user'):
+ visible = False
+ self.is_visible = visible
@api.model
def clean_url(self):
@@ -815,7 +823,7 @@ class Menu(models.Model):
url = self.page_id.sudo().url
else:
url = self.url
- if not self.url.startswith('/'):
+ if url and not self.url.startswith('/'):
if '@' in self.url:
if not self.url.startswith('mailto'):
url = 'mailto:%s' % self.url
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index c5241720c3f..abae2c3fdfc 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -67,14 +67,14 @@
<!-- Layout -->
<template id="submenu" name="Submenu">
- <li t-if="not submenu.child_id" t-att-class="
+ <li t-if="submenu.is_visible and not submenu.child_id.filtered(lambda menu: menu.is_visible)" t-att-class="
'active' if submenu.clean_url() and unslug_url(request.httprequest.path) == unslug_url(submenu.clean_url()) else None
">
<a t-att-href="submenu.clean_url()" t-ignore="true" t-att-target="'_blank' if submenu.new_window else None">
<span t-field="submenu.name"/>
</a>
</li>
- <li t-if="submenu.child_id" t-attf-class="dropdown #{
+ <li t-if="submenu.is_visible and submenu.child_id.filtered(lambda menu: menu.is_visible)" t-attf-class="dropdown #{
(submenu.clean_url() and submenu.clean_url() != '/' and any([request.httprequest.path == child.url for child in submenu.child_id if child.url]) or
(submenu.clean_url() and request.httprequest.path == submenu.clean_url())) and 'active'
}">
diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py
index 6ebb67c8782..0fc0871a0d9 100644
--- a/addons/website_blog/controllers/main.py
+++ b/addons/website_blog/controllers/main.py
@@ -173,7 +173,9 @@ class WebsiteBlog(http.Controller):
v = {}
v['blog'] = blog
v['base_url'] = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
- v['posts'] = request.env['blog.post'].search([('blog_id','=', blog.id)], limit=min(int(limit), 50))
+ v['posts'] = request.env['blog.post'].search([('blog_id','=', blog.id)],
+ limit=min(int(limit), 50),
+ order="post_date DESC")
v['html2plaintext'] = html2plaintext
r = request.render("website_blog.blog_feed", v, headers=[('Content-Type', 'application/atom+xml')])
return r
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index d215ba39ddd..e3aa0f60f90 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -151,7 +151,7 @@ class BlogPost(models.Model):
write_date = fields.Datetime('Last Modified on', index=True, readonly=True)
write_uid = fields.Many2one('res.users', 'Last Contributor', index=True, readonly=True)
author_avatar = fields.Binary(related='author_id.image_small', string="Avatar")
- visits = fields.Integer('No of Views')
+ visits = fields.Integer('No of Views', copy=False)
ranking = fields.Float(compute='_compute_ranking', string='Ranking')
@api.multi
diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml
index 33c84ad6367..2e5d467c3a0 100644
--- a/addons/website_blog/views/website_blog_templates.xml
+++ b/addons/website_blog/views/website_blog_templates.xml
@@ -548,14 +548,14 @@
<title t-esc="blog.name"/>
<link t-att-href="'%s/blog/%s' % (base_url ,blog.id)"/>
<id t-esc="'%s/blog/%s' % (base_url, blog.id)"/>
- <updated t-esc="(posts[0] if posts else blog).write_date.replace(' ', 'T') + 'Z'"/>
+ <updated t-esc="(posts[0] if posts else blog).post_date.replace(' ', 'T') + 'Z'"/>
<entry t-foreach="posts" t-as="post">
<title t-esc="post.name"/>
<link t-att-href="'%s%s' % (base_url, post.website_url)"/>
<id t-esc="'%s%s' % (base_url, post.website_url)"/>
<author><name t-esc="post.sudo().author_id.name"/></author>
<summary t-esc="html2plaintext(post.teaser)"/>
- <updated t-esc="post.write_date.replace(' ', 'T') + 'Z'"/>
+ <updated t-esc="post.post_date.replace(' ', 'T') + 'Z'"/>
</entry>
</feed>
</template>
diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py
index f2ae337deda..819176096b2 100644
--- a/addons/website_event_track/controllers/main.py
+++ b/addons/website_event_track/controllers/main.py
@@ -67,11 +67,13 @@ class WebsiteEventTrackController(http.Controller):
@http.route(['''/event/<model("event.event"):event>/agenda'''], type='http', auth="public", website=True, sitemap=False)
def event_agenda(self, event, tag=None, **post):
+ local_tz = pytz.timezone(event.date_tz or 'UTC')
days_tracks = collections.defaultdict(lambda: [])
for track in event.track_ids.sorted(lambda track: (track.date or '', bool(track.location_id))):
if not track.date:
continue
- days_tracks[track.date[:10]].append(track)
+ date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz)
+ days_tracks[str(date)[:10]].append(track)
days = {}
tracks_by_days = {}
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 88bbb95648c..d54978e290c 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -215,7 +215,7 @@ odoo.define('website_sale.website_sale', function (require) {
return false;
});
- $('.oe_website_sale .a-submit, #comment .a-submit').off('click').on('click', function (event) {
+ $('.oe_website_sale, #comment').off('click', '.a-submit').on('click', '.a-submit', function (event) {
if (!event.isDefaultPrevented() && !$(this).is(".disabled")) {
event.preventDefault();
$(this).closest('form').submit();
diff --git a/addons/website_sale_options/static/src/js/website_sale.js b/addons/website_sale_options/static/src/js/website_sale.js
index b766a71200e..da7025d65bb 100644
--- a/addons/website_sale_options/static/src/js/website_sale.js
+++ b/addons/website_sale_options/static/src/js/website_sale.js
@@ -35,7 +35,7 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
$(this).remove();
});
- $modal.on('click', '.a-submit', function () {
+ $modal.on('click', '.a-submit', function (ev) {
var $a = $(this);
$form.ajaxSubmit({
url: '/shop/cart/update_option',
@@ -50,6 +50,7 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
}
});
$modal.modal('hide');
+ ev.preventDefault();
});
$modal.on('click', '.css_attribute_color input', function (event) {
diff --git a/doc/_extensions/autojsdoc/ext/directives.py b/doc/_extensions/autojsdoc/ext/directives.py
index 98b3f9e8322..d127e24725b 100644
--- a/doc/_extensions/autojsdoc/ext/directives.py
+++ b/doc/_extensions/autojsdoc/ext/directives.py
@@ -565,6 +565,7 @@ def extract_subtypes(parent_name, doc):
'name': typename,
'doc': param.doc,
'_members': [
+ # TODO: add default value
(sub.name, jsdoc.PropertyDoc(dict(sub.to_dict(), sourcemodule=doc['sourcemodule'])))
for sub in subs
],
diff --git a/doc/_extensions/autojsdoc/parser/jsdoc.py b/doc/_extensions/autojsdoc/parser/jsdoc.py
index a34a2aa9b83..55ec998c94f 100644
--- a/doc/_extensions/autojsdoc/parser/jsdoc.py
+++ b/doc/_extensions/autojsdoc/parser/jsdoc.py
@@ -122,8 +122,14 @@ class FunctionDoc(CommentDoc):
@property
def params(self):
tag_texts = self.get_as_list('param')
- if self.get('guessed_params') is None:
- return [ParamDoc(text) for text in tag_texts]
+ # turns out guessed_params is *almost* (?) always set to a list,
+ # if empty list of guessed params fall back to @params
+ if not self['guessed_params']:
+ # only get "primary" params (no "." in name)
+ return [
+ p for p in map(ParamDoc, tag_texts)
+ if '.' not in p.name
+ ]
else:
param_dict = {}
for text in tag_texts:
diff --git a/doc/cla/corporate/meta-it.md b/doc/cla/corporate/meta-it.md
index 01b33f35c8b..f321433af3f 100644
--- a/doc/cla/corporate/meta-it.md
+++ b/doc/cla/corporate/meta-it.md
@@ -12,4 +12,4 @@ Robin Lucbenet robin@meta-it.fr https://github.com/n1b0r
List of contributors:
-Guillaume Masson guillaume.masson@meta-it.fr
+Guillaume Masson guillaume.masson@meta-it.fr https://github.com/metaminux
diff --git a/doc/cla/individual/rcastro-tyc.md b/doc/cla/individual/rcastro-tyc.md
new file mode 100644
index 00000000000..ea6de8af17b
--- /dev/null
+++ b/doc/cla/individual/rcastro-tyc.md
@@ -0,0 +1,11 @@
+Costa Rica, 2018-01-11
+
+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,
+
+Randall Castro randall_castro@me.com https://github.com/rcastro-tyc
diff --git a/doc/reference/testing.rst b/doc/reference/testing.rst
index a54440293cc..ada14166202 100644
--- a/doc/reference/testing.rst
+++ b/doc/reference/testing.rst
@@ -75,6 +75,10 @@ in each method::
# other tests...
+.. note::
+
+ Test methods must start with ``test_``
+
.. autoclass:: odoo.tests.common.Form
:members:
diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py
index 776a098e453..d857c14593b 100644
--- a/odoo/addons/base/models/ir_actions.py
+++ b/odoo/addons/base/models/ir_actions.py
@@ -442,7 +442,12 @@ class IrActionsServer(models.Model):
for exp in action.fields_lines:
res[exp.col1.name] = exp.eval_value(eval_context=eval_context)[exp.id]
- self.env[action.model_id.model].browse(self._context.get('active_id')).write(res)
+ if self._context.get('onchange_self'):
+ record_cached = self._context['onchange_self']
+ for field, new_value in res.items():
+ record_cached[field] = new_value
+ else:
+ self.env[action.model_id.model].browse(self._context.get('active_id')).write(res)
@api.model
def run_action_object_create(self, action, eval_context=None):
@@ -537,6 +542,8 @@ class IrActionsServer(models.Model):
elif hasattr(self, 'run_action_%s' % action.state):
active_id = self._context.get('active_id')
+ if not active_id and self._context.get('onchange_self'):
+ active_id = self._context['onchange_self']._origin.id
active_ids = self._context.get('active_ids', [active_id] if active_id else [])
for active_id in active_ids:
# run context dedicated to a particular active_id
diff --git a/odoo/addons/base/models/ir_cron.py b/odoo/addons/base/models/ir_cron.py
index 375d9a966da..58dc0f06fb7 100644
--- a/odoo/addons/base/models/ir_cron.py
+++ b/odoo/addons/base/models/ir_cron.py
@@ -215,7 +215,7 @@ class ir_cron(models.Model):
_logger.debug("Job `%s` already executed by another process/thread. skipping it", job['cron_name'])
continue
# Got the lock on the job row, run its code
- _logger.debug('Starting job `%s`.', job['cron_name'])
+ _logger.info('Starting job `%s`.', job['cron_name'])
job_cr = db.cursor()
try:
registry = odoo.registry(db_name)
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index 70094f476d1..c11d1a2e340 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -247,7 +247,7 @@ actual arch.
if fullpath:
arch_fs = get_view_arch_from_file(fullpath, view.xml_id)
# replace %(xml_id)s, %(xml_id)d, %%(xml_id)s, %%(xml_id)d by the res_id
- arch_fs = arch_fs and resolve_external_ids(arch_fs, view.xml_id)
+ arch_fs = arch_fs and resolve_external_ids(arch_fs, view.xml_id).replace('%%', '%')
else:
_logger.warning("View %s: Full path [%s] cannot be found.", view.xml_id, view.arch_fs)
arch_fs = False
diff --git a/odoo/api.py b/odoo/api.py
index 143f51bc31c..e9657b798c0 100644
--- a/odoo/api.py
+++ b/odoo/api.py
@@ -1004,11 +1004,14 @@ class Cache(object):
def get_records(self, model, field):
""" Return the records of ``model`` that have a value for ``field``. """
- browse = model.browse
- ids = [record_id
- for record_id, field_record_cache in self._data[field].items()
- if field.cache_key(browse(record_id)) in field_record_cache]
- return browse(ids)
+ key = field.cache_key(model)
+ # optimization: do not field.cache_key(record) for each record in cache
+ ids = [
+ record_id
+ for record_id, field_record_cache in self._data[field].items()
+ if key in field_record_cache
+ ]
+ return model.browse(ids)
def invalidate(self, spec=None):
""" Invalidate the cache, partially or totally depending on ``spec``. """
diff --git a/odoo/fields.py b/odoo/fields.py
index b6b13e97c8c..519fd5c124e 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -763,6 +763,9 @@ class Field(MetaField('DummyField', (object,), {})):
cache, the full cache key being ``(self, record.id, key)``.
"""
env = record.env
+ # IMPORTANT: odoo.api.Cache.get_records() depends on the fact that the
+ # result does not depend on record.id. If you ever make the following
+ # dependent on record.id, don't forget to fix the other method!
return env if self.context_dependent else (env.cr, env.uid)
def null(self, record):
diff --git a/odoo/service/db.py b/odoo/service/db.py
index ec15383bd4b..ce868f693dd 100644
--- a/odoo/service/db.py
+++ b/odoo/service/db.py
@@ -231,9 +231,13 @@ def dump_db(db_name, stream, backup_format='zip'):
@check_db_management_enabled
def exp_restore(db_name, data, copy=False):
+ def chunks(d, n=8192):
+ for i in range(0, len(d), n):
+ yield d[i:i+n]
data_file = tempfile.NamedTemporaryFile(delete=False)
try:
- data_file.write(base64.b64decode(data))
+ for chunk in chunks(data):
+ data_file.write(base64.b64decode(chunk))
data_file.close()
restore_db(db_name, data_file.name, copy=copy)
finally: