diff options
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 && !(state.reconciliation_proposition[0] || {}).partial_reconcile"> + <tr t-if="state.balance.amount_currency && !(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 < 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 < 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 > 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 < 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 < 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> </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: |
