diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 5f1ae94b93a..d565b07e54c 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -307,7 +307,10 @@ class AccountBankStatement(models.Model): #try to assign partner to bank_statement_line stl_to_assign_partner = [stl.id for stl in st_lines_left if not stl.partner_id] refs = list(set([st.name for st in st_lines_left if not stl.partner_id])) - if st_lines_left and stl_to_assign_partner and refs: + if st_lines_left and stl_to_assign_partner and refs\ + and st_lines_left[0].journal_id.default_credit_account_id\ + and st_lines_left[0].journal_id.default_debit_account_id: + sql_query = """SELECT aml.partner_id, aml.ref, stl.id FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id @@ -605,13 +608,14 @@ class AccountBankStatementLine(models.Model): acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true" select_clause = "SELECT aml.id " from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id " - where_clause = """WHERE aml.company_id = %(company_id)s - AND ( - (aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s - AND aml.payment_id IS NOT NULL) - OR - ("""+acc_type+""" AND aml.reconciled = false) - )""" + account_clause = '' + if self.journal_id.default_credit_account_id and self.journal_id.default_debit_account_id: + account_clause = "(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s AND aml.payment_id IS NOT NULL) OR" + where_clause = """WHERE aml.company_id = %(company_id)s + AND ( + """ + account_clause + """ + ("""+acc_type+""" AND aml.reconciled = false) + )""" where_clause = where_clause + ' AND aml.partner_id = %(partner_id)s' if self.partner_id else where_clause where_clause = where_clause + ' AND aml.id NOT IN %(excluded_ids)s' if excluded_ids else where_clause if split: diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 9fd47eb1592..d5630cdc134 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -837,7 +837,6 @@ class AccountInvoice(models.Model): if tax.amount_type == "group": for child_tax in tax.children_tax_ids: done_taxes.append(child_tax.id) - done_taxes.append(tax.id) res.append({ 'invoice_tax_line_id': tax_line.id, 'tax_line_id': tax_line.tax_id.id, @@ -849,8 +848,9 @@ class AccountInvoice(models.Model): 'account_id': tax_line.account_id.id, 'account_analytic_id': tax_line.account_analytic_id.id, 'invoice_id': self.id, - 'tax_ids': [(6, 0, done_taxes)] if tax_line.tax_id.include_base_amount else [] + 'tax_ids': [(6, 0, list(done_taxes))] if tax_line.tax_id.include_base_amount else [] }) + done_taxes.append(tax.id) return res def inv_line_characteristic_hashcode(self, invoice_line): diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 413c2e39440..d2369a6292b 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -539,6 +539,7 @@ class AccountMoveLine(models.Model): {2} WHERE a.reconcile IS TRUE + AND l.full_reconcile_id is NULL {3} {4} {5} @@ -1557,6 +1558,50 @@ class AccountPartialReconcile(models.Model): move.post() return line_to_reconcile, partial_rec + # Do not forwardport in master as of 2017-07-20 + def _fix_multiple_exchange_rates_diff(self, amls_to_fix, amount_diff, diff_in_currency, currency, move): + self.ensure_one() + move_lines = self.env['account.move.line'].with_context(check_move_validity=False) + partial_reconciles = self.with_context(skip_full_reconcile_check=True) + amount_diff = self.company_id.currency_id.round(amount_diff) + diff_in_currency = currency.round(diff_in_currency) + + for aml in amls_to_fix: + account_payable_line = move_lines.create({ + 'name': _('Currency exchange rate difference'), + 'debit': amount_diff < 0 and -aml.amount_residual or 0.0, + 'credit': amount_diff > 0 and aml.amount_residual or 0.0, + 'account_id': self.debit_move_id.account_id.id, + 'move_id': move.id, + 'currency_id': currency.id, + 'amount_currency': -aml.amount_residual_currency, + 'partner_id': self.debit_move_id.partner_id.id, + }) + + move_lines.create({ + 'name': _('Currency exchange rate difference'), + 'debit': amount_diff > 0 and aml.amount_residual or 0.0, + 'credit': amount_diff < 0 and -aml.amount_residual or 0.0, + 'account_id': amount_diff > 0 and self.company_id.currency_exchange_journal_id.default_debit_account_id.id or self.company_id.currency_exchange_journal_id.default_credit_account_id.id, + 'move_id': move.id, + 'currency_id': currency.id, + 'amount_currency': aml.amount_residual_currency, + 'partner_id': self.debit_move_id.partner_id.id}) + + partial_rec = super(AccountPartialReconcile, partial_reconciles).create({ + 'debit_move_id': aml.credit and account_payable_line.id or aml.id, + 'credit_move_id': aml.debit and account_payable_line.id or aml.id, + 'amount': abs(aml.amount_residual), + 'amount_currency': abs(aml.amount_residual_currency), + 'currency_id': currency.id, + }) + + move_lines |= account_payable_line + partial_reconciles |= partial_rec + + partial_reconciles._compute_partial_lines() + return move_lines, partial_reconciles + def create_tax_cash_basis_entry(self, percentage_before_rec): self.ensure_one() move_date = self.debit_move_id.date @@ -1659,30 +1704,16 @@ class AccountPartialReconcile(models.Model): } return self.env['account.move'].create(move_vals) - @api.model - def create(self, vals): - aml = [] - if vals.get('debit_move_id', False): - aml.append(vals['debit_move_id']) - if vals.get('credit_move_id', False): - aml.append(vals['credit_move_id']) - # Get value of matched percentage from both move before reconciliating - lines = self.env['account.move.line'].browse(aml) - if lines[0].account_id.internal_type in ('receivable', 'payable'): - percentage_before_rec = lines._get_matched_percentage() - # Reconcile - res = super(AccountPartialReconcile, self).create(vals) - # if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry - if lines[0].account_id.internal_type in ('receivable', 'payable'): - res.create_tax_cash_basis_entry(percentage_before_rec) + # Do not forwardport in master as of 2017-07-20 + def _compute_partial_lines(self): if self._context.get('skip_full_reconcile_check'): #when running the manual reconciliation wizard, don't check the partials separately for full #reconciliation or exchange rate because it is handled manually after the whole processing - return res + return self #check if the reconcilation is full #first, gather all journal items involved in the reconciliation just created - partial_rec_set = OrderedDict.fromkeys([x for x in res]) - aml_set = self.env['account.move.line'] + partial_rec_set = OrderedDict.fromkeys([x for x in self]) + aml_set = aml_to_balance = self.env['account.move.line'] total_debit = 0 total_credit = 0 total_amount_currency = 0 @@ -1690,7 +1721,7 @@ class AccountPartialReconcile(models.Model): #possible to compute the exchange difference entry and it has to be done manually. currency = list(partial_rec_set)[0].currency_id maxdate = None - aml_to_balance = None + for partial_rec in partial_rec_set: if partial_rec.currency_id != currency: #no exchange rate entry will be created @@ -1698,7 +1729,7 @@ class AccountPartialReconcile(models.Model): for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]: if aml not in aml_set: if aml.amount_residual or aml.amount_residual_currency: - aml_to_balance = aml + aml_to_balance |= aml maxdate = max(aml.date, maxdate) total_debit += aml.debit total_credit += aml.credit @@ -1716,16 +1747,21 @@ class AccountPartialReconcile(models.Model): aml_ids = aml_set.ids #then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full digits_rounding_precision = aml_set[0].company_id.currency_id.rounding + if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0: exchange_move_id = False exchange_partial_rec_id = False if currency and aml_to_balance: + exchange_move = (self.env['account.move'] + .create(self.env['account.full.reconcile'] + ._prepare_exchange_diff_move(move_date=maxdate, company=aml_to_balance[0].company_id))) #eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates - rate_diff_aml, rate_diff_partial_rec = partial_rec.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, maxdate) - aml_ids.append(rate_diff_aml.id) - partial_rec_ids.append(rate_diff_partial_rec.id) - exchange_move_id = rate_diff_aml.move_id.id - exchange_partial_rec_id = rate_diff_partial_rec.id + rate_diff_amls, rate_diff_partial_recs = partial_rec._fix_multiple_exchange_rates_diff(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, exchange_move) + aml_ids += rate_diff_amls.ids + partial_rec_ids += rate_diff_partial_recs.ids + exchange_move.post() + exchange_move_id = exchange_move.id + exchange_partial_rec_id = rate_diff_partial_recs[-1:].id #mark the reference of the full reconciliation on the partial ones and on the entries self.env['account.full.reconcile'].with_context(check_move_validity=False).create({ 'partial_reconcile_ids': [(4, p_id) for p_id in partial_rec_ids], @@ -1733,6 +1769,25 @@ class AccountPartialReconcile(models.Model): 'exchange_move_id': exchange_move_id, 'exchange_partial_rec_id': exchange_partial_rec_id, }) + + # Do not forwardport in master as of 2017-07-20 + @api.model + def create(self, vals): + aml = [] + if vals.get('debit_move_id', False): + aml.append(vals['debit_move_id']) + if vals.get('credit_move_id', False): + aml.append(vals['credit_move_id']) + # Get value of matched percentage from both move before reconciliating + lines = self.env['account.move.line'].browse(aml) + if lines[0].account_id.internal_type in ('receivable', 'payable'): + percentage_before_rec = lines._get_matched_percentage() + # Reconcile + res = super(AccountPartialReconcile, self).create(vals) + # if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry + if lines[0].account_id.internal_type in ('receivable', 'payable'): + res.create_tax_cash_basis_entry(percentage_before_rec) + res._compute_partial_lines() return res @api.multi @@ -1778,3 +1833,20 @@ class AccountFullReconcile(models.Model): rec.exchange_move_id = False to_reverse.reverse_moves() return super(AccountFullReconcile, self).unlink() + + # Do not forwardport in master as of 2017-07-20 + @api.model + def _prepare_exchange_diff_move(self, move_date, company): + if not company.currency_exchange_journal_id: + raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + if not company.income_currency_exchange_account_id.id: + raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + if not company.expense_currency_exchange_account_id.id: + raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + res = {'journal_id': company.currency_exchange_journal_id.id} + # The move date should be the maximum date between payment and invoice + # (in case of payment in advance). However, we should make sure the + # move date is not recorded after the end of year closing. + if move_date > company.fiscalyear_lock_date: + res['date'] = move_date + return res diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 50012321db5..0093915eee5 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -477,3 +477,147 @@ class TestReconciliation(AccountingTestCase): self.assertEquals(round(aml.credit, 2), line['credit']) self.assertEquals(round(aml.amount_currency, 2), line['amount_currency']) self.assertEquals(aml.currency_id.id, line['currency_id']) + + def test_partial_reconcile_currencies(self): + # client Account (payable, rsa) + # Debit Credit + # -------------------------------------------------------- + # Pay a : 25/0.5 = 50 | Inv a : 50/0.5 = 100 + # Pay b: 50/0.75 = 66.66 | Inv b : 50/0.75 = 66.66 + # Pay c: 25/0.8 = 31.25 | + # + # Debit_currency = 100 | Credit currency = 100 + # Debit = 147.91 | Credit = 166.66 + # Balance Debit = 18.75 + # Counterpart Credit goes in Exchange diff + + dest_journal_id = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', self.env.ref('base.main_company').id)], limit=1) + account_expenses = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1) + + self.bank_journal_euro.write({'default_debit_account_id': self.account_rsa.id, + 'default_credit_account_id': self.account_rsa.id}) + dest_journal_id.write({'default_debit_account_id': self.account_rsa.id, + 'default_credit_account_id': self.account_rsa.id}) + # Setting up rates for USD (main_company is in EUR) + self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01', + 'rate': 0.5, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id}) + + self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '08' + '-01', + 'rate': 0.75, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id}) + + self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '09' + '-01', + 'rate': 0.80, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id}) + + # Preparing Invoices (from vendor) + invoice_a = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, + 'reference_type': 'none', + 'currency_id': self.currency_usd_id, + 'name': 'invoice to vendor', + 'account_id': self.account_rsa.id, + 'type': 'in_invoice', + 'date_invoice': time.strftime('%Y') + '-' + '07' + '-01', + }) + self.account_invoice_line_model.create({'product_id': self.product.id, + 'quantity': 1, + 'price_unit': 50, + 'invoice_id': invoice_a.id, + 'name': 'product that cost ' + str(50), + 'account_id': account_expenses.id, + }) + + invoice_b = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, + 'reference_type': 'none', + 'currency_id': self.currency_usd_id, + 'name': 'invoice to vendor', + 'account_id': self.account_rsa.id, + 'type': 'in_invoice', + 'date_invoice': time.strftime('%Y') + '-' + '08' + '-01', + }) + self.account_invoice_line_model.create({'product_id': self.product.id, + 'quantity': 1, + 'price_unit': 50, + 'invoice_id': invoice_b.id, + 'name': 'product that cost ' + str(50), + 'account_id': account_expenses.id, + }) + + invoice_a.action_invoice_open() + invoice_b.action_invoice_open() + + # Preparing Payments + # One partial for invoice_a (fully assigned to it) + payment_a = self.env['account.payment'].create({'payment_type': 'outbound', + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_euro.id, + 'company_id': self.env.ref('base.main_company').id, + 'payment_date': time.strftime('%Y') + '-' + '07' + '-01', + 'partner_id': self.partner_agrolait_id, + 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id, + 'destination_journal_id': dest_journal_id.id, + 'partner_type': 'supplier'}) + + # One that will complete the payment of a, the rest goes to b + payment_b = self.env['account.payment'].create({'payment_type': 'outbound', + 'amount': 50, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_euro.id, + 'company_id': self.env.ref('base.main_company').id, + 'payment_date': time.strftime('%Y') + '-' + '08' + '-01', + 'partner_id': self.partner_agrolait_id, + 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id, + 'destination_journal_id': dest_journal_id.id, + 'partner_type': 'supplier'}) + + # The last one will complete the payment of b + payment_c = self.env['account.payment'].create({'payment_type': 'outbound', + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_euro.id, + 'company_id': self.env.ref('base.main_company').id, + 'payment_date': time.strftime('%Y') + '-' + '09' + '-01', + 'partner_id': self.partner_agrolait_id, + 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id, + 'destination_journal_id': dest_journal_id.id, + 'partner_type': 'supplier'}) + + payment_a.post() + payment_b.post() + payment_c.post() + + # Assigning payments to invoices + debit_line_a = payment_a.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id) + debit_line_b = payment_b.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id) + debit_line_c = payment_c.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id) + + invoice_a.assign_outstanding_credit(debit_line_a.id) + invoice_a.assign_outstanding_credit(debit_line_b.id) + invoice_b.assign_outstanding_credit(debit_line_b.id) + invoice_b.assign_outstanding_credit(debit_line_c.id) + + # Asserting correctness (only in the payable account) + full_reconcile = False + for inv in (invoice_a + invoice_b): + self.assertTrue(inv.reconciled) + for aml in (inv.payment_move_line_ids + inv.move_id.line_ids).filtered(lambda l: l.account_id == self.account_rsa): + self.assertEqual(aml.amount_residual, 0.0) + self.assertEqual(aml.amount_residual_currency, 0.0) + self.assertTrue(aml.reconciled) + if not full_reconcile: + full_reconcile = aml.full_reconcile_id + else: + self.assertTrue(aml.full_reconcile_id == full_reconcile) + + full_rec_move = full_reconcile.exchange_move_id + # Globally check whether the amount is correct + self.assertEqual(full_rec_move.amount, 18.75) + + # Checking if the direction of the move is correct + full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa) + self.assertEqual(full_rec_payable.balance, 18.75) diff --git a/addons/account/views/account_config_settings_views.xml b/addons/account/views/account_config_settings_views.xml index d7aad5c3dfe..9bb0a845201 100644 --- a/addons/account/views/account_config_settings_views.xml +++ b/addons/account/views/account_config_settings_views.xml @@ -16,7 +16,7 @@
diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py index f4cafe12319..d4b65d6b020 100644 --- a/addons/account_asset/models/account_invoice.py +++ b/addons/account_asset/models/account_invoice.py @@ -108,3 +108,6 @@ class AccountInvoiceLine(models.Model): elif invoice.type == 'in_invoice': self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id super(AccountInvoiceLine, self)._set_additional_fields(invoice) + + def get_invoice_line_account(self, type, product, fpos, company): + return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company) diff --git a/addons/account_asset/static/tests/account_asset_tests.js b/addons/account_asset/static/tests/account_asset_tests.js index 8f353f3bf5c..3ae5ac40073 100644 --- a/addons/account_asset/static/tests/account_asset_tests.js +++ b/addons/account_asset/static/tests/account_asset_tests.js @@ -125,7 +125,7 @@ QUnit.test('basic rendering', function (assert) { }); QUnit.test('click events are correctly triggered', function (assert) { - assert.expect(3); + assert.expect(2); var form = createView({ View: FormView, @@ -146,8 +146,7 @@ QUnit.test('click events are correctly triggered', function (assert) { intercepts: { execute_action: function (event) { var data = event.data; - assert.strictEqual(data.model, 'line', "should have correct model"); - assert.deepEqual(data.res_ids, [4], "should have correct res_ids"); + assert.strictEqual(data.env.model, 'line', "should have correct model"); assert.strictEqual(data.action_data.name, 'create_move', "should call correct method"); }, diff --git a/addons/base_setup/views/base_config_settings_views.xml b/addons/base_setup/views/base_config_settings_views.xml index 6d4f9277de1..a0dab58c570 100644 --- a/addons/base_setup/views/base_config_settings_views.xml +++ b/addons/base_setup/views/base_config_settings_views.xml @@ -7,7 +7,7 @@
diff --git a/addons/event/models/event.py b/addons/event/models/event.py index f0d6d0312cf..95310a67122 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -316,6 +316,9 @@ class EventEvent(models.Model): for attendee in self.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send) + @api.multi + def _is_event_registrable(self): + return True class EventRegistration(models.Model): _name = 'event.registration' diff --git a/addons/event/views/event_config_settings_views.xml b/addons/event/views/event_config_settings_views.xml index aba1ec2f607..5d9ea62c0f8 100644 --- a/addons/event/views/event_config_settings_views.xml +++ b/addons/event/views/event_config_settings_views.xml @@ -8,7 +8,7 @@

Advertising

diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py index a0d27bd268f..d1c28b32d2a 100644 --- a/addons/event_sale/models/event.py +++ b/addons/event_sale/models/event.py @@ -53,6 +53,12 @@ class Event(models.Model): }) for ticket in self.event_type_id.event_ticket_ids] + @api.multi + def _is_event_registrable(self): + self.ensure_one() + if not self.event_ticket_ids: + return True + return all(self.event_ticket_ids.with_context(active_test=False).mapped(lambda t: t.product_id.active)) class EventTicket(models.Model): _name = 'event.event.ticket' diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml index 3b983ebd256..de76683ddc0 100644 --- a/addons/gamification/data/goal_base.xml +++ b/addons/gamification/data/goal_base.xml @@ -98,7 +98,7 @@ % else: - The following message contains the current progress for the challenge ${object.name}. + Challenge: ${object.name}. % for line in challenge_lines: @@ -113,7 +113,7 @@ % if len(line['goals'])>2:
- +
% set top_goals = [line['goals'][1], line['goals'][0], line['goals'][2]] % for goal in top_goals: @@ -144,9 +144,10 @@
${extra_div | safe}
- + % set path = object.env['res.users'].browse(goal['user_id']).partner_id.image_small + ${goal['name']}
-
+
${goal['name']}
@@ -177,8 +178,8 @@
- - + + - - + - + diff --git a/addons/hr_attendance/views/attendance_config_settings_views.xml b/addons/hr_attendance/views/attendance_config_settings_views.xml index b8c11252fbf..cf72f9f2220 100644 --- a/addons/hr_attendance/views/attendance_config_settings_views.xml +++ b/addons/hr_attendance/views/attendance_config_settings_views.xml @@ -7,7 +7,7 @@

Check-In/Out

@@ -64,4 +64,4 @@ - \ No newline at end of file + diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index cdcf25b491f..ce0054a5636 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -385,7 +385,7 @@ class HrExpenseSheet(models.Model): _order = "accounting_date desc, id desc" name = fields.Char(string='Expense Report Summary', required=True) - expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False) + expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False) state = fields.Selection([('submit', 'Submitted'), ('approve', 'Approved'), ('post', 'Posted'), diff --git a/addons/hr_expense/views/hr_expense_config_settings_views.xml b/addons/hr_expense/views/hr_expense_config_settings_views.xml index 19078d6071c..897e78837ca 100644 --- a/addons/hr_expense/views/hr_expense_config_settings_views.xml +++ b/addons/hr_expense/views/hr_expense_config_settings_views.xml @@ -9,7 +9,7 @@

Recording

diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index 26a58ea79ce..643e62b5505 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -392,7 +392,7 @@ - + diff --git a/addons/hr_payroll/views/hr_payroll_config_settings_views.xml b/addons/hr_payroll/views/hr_payroll_config_settings_views.xml index 548ccddcc28..673e35c1992 100644 --- a/addons/hr_payroll/views/hr_payroll_config_settings_views.xml +++ b/addons/hr_payroll/views/hr_payroll_config_settings_views.xml @@ -15,7 +15,7 @@
diff --git a/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml b/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml index 54499005565..6617b8d4153 100644 --- a/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml +++ b/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml @@ -8,7 +8,7 @@

Job Posting

diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index d8d07c42fd8..3023fdcea59 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model): @api.model def default_get(self, field_list): result = super(AccountAnalyticLine, self).default_get(field_list) - if 'employee_id' in field_list and result.get('user_id') and result.get('project_id'): + if 'employee_id' in field_list and result.get('user_id'): result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id return result diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 988fed44727..e9bf605c382 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -571,6 +571,7 @@ class MailTemplate(models.Model): 'name': attachment[0], 'datas_fname': attachment[0], 'datas': attachment[1], + 'type': 'binary', 'res_model': 'mail.message', 'res_id': mail.mail_message_id.id, } diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index c672cab2835..0d23032d051 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -950,13 +950,14 @@ class MailThread(models.AbstractModel): # Alias: check alias_contact settings if alias: + obj = None if thread_id: obj = record_set[0] - elif alias.alias_parent_thread_id: + elif alias.alias_parent_model_id and alias.alias_parent_thread_id: obj = self.env[alias.alias_parent_model_id.model].browse(alias.alias_parent_thread_id) - elif model and hasattr(record_set, '_alias_check_contact'): + elif model: obj = self.env[model] - else: + if not hasattr(obj, '_alias_check_contact'): obj = self.env['mail.alias.mixin'] check_result = obj._alias_check_contact(message, message_dict, alias) if check_result is not True: @@ -1727,6 +1728,7 @@ class MailThread(models.AbstractModel): data_attach = { 'name': name, 'datas': base64.b64encode(str(content)), + 'type': 'binary', 'datas_fname': name, 'description': name, 'res_model': message_data['model'], diff --git a/addons/mass_mailing/views/mass_mailing_config_settings_views.xml b/addons/mass_mailing/views/mass_mailing_config_settings_views.xml index d7164d017d7..7c9dd4e0382 100644 --- a/addons/mass_mailing/views/mass_mailing_config_settings_views.xml +++ b/addons/mass_mailing/views/mass_mailing_config_settings_views.xml @@ -7,7 +7,7 @@

Design

diff --git a/addons/mrp/views/mrp_config_settings_views.xml b/addons/mrp/views/mrp_config_settings_views.xml index df407343714..5b2ca1593f8 100644 --- a/addons/mrp/views/mrp_config_settings_views.xml +++ b/addons/mrp/views/mrp_config_settings_views.xml @@ -8,7 +8,7 @@

Operations

diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py index aab5abe3244..945e24f4308 100644 --- a/addons/pad/models/pad.py +++ b/addons/pad/models/pad.py @@ -122,7 +122,7 @@ class PadCommon(models.AbstractModel): # Update the pad if the `pad_content_field` is modified for k, field in pycompat.items(self._fields): - if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field): + if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]: company = self.env.user.sudo().company_id myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api') path = self[k].split('/p/')[1] diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js index 4068ff24ead..1d162bdbc87 100644 --- a/addons/pad/static/src/js/pad.js +++ b/addons/pad/static/src/js/pad.js @@ -55,7 +55,7 @@ var FieldPad = AbstractField.extend({ // Guess what we decided... var url = {}; url.toJSON = _.constant(this.url); - this._setValue(url); + this._setValue(url, {doNotSetDirty: true}); } return this._super.apply(this, arguments); diff --git a/addons/pad/static/tests/pad_tests.js b/addons/pad/static/tests/pad_tests.js index 5d18e55c1e0..dedf799db66 100644 --- a/addons/pad/static/tests/pad_tests.js +++ b/addons/pad/static/tests/pad_tests.js @@ -193,5 +193,41 @@ QUnit.module('pad widget', { delete FieldPad.prototype.isPadConfigured; }); + QUnit.test('pad widget is not considered dirty at edition', function (assert) { + assert.expect(2); + + var form = createView({ + View: FormView, + model: 'task', + data: this.data, + arch:'' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 2, + mockRPC: function (route, args) { + if (!args.method) { + return $.when(true); + } + return this._super.apply(this, arguments); + }, + session: { + userName: "batman", + }, + }); + form.$buttons.find('.o_form_button_edit').click(); + var def = form.canBeDiscarded(); + + assert.strictEqual($('.modal').length, 0, + "should have no confirmation modal opened"); + + assert.strictEqual(def.state(), 'resolved', + "can be discarded was succesfully resolved"); + form.destroy(); + delete FieldPad.prototype.isPadConfigured; + }); }); diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py index 9455f4e1491..e43e0cbd71f 100644 --- a/addons/payment_stripe/models/payment.py +++ b/addons/payment_stripe/models/payment.py @@ -15,6 +15,12 @@ _logger = logging.getLogger(__name__) # changelog https://stripe.com/docs/upgrades#api-changelog STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'} +# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal +INT_CURRENCIES = [ + 'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', 'RWF', 'KRW', 'VUV', + 'VND', 'XOF' +]; + class PaymentAcquirerStripe(models.Model): _inherit = 'payment.acquirer' @@ -84,7 +90,7 @@ class PaymentTransactionStripe(models.Model): def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None): api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url()) charge_params = { - 'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support) + 'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else self.amount*100), 'currency': self.currency_id.name, 'metadata[reference]': self.reference } diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js index 159bdc14214..3a736330657 100644 --- a/addons/payment_stripe/static/src/js/stripe.js +++ b/addons/payment_stripe/static/src/js/stripe.js @@ -1,6 +1,14 @@ odoo.define('payment_stripe.stripe', function(require) { "use strict"; + var ajax = require('web.ajax'); + // The following currencies are integer only, see + // https://stripe.com/docs/currencies#zero-decimal + var int_currencies = [ + 'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', + 'RWF', 'KRW', 'VUV', 'VND', 'XOF' + ]; + var handler = StripeCheckout.configure({ key: $("input[name='stripe_key']").val(), image: $("input[name='stripe_image']").val(), @@ -50,19 +58,31 @@ odoo.define('payment_stripe.stripe', function(require) { e.preventDefault(); if ($('.o_website_payment').length !== 0) { + var currency = $("input[name='currency']").val(); + var amount = parseFloat($("input[name='amount']").val() || '0.0'); + if (!_.contains(int_currencies, currency)) { + amount = amount*100; + } + ajax.jsonRpc('/website_payment/transaction', 'call', { reference: $("input[name='invoice_num']").val(), - amount: $("input[name='amount']").val(), - currency_id: $("input[name='currency_id']").val(), + amount: amount, + currency_id: currency, acquirer_id: acquirer_id }) handler.open({ name: $("input[name='merchant']").val(), description: $("input[name='invoice_num']").val(), - currency: $("input[name='currency']").val(), - amount: $("input[name='amount']").val()*100 + currency: currency, + amount: amount, }); } else { + var currency = $("input[name='currency']").val(); + var amount = parseFloat($("input[name='amount']").val() || '0.0'); + if (!_.contains(int_currencies, currency)) { + amount = amount*100; + } + ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', { so_id: so_id, access_token: access_token @@ -71,8 +91,8 @@ odoo.define('payment_stripe.stripe', function(require) { handler.open({ name: $("input[name='merchant']").val(), description: $("input[name='invoice_num']").val(), - currency: $("input[name='currency']").val(), - amount: $("input[name='amount']").val()*100 + currency: currency, + amount: amount, }); }); } diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index f848a64cbe1..5e059a41cfc 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -870,7 +870,7 @@ class PosOrderLine(models.Model): def create(self, values): if values.get('order_id') and not values.get('name'): # set name based on the sequence specified on the config - config_id = self.env['pos.order'].browse(values['order_id']).session_id.config_id.id + config_id = self.order_id.browse(values['order_id']).session_id.config_id.id # HACK: sequence created in the same transaction as the config # cf TODO master is pos.config create # remove me saas-15 diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index ac9831e343d..b621d5ba16f 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -397,12 +397,13 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.') # I generate an invoice from the order - self.invoice = self.pos_order_pos1.action_pos_order_invoice() + res = self.pos_order_pos1.action_pos_order_invoice() + self.assertIn('res_id', res, "No invoice created") # I test that the total of the attached invoice is correct - self.amount_total = self.pos_order_pos1.amount_total + invoice = self.env['account.invoice'].browse(res['res_id']) self.assertEqual( - float_compare(self.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct") + float_compare(invoice.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct") """In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports""" diff --git a/addons/point_of_sale/views/pos_config_settings_views.xml b/addons/point_of_sale/views/pos_config_settings_views.xml index 8edc687273a..7cb3922e444 100644 --- a/addons/point_of_sale/views/pos_config_settings_views.xml +++ b/addons/point_of_sale/views/pos_config_settings_views.xml @@ -7,7 +7,7 @@

Point of Sale

diff --git a/addons/product/models/product.py b/addons/product/models/product.py index be9ea3db7c6..638e898aed8 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -295,7 +295,9 @@ class ProductProduct(models.Model): @api.model def create(self, vals): product = super(ProductProduct, self.with_context(create_product_product=True)).create(vals) - product._set_standard_price(vals.get('standard_price', 0.0)) + # When a unique variant is created from tmpl then the standard price is set by _set_standard_price + if not (self.env.context.get('create_from_tmpl') and len(product.product_tmpl_id.product_variant_ids) == 1): + product._set_standard_price(vals.get('standard_price') or 0.0) return product @api.multi diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index 41e9b79110b..a0457f021ba 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -113,8 +113,7 @@ class Pricelist(models.Model): uom_id = self._context['uom'] if uom_id: # rebrowse with uom if given - product_ids = [item[0].id for item in products_qty_partner] - products = self.env['product.product'].with_context(uom=uom_id).browse(product_ids) + products = [item[0].with_context(uom=uom_id) for item in products_qty_partner] products_qty_partner = [(products[index], data_struct[1], data_struct[2]) for index, data_struct in enumerate(products_qty_partner)] else: products = [item[0] for item in products_qty_partner] diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index efb701a6092..ccfbcf28b96 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -172,7 +172,9 @@ class ProductTemplate(models.Model): # Support context pricelists specified as display_name or ID for compatibility if isinstance(pricelist_id_or_name, basestring): - pricelist = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) + pricelist_data = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) + if pricelist_data: + pricelist = self.env['product.pricelist'].browse(pricelist_data[0][0]) elif isinstance(pricelist_id_or_name, pycompat.integer_types): pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name) @@ -288,7 +290,7 @@ class ProductTemplate(models.Model): tools.image_resize_images(vals) template = super(ProductTemplate, self).create(vals) if "create_product_product" not in self._context: - template.create_variant_ids() + template.with_context(create_from_tmpl=True).create_variant_ids() # This is needed to set given values to first variant after creation related_vals = {} diff --git a/addons/project/views/project_config_settings_views.xml b/addons/project/views/project_config_settings_views.xml index 8ec189d2e02..bff22fc3803 100644 --- a/addons/project/views/project_config_settings_views.xml +++ b/addons/project/views/project_config_settings_views.xml @@ -8,7 +8,7 @@

Task Management

diff --git a/addons/purchase/views/purchase_config_settings_views.xml b/addons/purchase/views/purchase_config_settings_views.xml index a1e253ca6e5..92ba3057d9a 100644 --- a/addons/purchase/views/purchase_config_settings_views.xml +++ b/addons/purchase/views/purchase_config_settings_views.xml @@ -7,7 +7,7 @@
diff --git a/addons/sales_team/views/sale_config_settings_views.xml b/addons/sales_team/views/sale_config_settings_views.xml index 4e2c4625ef0..5ebea57ebb9 100644 --- a/addons/sales_team/views/sale_config_settings_views.xml +++ b/addons/sales_team/views/sale_config_settings_views.xml @@ -8,7 +8,7 @@

Integrations

@@ -84,4 +84,4 @@ form inline - \ No newline at end of file + diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py index 3e7fccf5b5f..8b7e70f80f7 100644 --- a/addons/stock/models/stock_location.py +++ b/addons/stock/models/stock_location.py @@ -69,7 +69,7 @@ class Location(models.Model): _sql_constraints = [('barcode_company_uniq', 'unique (barcode,company_id)', 'The barcode for a location must be unique per company !')] @api.one - @api.depends('name', 'location_id') + @api.depends('name', 'location_id.name') def _compute_complete_name(self): """ Forms complete name of location from parent location to child location. """ name = self.name diff --git a/addons/stock/views/stock_config_settings_views.xml b/addons/stock/views/stock_config_settings_views.xml index c86a9d08394..3719c4ab3f3 100644 --- a/addons/stock/views/stock_config_settings_views.xml +++ b/addons/stock/views/stock_config_settings_views.xml @@ -8,7 +8,7 @@
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index fd2d596a4b2..9a29c2ec3e8 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -684,7 +684,7 @@ class Database(http.Controller): request.session.authenticate(name, post['login'], password) return http.local_redirect('/web/') except Exception as e: - error = "Database creation error: %s" % str(e) or repr(e) + error = "Database creation error: %s" % (str(e) or repr(e)) return self._render_template(error=error) @http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False) @@ -695,7 +695,7 @@ class Database(http.Controller): dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name]) return http.local_redirect('/web/database/manager') except Exception as e: - error = "Database duplication error: %s" % str(e) or repr(e) + error = "Database duplication error: %s" % (str(e) or repr(e)) return self._render_template(error=error) @http.route('/web/database/drop', type='http', auth="none", methods=['POST'], csrf=False) @@ -705,7 +705,7 @@ class Database(http.Controller): request._cr = None # dropping a database leads to an unusable cursor return http.local_redirect('/web/database/manager') except Exception as e: - error = "Database deletion error: %s" % str(e) or repr(e) + error = "Database deletion error: %s" % (str(e) or repr(e)) return self._render_template(error=error) @http.route('/web/database/backup', type='http', auth="none", methods=['POST'], csrf=False) @@ -723,7 +723,7 @@ class Database(http.Controller): return response except Exception as e: _logger.exception('Database.backup') - error = "Database backup error: %s" % str(e) or repr(e) + error = "Database backup error: %s" % (str(e) or repr(e)) return self._render_template(error=error) @http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False) @@ -733,7 +733,7 @@ class Database(http.Controller): dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)]) return http.local_redirect('/web/database/manager') except Exception as e: - error = "Database restore error: %s" % str(e) or repr(e) + error = "Database restore error: %s" % (str(e) or repr(e)) return self._render_template(error=error) @http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False) @@ -742,7 +742,7 @@ class Database(http.Controller): dispatch_rpc('db', 'change_admin_password', [master_pwd, master_pwd_new]) return http.local_redirect('/web/database/manager') except Exception as e: - error = "Master password update error: %s" % str(e) or repr(e) + error = "Master password update error: %s" % (str(e) or repr(e)) return self._render_template(error=error) @http.route('/web/database/list', type='json', auth='none') diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js index a4a1d65f7b6..5962bb28acb 100644 --- a/addons/web/static/src/js/chrome/view_manager.js +++ b/addons/web/static/src/js/chrome/view_manager.js @@ -20,7 +20,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { custom_events: { execute_action: function(event) { var data = event.data; - this.do_execute_action(data.action_data, data.model, data.res_ids, data.on_closed) + this.do_execute_action(data.action_data, data.env, data.on_closed) .then(data.on_success, data.on_fail); }, search: function(event) { @@ -554,16 +554,19 @@ var ViewManager = Widget.extend(ControlPanelMixin, { * @param {string} [action_data.effect] if given, a visual effect (a * rainbowman by default) will be displayed when the action is complete, * with the string (evaluated) given as options. - * @param {DataSet} dataset a dataset object used to communicate with the server - * @param {integer[]} [res_ids] the res_ids of the objects on which the action is to be applied (fallback on env.ids if not set) + * @param {Object} env + * @param {string} env.model the model of the record(s) triggering the action + * @param {integer[]} [env.resIDs] the current ids in the environment where the action is triggered + * @param {integer} [env.currentID] the id of the record triggering the action + * @param {Object} [env.context] a context to pass to the action * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action) */ - do_execute_action: function (action_data, model, res_ids, on_closed) { + do_execute_action: function (action_data, env, on_closed) { var self = this; var result_handler = on_closed || function () {}; - var context = new Context(this.env.context, action_data.context || {}); + var context = new Context(env.context, action_data.context || {}); // OR NULL hereunder: pyeval waits specifically for a null value, different from undefined - var record_id = res_ids && res_ids[0] || null; + var recordID = env.currentID || null; // response handler var handler = function (action) { @@ -585,11 +588,11 @@ var ViewManager = Widget.extend(ControlPanelMixin, { })) ); ncontext.add(action_data.context || {}); - ncontext.add({active_model: self.env.modelName}); - if (res_ids) { + ncontext.add({active_model: env.model}); + if (recordID) { ncontext.add({ - active_id: record_id, - active_ids: res_ids, + active_id: recordID, + active_ids: [recordID], }); } ncontext.add(action.context || {}); @@ -611,7 +614,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { if (action_data.special) { return handler({"type":"ir.actions.act_window_close"}); } else if (action_data.type === "object") { - var args = res_ids ? [res_ids] : [this.env.ids]; + var args = recordID ? [[recordID]] : [env.resIDs]; if (action_data.args) { try { // Warning: quotes and double quotes problem due to json and xml clash @@ -623,13 +626,13 @@ var ViewManager = Widget.extend(ControlPanelMixin, { } } args.push(context); - var dataset = new data.DataSet(this, model, this.env.context); + var dataset = new data.DataSet(this, env.model, env.context); return dataset.call_button(action_data.name, args).then(handler); } else if (action_data.type === "action") { return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), { - active_model: this.env.modelName, - active_ids: this.env.ids, - active_id: record_id, + active_model: env.model, + active_ids: env.resIDs, + active_id: recordID, })).then(handler); } }, diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 64049d8b1ec..0dce6ec2bb4 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -219,10 +219,16 @@ var FieldMany2One = AbstractField.extend({ /** * @private * @param {string} name + * @returns {Deferred} resolved after the name_create or when the slowcreate + * modal is closed. */ _quickCreate: function (name) { var self = this; - var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name)); + var def = $.Deferred(); + var slowCreate = function () { + var dialog = self._searchCreatePopup.bind(self, "form", false, self._createContext(name)); + dialog.on('closed', self, def.resolve.bind(def)); + }; if (this.nodeOptions.quick_create) { this.trigger_up('mutexify', { action: function () { @@ -235,12 +241,16 @@ var FieldMany2One = AbstractField.extend({ if (self.mode === "edit") { self.reinitialize({id: result[0], display_name: result[1]}); } - }, slowCreate); + def.resolve(); + }).fail(function () { + slowCreate(); + }); }, }); } else { slowCreate(); } + return def; }, /** * @private diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index 8de118b29b7..644ba8ac985 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -29,6 +29,7 @@ var ajax = require('web.ajax'); var AbstractModel = require('web.AbstractModel'); var AbstractRenderer = require('web.AbstractRenderer'); var AbstractController = require('web.AbstractController'); +var Context = require('web.Context'); var AbstractView = Class.extend({ // name displayed in view switchers @@ -152,9 +153,9 @@ var AbstractView = Class.extend({ }); }, /** - * Returns the view model or create an instance of it if there is noney + * Returns the view model or create an instance of it if none * - * @param {Widget} parent the parent of the model + * @param {Widget} parent the parent of the model, if it has to be created * @return {Object} instance of the view model */ getModel: function (parent) { @@ -167,7 +168,7 @@ var AbstractView = Class.extend({ /** * Returns the a new view renderer instance * - * @param {Widget} parent the parent of the model + * @param {Widget} parent the parent of the model, if it has to be created * @param {Object} state the information related to the rendered view * @return {Object} instance of the view renderer */ @@ -239,7 +240,7 @@ var AbstractView = Class.extend({ * Loads the subviews for x2many fields when they are not inline * * @private - * @param {Widget} parent the parent of the model + * @param {Widget} parent the parent of the model, if it has to be created * @returns {Deferred} */ _loadSubviews: function (parent) { diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 3a47f65ab03..6ba9dbea477 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -232,13 +232,17 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { return self.isDestroyed() ? $.when() : self.reload(); }; record = record || this.model.get(this.handle); - var recordID = record.data.id; + this.trigger_up('execute_action', { action_data: _.extend({}, attrs, { - context: record.getContext({additionalContext: attrs.context || {}}), + context: record.getContext({additionalContext: attrs.context || {}}), }), - model: record.model, - res_ids: [recordID], + env: { + context: record.getContext(), + currentID: record.data.id, + model: record.model, + resIDs: record.res_ids, + }, on_closed: function (reason) { if (!_.isObject(reason)) { reload(reason); 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 04fe384afdc..8755805f993 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -649,7 +649,7 @@ var BasicModel = AbstractModel.extend({ var element = this.localData[id]; if (element.type === 'record') { - if ('currentId' in options && !options.currentId) { + if (!options.currentId && (('currentId' in options) || this.isNew(id))) { var params = { context: element.context, fieldsInfo: element.fieldsInfo, @@ -2310,12 +2310,20 @@ var BasicModel = AbstractModel.extend({ }); list = this._applyX2ManyOperations(list); if (type === 'many2many' || list._forceM2MLink) { + var relRecordCreated = _.filter(relRecordAdded, function (rec) { + return typeof rec.res_id === 'string'; + }); + var realIDs = _.difference(list.res_ids, _.pluck(relRecordCreated, 'res_id')); // deliberately generate a single 'replace' command instead // of a 'delete' and a 'link' commands with the exact diff // because 1) performance-wise it doesn't change anything // and 2) to guard against concurrent updates (policy: force // a complete override of the actual value of the m2m) - commands[fieldName].push(x2ManyCommands.replace_with(list.res_ids)); + commands[fieldName].push(x2ManyCommands.replace_with(realIDs)); + _.each(relRecordCreated, function (relRecord) { + var changes = self._generateChanges(relRecord, options); + commands[fieldName].push(x2ManyCommands.create(changes)); + }); // generate update commands for records that have been // updated (it may happen with editable lists) _.each(relRecordUpdated, function (relRecord) { @@ -2435,6 +2443,12 @@ var BasicModel = AbstractModel.extend({ */ _getDomain: function (element, options) { if (options && options.fieldName) { + if (element._domains[options.fieldName]) { + return Domain.prototype.stringToArray( + element._domains[options.fieldName], + this._getEvalContext(element, true) + ); + } var viewType = options.viewType || element.viewType; var fieldInfo = element.fieldsInfo[viewType][options.fieldName]; if (fieldInfo && fieldInfo.domain) { @@ -2697,6 +2711,7 @@ var BasicModel = AbstractModel.extend({ var dataPoint = { _cache: type === 'list' ? {} : undefined, _changes: null, + _domains: {}, aggregateValues: params.aggregateValues || {}, context: params.context || {}, count: params.count || res_ids.length, @@ -2870,6 +2885,9 @@ var BasicModel = AbstractModel.extend({ r._changes = _.defaults(value[2], r.data); for (var name in r._changes) { + if (r._changes[name] === null) { + continue; + } var isFieldInView = name in r.fields; if (isFieldInView && r.fields[name].type === 'many2one') { var rec = self._makeDataPoint({ @@ -3021,12 +3039,7 @@ var BasicModel = AbstractModel.extend({ record._warning = true; } if (result.domain) { - var fieldsInfo = record.fieldsInfo[viewType || record.viewType]; - for (var fieldName in result.domain) { - if (fieldsInfo[fieldName]) { - fieldsInfo[fieldName].domain = result.domain[fieldName]; - } - } + record._domains = _.extend(record._domains, result.domain); } return self._applyOnChange(result.value, record).then(function () { return result; @@ -3083,7 +3096,9 @@ var BasicModel = AbstractModel.extend({ */ _readGroup: function (list) { var self = this; - var fields = _.uniq(list.getFieldNames().concat(list.groupedBy)); + var groupByField = list.groupedBy[0]; + var rawGroupBy = groupByField.split(':')[0]; + var fields = _.uniq(list.getFieldNames().concat(rawGroupBy)); return this._rpc({ model: list.model, method: 'read_group', @@ -3094,8 +3109,6 @@ var BasicModel = AbstractModel.extend({ lazy: true, }) .then(function (groups) { - var groupByField = list.groupedBy[0]; - var rawGroupBy = groupByField.split(':')[0]; var previousGroups = _.map(list.data, function (groupID) { return self.localData[groupID]; }); diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js index 4f5719063ff..5fb79cfa3a0 100644 --- a/addons/web/static/src/js/views/calendar/calendar_renderer.js +++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js @@ -322,10 +322,11 @@ return AbstractRenderer.extend({ element.addClass($render.attr('class')); var display_hour = ''; if (!event.allDay) { - display_hour = (event.start.format('HH:mm') === '00:00' ? event.r_start.format('HH:mm') : event.start.format('HH:mm')) + ' - ' + - (event.end && event.end.format('HH:mm') !== '00:00' ? event.end.format('HH:mm') : event.r_end.format('HH:mm')); + var start = event.r_start || event.start; + var end = event.r_end || event.end; + display_hour = start.format('HH:mm') + ' - ' + end.format('HH:mm'); if (display_hour === '00:00 - 00:00') { - display_hour = _t('All the day'); + display_hour = _t('All day'); } } element.find('.fc-content .fc-time').text(display_hour); diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index cdfe2cb8d5d..0718b4e202e 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -217,7 +217,7 @@ var FormController = BasicController.extend({ * * @private * @override method from field manager mixin - * @param {string} id + * @param {string} id - id of the previously changed record * @returns {Deferred} */ _confirmSave: function (id) { @@ -228,15 +228,21 @@ var FormController = BasicController.extend({ return this._setMode('readonly'); } } else { - // a subrecord changed, so update the corresponding relational field + // A subrecord has changed, so update the corresponding relational field // i.e. the one whose value is a record with the given id or a list // having a record with the given id in its data var record = this.model.get(this.handle); - var fieldsChanged = _.findKey(record.data, function (d) { - return _.isObject(d) && - (d.id === id || _.findWhere(d.data, {id: id})); - }); - return this.renderer.confirmChange(record, record.id, [fieldsChanged]); + + // Callback function which returns true + // if a value recursively contains a record with the given id. + // This will be used to determine the list of fields to reload. + var containsChangedRecord = function (value) { + return _.isObject(value) && + (value.id === id || _.find(value.data, containsChangedRecord)); + }; + + var changedFields = _.findKey(record.data, containsChangedRecord); + return this.renderer.confirmChange(record, record.id, [changedFields]); } }, /** 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 2cf6a94285e..6870dbbbe24 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -237,8 +237,12 @@ var KanbanController = BasicController.extend({ } this.trigger_up('execute_action', { action_data: attrs, - model: record.model, - res_ids: [record.res_id], + env: { + context: record.getContext(), + currentID: record.res_id, + model: record.model, + resIDs: record.res_ids, + }, on_closed: function () { self.model.reload(record.id).then(function (db_id) { var data = self.model.get(db_id); diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index 3977ac0df87..71402fc92ec 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -240,7 +240,7 @@ var KanbanModel = BasicModel.extend({ */ _readTooltipFields: function (list) { var self = this; - var groupedByField = list.fields[list.groupedBy[0]]; + var groupedByField = list.fields[list.groupedBy[0].split(':')[0]]; if (groupedByField.type !== 'many2one') { return $.when(); } diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 295266db276..6e69c0217b2 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -281,15 +281,19 @@ var ListRenderer = BasicRenderer.extend({ this._registerModifiers(node, record, $button); - // TODO this should be moved to a handler - var self = this; - $button.on("click", function (e) { - e.stopPropagation(); - self.trigger_up('button_clicked', { - attrs: node.attrs, - record: record, + if (record.res_id) { + // TODO this should be moved to a handler + var self = this; + $button.on("click", function (e) { + e.stopPropagation(); + self.trigger_up('button_clicked', { + attrs: node.attrs, + record: record, + }); }); - }); + } else { + $button.prop('disabled', true); + } return $button; }, diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 0ad9aa2a416..29f8666455a 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -922,6 +922,140 @@ QUnit.module('relational_fields', { }); }); + QUnit.test('list in form: create with one2many with many2one', function (assert) { + assert.expect(1); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + mockRPC: function (route, args) { + if (args.method === 'default_get') { + return $.when({p: [[0, 0, {display_name: 'new record'}]]}); + } else if (args.method === 'name_get') { + // This should not be called at all and thus is not accounted for + // in the assert.expect. If this is called, you broke this test. + assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'), + "should not call name_get for the m2o inside o2m which has no value"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual($('td.o_data_cell:first').text(), 'new record', + "should have created the new record in the o2m with the correct name"); + + form.destroy(); + }); + + QUnit.test('list in form: default_get with x2many create', function (assert) { + assert.expect(3); + + this.data.partner.onchanges.timmy = function (obj) { + assert.deepEqual( + obj.timmy, + [ + [6, false, []], + [0, false, {display_name: 'brandon is the new timmy', name: 'brandon'}] + ], + "should have properly created the x2many command list"); + obj.int_field = obj.timmy.length; + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + mockRPC: function (route, args) { + if (args.method === 'default_get') { + return $.when({timmy: [[0, 0, {display_name: 'brandon is the new timmy', name: 'brandon'}]]}); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual($('td.o_data_cell:first').text(), 'brandon is the new timmy', + "should have created the new record in the m2m with the correct name"); + assert.strictEqual($('input.o_field_integer').val(), '2', + "should have called and executed the onchange properly"); + + form.destroy(); + }); + + QUnit.test('list in form: call button in sub view', function (assert) { + assert.expect(6); + + this.data.partner.records[0].p = [2]; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 1, + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/product/get_formview_id') { + return $.when(false); + } + return this._super.apply(this, arguments); + }, + intercepts: { + execute_action: function (event) { + assert.strictEqual(event.data.env.model, 'product', + 'should call with correct model in env'); + assert.strictEqual(event.data.env.currentID, 37, + 'should call with correct currentID in env'); + assert.deepEqual(event.data.env.resIDs, [37], + 'should call with correct resIDs in env'); + }, + }, + archs: { + 'product,false,form': '
' + + '
' + + '
' + + '', + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('td.o_data_cell:first').click(); // edit first one2many line + form.$('.o_external_button').click(); // open product sub view in modal + $('button:contains("Just do it !")').click(); // click on action button + $('button:contains("Just don\'t do it !")').click(); // click on object button + + form.destroy(); + }); + QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) { assert.expect(1); @@ -1418,6 +1552,115 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('many2one in one2many: domain updated by an onchange', function (assert) { + assert.expect(3); + + this.data.partner.onchanges = { + trululu: function () {}, + }; + + var domain = []; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'onchange') { + return $.when({ + domain: { + trululu: domain, + }, + }); + } + if (args.method === 'name_search') { + assert.deepEqual(args.kwargs.args, domain, + "sent domain should be correct"); + } + return this._super(route, args); + }, + viewOptions: { + mode: 'edit', + }, + }); + + // add a first row with a specific domain for the m2o + domain = [['id', 'in', [10]]]; // domain for subrecord 1 + form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange + form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search + + // add a second row with another domain for the m2o + domain = [['id', 'in', [5]]]; // domain for subrecord 2 + form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange + form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search + + // check again the first row to ensure that the domain hasn't change + domain = [['id', 'in', [10]]]; // domain for subrecord 1 should have been kept + form.$('.o_data_row:first .o_data_cell').click(); + form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search + + form.destroy(); + }); + + QUnit.test('updating a many2one from a many2many', function (assert) { + assert.expect(4); + + this.data.turtle.records[1].turtle_trululu = 1; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 1, + archs: { + 'partner,false,form': '
', + }, + mockRPC: function (route, args) { + if (args.method === 'get_formview_id') { + assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id"); + return $.when(false); + } + return this._super(route, args); + }, + }); + + // Opening the modal + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_data_row td:contains(first record)').click(); + form.$('.o_external_button').click(); + assert.strictEqual($('.modal').length, 1, + "should have one modal in body"); + + // Changing the 'trululu' value + $('.modal input[name="display_name"]').val('test').trigger('input'); + $('.modal button.btn-primary').click(); + + // Test whether the value has changed + assert.strictEqual($('.modal').length, 0, + "the modal should be closed"); + assert.equal(form.$('.o_data_cell:contains(test)').text(), 'test', + "the partner name should have been updated to 'test'"); + + form.destroy(); + }); + QUnit.module('FieldOne2Many'); QUnit.test('one2many basic properties', function (assert) { @@ -3847,9 +4090,9 @@ QUnit.module('relational_fields', { res_id: 1, intercepts: { execute_action: function (event) { - assert.deepEqual(event.data.res_ids, [2], + assert.deepEqual(event.data.env.currentID, 2, 'should call with correct id'); - assert.strictEqual(event.data.model, 'partner', + assert.strictEqual(event.data.env.model, 'partner', 'should call with correct model'); assert.strictEqual(event.data.action_data.name, 'method_name', "should call correct method"); @@ -3891,9 +4134,9 @@ QUnit.module('relational_fields', { res_id: 1, intercepts: { execute_action: function (event) { - assert.deepEqual(event.data.res_ids, [2], + assert.deepEqual(event.data.env.currentID, 2, 'should call with correct id'); - assert.strictEqual(event.data.model, 'partner', + assert.strictEqual(event.data.env.model, 'partner', 'should call with correct model'); assert.strictEqual(event.data.action_data.name, 'method_name', "should call correct method"); diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js index 53577d90e40..8c23fc4f97c 100644 --- a/addons/web/static/tests/views/basic_model_tests.js +++ b/addons/web/static/tests/views/basic_model_tests.js @@ -956,6 +956,30 @@ QUnit.module('Views', { model.destroy(); }); + QUnit.test('group on date field with magic grouping method', function (assert) { + assert.expect(1); + + this.params.fieldNames = ['foo']; + this.params.groupedBy = ['date:month']; + this.params.res_id = undefined; + + var model = createModel({ + Model: BasicModel, + data: this.data, + mockRPC: function (route, args) { + if (args.method === 'read_group') { + assert.deepEqual(args.kwargs.fields, ['foo', 'date'], + "should have correctly trimmed the magic grouping info from the field name"); + } + return this._super.apply(this, arguments); + }, + }); + + model.load(this.params); + model.destroy(); + }); + + QUnit.test('read group when grouped by a selection field', function (assert) { assert.expect(5); @@ -1949,4 +1973,39 @@ QUnit.module('Views', { model.destroy(); }); + QUnit.test('reload a new record', function (assert) { + assert.expect(6); + + this.params.context = {}; + this.params.fieldNames = ['product_id', 'category', 'product_ids']; + this.params.res_id = undefined; + this.params.type = 'record'; + + var model = createModel({ + Model: BasicModel, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super(route, args); + }, + }); + + model.load(this.params).then(function (recordID) { + model.reload(recordID).then(function (recordID) { + assert.verifySteps(['default_get', 'default_get'], + "two default_get RPCs should have been done"); + var record = model.get(recordID); + assert.strictEqual(record.data.product_id, false, + "m2o default value should be false"); + assert.deepEqual(record.data.product_ids.data, [], + "o2m default should be []"); + assert.deepEqual(record.data.category.data, [], + "m2m default should be []"); + }); + }); + + model.destroy(); + }); + + });}); diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 31b1babc9b3..6aa01daac5a 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -789,7 +789,7 @@ QUnit.module('Views', { assert.strictEqual($event1Avatars.find('span').length, 0, "should not have a span for more attendees since there is only one"); - var $event2Avatars = calendar.$('.fc-event:contains(All the day) .o_calendar_avatars'); + var $event2Avatars = calendar.$('.fc-event:contains(All day) .o_calendar_avatars'); assert.strictEqual($event2Avatars.find('img').length, 3, "should have 3 avatars"); assert.strictEqual($event2Avatars.find('span').text(), '+2', "should indicate there are 2 more attendees that we don't show"); @@ -1034,6 +1034,46 @@ QUnit.module('Views', { "should display 5 events"); calendar.destroy(); }); + + QUnit.test('events starting at midnight', function (assert) { + assert.expect(2); + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: '', + viewOptions: { + initialDate: initialDate, + }, + }); + + // Click on Tuesday 12am + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); + var top = calendar.$('.fc-axis:contains(12am)').offset().top + 5; + var left = calendar.$('.fc-day:eq(2)').offset().left + 5; + try { + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + testUtils.triggerPositionalMouseEvent(left, top, "mouseup"); + } catch (e) { + calendar.destroy(); + $view.remove(); + throw new Error('The test failed to simulate a click on the screen.' + + 'Your screen is probably too small or your dev tools are open.'); + } + assert.ok($('.modal-dialog.modal-sm').length, + "should open the quick create dialog"); + + // Creating the event + $('.modal-body input:first').val('new event in quick create').trigger('input'); + $('.modal button.btn:contains(Create)').trigger('click').trigger('click'); + assert.strictEqual(calendar.$('.fc-event:contains(new event in quick create)').length, 1, + "should display the new record"); + + calendar.destroy(); + $view.remove(); + }); }); }); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 7eed1a2d100..26d15e33c2a 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -903,7 +903,7 @@ QUnit.module('Views', { testUtils.intercept(form, 'execute_action', function (event) { assert.strictEqual(event.data.action_data.name, "post", "should trigger execute_action with correct method name"); - assert.deepEqual(event.data.res_ids, [2], "should have correct id in event data"); + assert.deepEqual(event.data.env.currentID, 2, "should have correct id in event data"); event.data.on_success(); event.data.on_closed(); }); @@ -956,7 +956,7 @@ QUnit.module('Views', { testUtils.intercept(form, 'execute_action', function (event) { assert.step('execute_action'); - assert.deepEqual(event.data.res_ids, [resID], + assert.deepEqual(event.data.env.currentID, resID, "execute action should be done on correct record id"); event.data.on_success(); event.data.on_closed(); @@ -1006,7 +1006,7 @@ QUnit.module('Views', { testUtils.intercept(form, 'execute_action', function (event) { assert.step('execute_action'); - assert.deepEqual(event.data.res_ids, [resID], + assert.deepEqual(event.data.env.currentID, resID, "execute action should be done on correct record id"); event.data.on_success(); event.data.on_closed(); @@ -3597,8 +3597,9 @@ QUnit.module('Views', { }, intercepts: { execute_action: function (e) { - assert.deepEqual(e.data.action_data.context, {test: 2}, - "button context should have been evaluated and given to the action, without previous context"); + assert.deepEqual(e.data.action_data.context, { + 'test': 2 + }, "button context should have been evaluated and given to the action, with magicc without previous context"); }, }, }); @@ -3631,8 +3632,8 @@ QUnit.module('Views', { }, intercepts: { execute_action: function (e) { - assert.deepEqual(e.data.action_data.context, {}, - "button context should have been evaluated and given to the action, without previous context"); + assert.deepEqual(e.data.action_data.context, { + }, "button context should have been evaluated and given to the action, with magic keys but without previous context"); }, }, }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index f0370ac7cf8..2247a64fa3c 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -984,7 +984,7 @@ QUnit.module('Views', { }); QUnit.test('list view, editable, without data', function (assert) { - assert.expect(9); + assert.expect(11); this.data.foo.records = []; @@ -998,6 +998,7 @@ QUnit.module('Views', { '' + '' + '' + + '
RankNameRankName Performance % if line['suffix']: (${line['suffix']}) @@ -199,12 +200,12 @@ % set tdColor = 'gray' % set mutedColor = '#AAAAAA' % set tdPercentageColor = '#9A6C8E' - ${goal['rank']+1} + ${goal['rank']+1} ${goal['name']}${"%.2f" % goal['current']}
on ${"%.2f" % line['target']} +
${goal['name']}${"%.2f" % goal['current']}
on ${"%.2f" % line['target']}
${goal['completeness'] | int}%${goal['completeness'] | int}%