diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-01-14 10:34:50 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-01-14 10:34:50 +0100 |
| commit | 3cbc9dd6c034f8bbba8a75def07d063e3f625bdb (patch) | |
| tree | 55d7e9b940e27a1dad5415b6b721ab464d0bc418 /addons | |
| parent | 976429c8c69b2225e96ccb0e254ee4849832ed27 (diff) | |
| parent | 6a0675d36dd888daa4183f3d609ce334ed3cb493 (diff) | |
[MERGE] forward port branch 12.0 up to 6a0675d36dd
Diffstat (limited to 'addons')
75 files changed, 905 insertions, 172 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index d57fb164ee4..3981ed91640 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -667,8 +667,9 @@ class AccountBankStatementLine(models.Model): #create the res.partner.bank if needed if self.account_number and self.partner_id and not self.bank_account_id: - bank_account = self.env['res.partner.bank'].search( - [('acc_number', '=', self.account_number), ('partner_id', '=', self.partner_id.id)]) + # Search bank account without partner to handle the case the res.partner.bank already exists but is set + # on a different partner. + bank_account = self.env['res.partner.bank'].search([('acc_number', '=', self.account_number)]) if not bank_account: bank_account = self.env['res.partner.bank'].create({ 'acc_number': self.account_number, 'partner_id': self.partner_id.id diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index ef7c145e072..50760c25ff9 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -992,6 +992,7 @@ class AccountInvoice(models.Model): @api.multi def get_taxes_values(self): tax_grouped = {} + round_curr = self.currency_id.round for line in self.invoice_line_ids: if not line.account_id: continue @@ -1003,9 +1004,10 @@ class AccountInvoice(models.Model): if key not in tax_grouped: tax_grouped[key] = val + tax_grouped[key]['base'] = round_curr(val['base']) else: tax_grouped[key]['amount'] += val['amount'] - tax_grouped[key]['base'] += val['base'] + tax_grouped[key]['base'] += round_curr(val['base']) return tax_grouped @api.multi @@ -1734,17 +1736,17 @@ class AccountInvoiceLine(models.Model): self.price_unit = 0.0 domain['uom_id'] = [] else: + self_lang = self if part.lang: - product = self.product_id.with_context(lang=part.lang) - else: - product = self.product_id - + self_lang = self.with_context(lang=part.lang) + + product = self_lang.product_id account = self.get_invoice_line_account(type, product, fpos, company) if account: self.account_id = account.id self._set_taxes() - product_name = self._get_invoice_line_name_from_product() + product_name = self_lang._get_invoice_line_name_from_product() if product_name != None: self.name = product_name diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 7b038433905..b19485674a4 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -775,7 +775,7 @@ class AccountMoveLine(models.Model): (multiple_currency and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0): exchange_move_id = False # Eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates - if to_balance and any([residual for aml, residual in to_balance.values()]): + if to_balance and any([not float_is_zero(residual, precision_rounding=digits_rounding_precision) for aml, residual in to_balance.values()]): exchange_move = self.env['account.move'].create( self.env['account.full.reconcile']._prepare_exchange_diff_move(move_date=maxdate, company=amls[0].company_id)) part_reconcile = self.env['account.partial.reconcile'] @@ -804,7 +804,7 @@ class AccountMoveLine(models.Model): (debit_moves + credit_moves).read([field]) to_create = [] cash_basis = debit_moves and debit_moves[0].account_id.internal_type in ('receivable', 'payable') or False - cash_basis_percentage_before_rec = [] + cash_basis_percentage_before_rec = {} while (debit_moves and credit_moves): debit_move = debit_moves[0] credit_move = credit_moves[0] @@ -839,7 +839,7 @@ class AccountMoveLine(models.Model): if cash_basis: tmp_set = debit_move | credit_move - cash_basis_percentage_before_rec.append(tmp_set._get_matched_percentage()) + cash_basis_percentage_before_rec.update(tmp_set._get_matched_percentage()) to_create.append({ 'debit_move_id': debit_move.id, @@ -850,14 +850,12 @@ class AccountMoveLine(models.Model): }) part_rec = self.env['account.partial.reconcile'] - index = 0 with self.env.norecompute(): for partial_rec_dict in to_create: new_rec = self.env['account.partial.reconcile'].create(partial_rec_dict) part_rec += new_rec if cash_basis: - new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec[index]) - index += 1 + new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec) self.recompute() return debit_moves+credit_moves @@ -1468,10 +1466,14 @@ class AccountPartialReconcile(models.Model): #move_date is the max of the 2 reconciled items if move_date < move.date: move_date = move.date + percentage_before = percentage_before_rec[move.id] + percentage_after = move.line_ids[0]._get_matched_percentage()[move.id] + # update the percentage before as the move can be part of + # multiple partial reconciliations + percentage_before_rec[move.id] = percentage_after + for line in move.line_ids: if not line.tax_exigible: - percentage_before = percentage_before_rec[move.id] - percentage_after = line._get_matched_percentage()[move.id] #amount is the current cash_basis amount minus the one before the reconciliation amount = line.balance * percentage_after - line.balance * percentage_before rounded_amt = self._get_amount_tax_cash_basis(amount, line) @@ -1543,6 +1545,7 @@ class AccountPartialReconcile(models.Model): 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, 'partner_id': line.partner_id.id, }) + if newly_created_move: if move_date > (self.company_id.period_lock_date or date.min) and newly_created_move.date != move_date: # The move date should be the maximum date between payment and invoice (in case diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index e69aa30e431..c83d2b31ed3 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -736,12 +736,15 @@ class account_payment(models.Model): """ Return dict to create the payment move """ journal = journal or self.journal_id - return { + move_vals = { 'date': self.payment_date, 'ref': self.communication or '', 'company_id': self.company_id.id, 'journal_id': journal.id, } + if self.move_name: + move_vals['name'] = self.move_name + return move_vals def _get_shared_move_line_vals(self, debit, credit, amount_currency, move_id, invoice_id=False): """ Returns values common to both move lines (except for debit, credit and amount_currency which are reversed) diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index a192b2210af..5e756f28247 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -903,11 +903,11 @@ var StatementModel = BasicModel.extend({ }); var company_currency = session.get_currency(line.st_line.currency_id); var company_precision = company_currency && company_currency.digits[1] || 2; - total = utils.round_precision(total*1000, company_precision)/1000 || 0; + total = utils.round_decimals(total*1000, company_precision)/1000 || 0; if(isOtherCurrencyId){ var other_currency = session.get_currency(isOtherCurrencyId); var other_precision = other_currency && other_currency.digits[1] || 2; - amount_currency = utils.round_precision(amount_currency, other_precision) + amount_currency = utils.round_decimals(amount_currency, other_precision) } line.balance = { amount: total, diff --git a/addons/account/tests/test_account_customer_invoice.py b/addons/account/tests/test_account_customer_invoice.py index a6b3459c9b9..83822ef8ffb 100644 --- a/addons/account/tests/test_account_customer_invoice.py +++ b/addons/account/tests/test_account_customer_invoice.py @@ -103,3 +103,66 @@ class TestAccountCustomerInvoice(AccountTestUsers): # I clicked on Add Credit Note button. self.account_invoice_refund_0.invoice_refund() + + def test_customer_invoice_tax(self): + + self.env.user.company_id.tax_calculation_rounding_method = 'round_globally' + + payment_term = self.env.ref('account.account_payment_term_advance') + journalrec = self.env['account.journal'].search([('type', '=', 'sale')])[0] + partner3 = self.env.ref('base.res_partner_3') + account_id = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id + + tax = self.env['account.tax'].create({ + 'name': 'Tax 15.0', + 'amount': 15.0, + 'amount_type': 'percent', + 'type_tax_use': 'sale', + }) + + invoice_line_data = [ + (0, 0, + { + 'product_id': self.env.ref('product.product_product_1').id, + 'quantity': 40.0, + 'account_id': account_id, + 'name': 'product test 1', + 'discount' : 10.00, + 'price_unit': 2.27, + 'invoice_line_tax_ids': [(6, 0, [tax.id])], + } + ), + (0, 0, + { + 'product_id': self.env.ref('product.product_product_2').id, + 'quantity': 21.0, + 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id, + 'name': 'product test 2', + 'discount' : 10.00, + 'price_unit': 2.77, + 'invoice_line_tax_ids': [(6, 0, [tax.id])], + } + ), + (0, 0, + { + 'product_id': self.env.ref('product.product_product_3').id, + 'quantity': 21.0, + 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id, + 'name': 'product test 3', + 'discount' : 10.00, + 'price_unit': 2.77, + 'invoice_line_tax_ids': [(6, 0, [tax.id])], + } + ) + ] + + invoice = self.env['account.invoice'].create(dict( + name="Test Customer Invoice", + reference_type="none", + payment_term_id=payment_term.id, + journal_id=journalrec.id, + partner_id=partner3.id, + invoice_line_ids=invoice_line_data + )) + + self.assertEquals(invoice.amount_untaxed, sum([x.base for x in invoice.tax_line_ids])) diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index be446c818a5..970989a970f 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -49,6 +49,58 @@ class TestReconciliation(AccountingTestCase): 'payment_type': 'inbound', }) + self.expense_account = self.env['account.account'].create({ + 'name': 'EXP', + 'code': 'EXP', + 'user_type_id': self.env.ref('account.data_account_type_expenses').id, + 'company_id': company.id, + }) + # cash basis intermediary account + self.tax_waiting_account = self.env['account.account'].create({ + 'name': 'TAX_WAIT', + 'code': 'TWAIT', + 'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id, + 'reconcile': True, + 'company_id': company.id, + }) + # cash basis final account + self.tax_final_account = self.env['account.account'].create({ + 'name': 'TAX_TO_DEDUCT', + 'code': 'TDEDUCT', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + 'company_id': company.id, + }) + self.tax_base_amount_account = self.env['account.account'].create({ + 'name': 'TAX_BASE', + 'code': 'TBASE', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + 'company_id': company.id, + }) + + # Journals + self.purchase_journal = self.env['account.journal'].create({ + 'name': 'purchase', + 'code': 'PURCH', + 'type': 'purchase', + }) + self.cash_basis_journal = self.env['account.journal'].create({ + 'name': 'CABA', + 'code': 'CABA', + 'type': 'general', + }) + + # Tax Cash Basis + self.tax_cash_basis = self.env['account.tax'].create({ + 'name': 'cash basis 20%', + 'type_tax_use': 'purchase', + 'company_id': company.id, + 'amount': 20, + 'account_id': self.tax_waiting_account.id, + 'tax_exigibility': 'on_payment', + 'cash_basis_account_id': self.tax_final_account.id, + 'cash_basis_base_account_id': self.tax_base_amount_account.id, + }) + def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None): #we create an invoice in given currency invoice = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, @@ -1107,3 +1159,288 @@ class TestReconciliation(AccountingTestCase): receivable_lines = invoice.move_id.line_ids.filtered(lambda line: line.account_id == self.account_rcv).sorted('date_maturity')[0] self.assertTrue(receivable_lines.matched_credit_ids) + + def test_reconciliation_cash_basis01(self): + # Simulates an expense made up by 2 lines + # one is subject to a cash basis tax + # the other is not subject to tax + + company = self.env.ref('base.main_company') + company.tax_cash_basis_journal_id = self.cash_basis_journal + + AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False) + + # Purchase + purchase_move = self.env['account.move'].create({ + 'name': 'purchase', + 'journal_id': self.purchase_journal.id, + }) + + purchase_payable_line0 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'credit': 100, + 'move_id': purchase_move.id, + }) + purchase_payable_line1 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'credit': 50, + 'move_id': purchase_move.id, + }) + AccountMoveLine.create({ + 'name': 'expensNoTax', + 'account_id': self.expense_account.id, + 'debit': 50, + 'move_id': purchase_move.id, + }) + AccountMoveLine.create({ + 'name': 'expenseTaxed', + 'account_id': self.expense_account.id, + 'debit': 83.33, + 'move_id': purchase_move.id, + 'tax_ids': [(4, self.tax_cash_basis.id, False)], + }) + tax_line = AccountMoveLine.create({ + 'name': 'TaxLine', + 'account_id': self.tax_waiting_account.id, + 'debit': 16.67, + 'move_id': purchase_move.id, + 'tax_line_id': self.tax_cash_basis.id, + }) + purchase_move.post() + + # Payment Move + payment_move = self.env['account.move'].create({ + 'name': 'payment', + 'journal_id': self.bank_journal_euro.id, + }) + payment_payable_line = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'debit': 150, + 'move_id': payment_move.id, + }) + AccountMoveLine.create({ + 'account_id': self.account_euro.id, + 'credit': 150, + 'move_id': payment_move.id, + }) + payment_move.post() + + to_reconcile = (purchase_move + payment_move).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable') + to_reconcile.reconcile() + + cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)]) + + self.assertEqual(len(cash_basis_moves), 2) + self.assertTrue(cash_basis_moves.exists()) + + # check reconciliation in Payable account + self.assertTrue(purchase_payable_line0.full_reconcile_id.exists()) + self.assertEqual(purchase_payable_line0.full_reconcile_id.reconciled_line_ids, + purchase_payable_line0 + purchase_payable_line1 + payment_payable_line) + + cash_basis_aml_ids = cash_basis_moves.mapped('line_ids') + # check reconciliation in the tax waiting account + self.assertTrue(tax_line.full_reconcile_id.exists()) + self.assertEqual(tax_line.full_reconcile_id.reconciled_line_ids, + cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + tax_line) + + self.assertEqual(len(cash_basis_aml_ids), 8) + + # check amounts + cash_basis_move1 = cash_basis_moves.filtered(lambda m: m.amount == 33.34) + cash_basis_move2 = cash_basis_moves.filtered(lambda m: m.amount == 66.66) + + self.assertTrue(cash_basis_move1.exists()) + self.assertTrue(cash_basis_move2.exists()) + + # For first move + move_lines = cash_basis_move1.line_ids + base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account) + self.assertEqual(len(base_amount_tax_lines), 2) + self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 27.78) + self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 27.78) + + self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit, + 5.56) + self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit, + 5.56) + + # For second move + move_lines = cash_basis_move2.line_ids + base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account) + self.assertEqual(len(base_amount_tax_lines), 2) + self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 55.55) + self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 55.55) + + self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit, + 11.11) + self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit, + 11.11) + + def test_reconciliation_cash_basis02(self): + # Simulates an invoice made up by 2 lines + # both subjected to cash basis taxes + # with 2 payment terms + # And partial payment not martching any payment term + + company = self.env.ref('base.main_company') + company.tax_cash_basis_journal_id = self.cash_basis_journal + tax_cash_basis10percent = self.tax_cash_basis.copy({'amount': 10}) + tax_waiting_account10 = self.tax_waiting_account.copy({ + 'name': 'TAX WAIT 10', + 'code': 'TWAIT1', + }) + + + AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False) + + # Purchase + purchase_move = self.env['account.move'].create({ + 'name': 'invoice', + 'journal_id': self.purchase_journal.id, + }) + + purchase_payable_line0 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'credit': 105, + 'move_id': purchase_move.id, + }) + purchase_payable_line1 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'credit': 50, + 'move_id': purchase_move.id, + }) + AccountMoveLine.create({ + 'name': 'expenseTaxed 10%', + 'account_id': self.expense_account.id, + 'debit': 50, + 'move_id': purchase_move.id, + 'tax_ids': [(4, tax_cash_basis10percent.id, False)], + }) + tax_line0 = AccountMoveLine.create({ + 'name': 'TaxLine0', + 'account_id': tax_waiting_account10.id, + 'debit': 5, + 'move_id': purchase_move.id, + 'tax_line_id': tax_cash_basis10percent.id, + }) + AccountMoveLine.create({ + 'name': 'expenseTaxed 20%', + 'account_id': self.expense_account.id, + 'debit': 83.33, + 'move_id': purchase_move.id, + 'tax_ids': [(4, self.tax_cash_basis.id, False)], + }) + tax_line1 = AccountMoveLine.create({ + 'name': 'TaxLine1', + 'account_id': self.tax_waiting_account.id, + 'debit': 16.67, + 'move_id': purchase_move.id, + 'tax_line_id': self.tax_cash_basis.id, + }) + purchase_move.post() + + # Payment Move + payment_move0 = self.env['account.move'].create({ + 'name': 'payment', + 'journal_id': self.bank_journal_euro.id, + }) + payment_payable_line0 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'debit': 40, + 'move_id': payment_move0.id, + }) + AccountMoveLine.create({ + 'account_id': self.account_euro.id, + 'credit': 40, + 'move_id': payment_move0.id, + }) + payment_move0.post() + + # Payment Move + payment_move1 = self.env['account.move'].create({ + 'name': 'payment', + 'journal_id': self.bank_journal_euro.id, + }) + payment_payable_line1 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'debit': 115, + 'move_id': payment_move1.id, + }) + AccountMoveLine.create({ + 'account_id': self.account_euro.id, + 'credit': 115, + 'move_id': payment_move1.id, + }) + payment_move1.post() + + (purchase_move + payment_move0).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable').reconcile() + (purchase_move + payment_move1).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable').reconcile() + + cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)]) + + self.assertEqual(len(cash_basis_moves), 3) + self.assertTrue(cash_basis_moves.exists()) + + # check reconciliation in Payable account + self.assertTrue(purchase_payable_line0.full_reconcile_id.exists()) + self.assertEqual(purchase_payable_line0.full_reconcile_id.reconciled_line_ids, + purchase_payable_line0 + purchase_payable_line1 + payment_payable_line0 + payment_payable_line1) + + cash_basis_aml_ids = cash_basis_moves.mapped('line_ids') + + # check reconciliation in the tax waiting account + self.assertTrue(tax_line0.full_reconcile_id.exists()) + self.assertEqual(tax_line0.full_reconcile_id.reconciled_line_ids, + cash_basis_aml_ids.filtered(lambda l: l.account_id == tax_waiting_account10) + tax_line0) + + self.assertTrue(tax_line1.full_reconcile_id.exists()) + self.assertEqual(tax_line1.full_reconcile_id.reconciled_line_ids, + cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + tax_line1) + + self.assertEqual(len(cash_basis_aml_ids), 24) + + # check amounts + expected_move_amounts = [ + {'base_20': 56.45, 'tax_20': 11.29, 'base_10': 33.87, 'tax_10': 3.39}, + {'base_20': 21.50, 'tax_20': 4.30, 'base_10': 12.90, 'tax_10': 1.29}, + {'base_20': 5.38, 'tax_20': 1.08, 'base_10': 3.23, 'tax_10': 0.32}, + ] + + index = 0 + for cb_move in cash_basis_moves.sorted('amount', reverse=True): + expected = expected_move_amounts[index] + move_lines = cb_move.line_ids + base_amount_tax_lines20per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '20%' in l.name) + base_amount_tax_lines10per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '10%' in l.name) + self.assertEqual(len(base_amount_tax_lines20per), 2) + + self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('credit')), expected['base_20']) + self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('debit')), expected['base_20']) + + self.assertEqual(len(base_amount_tax_lines10per), 2) + self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('credit')), expected['base_10']) + self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('debit')), expected['base_10']) + + self.assertAlmostEqual( + (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per) + .filtered(lambda l: l.account_id == self.tax_waiting_account).credit, + expected['tax_20'] + ) + self.assertAlmostEqual( + (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per) + .filtered(lambda l: 'TaxLine1' in l.name).debit, + expected['tax_20'] + ) + + self.assertAlmostEqual( + (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per) + .filtered(lambda l: l.account_id == tax_waiting_account10).credit, + expected['tax_10'] + ) + self.assertAlmostEqual( + (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per) + .filtered(lambda l: 'TaxLine0' in l.name).debit, + expected['tax_10'] + ) + index += 1 diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 45696253ffa..15219345394 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -291,7 +291,7 @@ <field name="inbound_payment_method_ids" widget="many2many_checkboxes"/> <field name="outbound_payment_method_ids" widget="many2many_checkboxes"/> </group> - <group string="Accounting App Options" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase', 'bank', 'cash'])]}"> + <group string="Accounting App Options"> <field name="profit_account_id" attrs="{'invisible': [('type', '!=', 'cash')]}"/> <field name="loss_account_id" attrs="{'invisible': [('type', '!=', 'cash')]}"/> <field name="group_invoice_lines" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}"/> diff --git a/addons/barcodes/static/src/js/barcode_field.js b/addons/barcodes/static/src/js/barcode_field.js index ca5baa90b49..d8a7a1c853e 100644 --- a/addons/barcodes/static/src/js/barcode_field.js +++ b/addons/barcodes/static/src/js/barcode_field.js @@ -4,7 +4,7 @@ odoo.define('barcodes.field', function(require) { var AbstractField = require('web.AbstractField'); var basicFields = require('web.basic_fields'); var fieldRegistry = require('web.field_registry'); -var is_special_key = require('barcodes.BarcodeEvents').BarcodeEvents.is_special_key; +var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents; // Field in which the user can both type normally and scan barcodes @@ -42,7 +42,7 @@ var FieldFloatScannable = basicFields.FieldFloat.extend({ _onKeypress: function (e) { /* only simulate a keypress if it has been previously prevented */ if (e.dispatched_by_barcode_reader !== true) { - if (!is_special_key(e)) { + if (!BarcodeEvents.is_special_key(e)) { e.preventDefault(); } return; diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index eb969156b09..accc102c2b7 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -16,8 +16,13 @@ class StockQuantPackage(models.Model): @api.depends('quant_ids') def _compute_weight(self): weight = 0.0 - for quant in self.quant_ids: - weight += quant.quantity * quant.product_id.weight + if self.env.context.get('picking_id'): + current_picking_move_line_ids = self.env['stock.move.line'].search([('result_package_id', '=', self.id), ('picking_id', '=', self.env.context['picking_id'])]) + for ml in current_picking_move_line_ids: + weight += ml.product_uom_id._compute_quantity(ml.qty_done,ml.product_id.uom_id) * ml.product_id.weight + else: + for quant in self.quant_ids: + weight += quant.quantity * quant.product_id.weight self.weight = weight def _get_default_weight_uom(self): diff --git a/addons/hr_holidays/i18n/hr_holidays.pot b/addons/hr_holidays/i18n/hr_holidays.pot index 15161b395a5..68e174d4053 100644 --- a/addons/hr_holidays/i18n/hr_holidays.pot +++ b/addons/hr_holidays/i18n/hr_holidays.pot @@ -659,6 +659,11 @@ msgid "Analyze from" msgstr "" #. module: hr_holidays +#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave_type__double_validation +msgid "Apply Double Validation" +msgstr "" + +#. module: hr_holidays #: model_terms:ir.ui.view,arch_db:hr_holidays.view_evaluation_report_graph msgid "Appraisal Analysis" msgstr "" diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 31aab06c21a..24062454dca 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -427,8 +427,7 @@ class HolidaysRequest(models.Model): employee = self.env['hr.employee'].browse(employee_id) return employee.get_work_days_data(date_from, date_to)['days'] - time_delta = date_to - date_from - return math.ceil(time_delta.days + float(time_delta.seconds) / 86400) + return self.env.user.company_id.resource_calendar_id.get_work_hours_count() / HOURS_PER_DAY #################################################### # ORM Overrides methods @@ -612,7 +611,7 @@ class HolidaysRequest(models.Model): 'date_from': self.date_from, 'date_to': self.date_to, 'notes': self.notes, - 'number_of_days': self.number_of_days, + 'number_of_days': employee.get_work_days_data(self.date_from, self.date_to)['days'], 'parent_id': self.id, 'employee_id': employee.id } diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index af3baf9830b..c542ea14909 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -67,6 +67,14 @@ class HolidaysType(models.Model): ('hr', 'Payroll Officer'), ('manager', 'Team Leader'), ('both', 'Team Leader and Payroll Officer')], default='hr', string='Validation By') + # TODO: remove me in master, the behavior is exactly the same if you choose 'hr' or 'manager' + # in the validation_type field. This field is used only to hide this possibility to the user + # to avoid misunderstandings. This field and its corresponding's functions must be removed once + # the functional part is implemented. + double_validation = fields.Boolean(string='Apply Double Validation', + compute='_compute_validation_type', inverse='_inverse_validation_type', + help="When selected, the Allocation/Leave Requests for this type require a second validation to be approved.") + allocation_type = fields.Selection([ ('fixed', 'Fixed by HR'), ('fixed_allocation', 'Fixed by HR + allocation request'), @@ -88,6 +96,25 @@ class HolidaysType(models.Model): leave_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Leave Notification Subtype') allocation_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Allocation Notification Subtype') + # TODO: remove me in master + @api.depends('validation_type') + def _compute_validation_type(self): + for holiday_type in self: + if holiday_type.validation_type == 'both': + holiday_type.double_validation = True + else: + holiday_type.double_validation = False + + # TODO: remove me in master + def _inverse_validation_type(self): + for holiday_type in self: + if holiday_type.double_validation == True: + holiday_type.validation_type = 'both' + else: + #IF to preserve the information (hr or manager) + if holiday_type.validation_type == 'both': + holiday_type.validation_type = 'hr' + @api.multi @api.constrains('validity_start', 'validity_stop') def _check_validity_dates(self): diff --git a/addons/hr_holidays/views/hr_leave_type_views.xml b/addons/hr_holidays/views/hr_leave_type_views.xml index 64562ab1f05..c7d48c1d319 100644 --- a/addons/hr_holidays/views/hr_leave_type_views.xml +++ b/addons/hr_holidays/views/hr_leave_type_views.xml @@ -49,7 +49,8 @@ <field name="time_type" groups="base.group_no_one"/> </group> <group name="validation" string="Validation"> - <field name="validation_type" widget="radio"/> + <field name="validation_type" widget="radio" invisible="1"/> + <field name="double_validation"/> </group> <group name="visibility" string="Allocation"> <field name="allocation_type" widget="radio" force_save="1"/> @@ -110,7 +111,8 @@ <field name="display_name"/> <field name="code"/> <field name="allocation_type"/> - <field name="validation_type"/> + <field name="validation_type" invisible="1"/> + <field name="double_validation"/> <field name="validity_start"/> <field name="validity_stop"/> </tree> diff --git a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py index e5e65390304..a224be4ce9a 100644 --- a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py +++ b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py @@ -144,13 +144,13 @@ class HrContract(models.Model): ) @api.depends('yearly_cost_before_charges', 'social_security_contributions', 'wage', - 'social_security_contributions', 'double_holidays', 'warrants_cost', 'meal_voucher_paid_by_employer') + 'social_security_contributions', 'warrants_cost', 'meal_voucher_paid_by_employer') def _compute_final_yearly_costs(self): for contract in self: contract.final_yearly_costs = ( contract.yearly_cost_before_charges + contract.social_security_contributions + - contract.double_holidays + + contract.wage * 0.92 + contract.warrants_cost + (220.0 * contract.meal_voucher_paid_by_employer) ) @@ -197,11 +197,11 @@ class HrContract(models.Model): for contract in self: contract.monthly_yearly_costs = contract.final_yearly_costs / 12.0 - @api.depends('wage') + @api.depends('wage_with_holidays') def _compute_holidays_advantages(self): for contract in self: - contract.double_holidays = contract.wage * 0.92 - contract.thirteen_month = contract.wage + contract.double_holidays = contract.wage_with_holidays * 0.92 + contract.thirteen_month = contract.wage_with_holidays @api.onchange('transport_mode_car', 'transport_mode_public', 'transport_mode_others') def _onchange_transport_mode(self): diff --git a/addons/l10n_nl/data/account_fiscal_position_tax_template.xml b/addons/l10n_nl/data/account_fiscal_position_tax_template.xml index c026f355bbb..e59c852ef24 100644 --- a/addons/l10n_nl/data/account_fiscal_position_tax_template.xml +++ b/addons/l10n_nl/data/account_fiscal_position_tax_template.xml @@ -112,7 +112,7 @@ <record id="position_tax_intracom_2_9" model="account.fiscal.position.tax.template"> <field name="position_id" ref="fiscal_position_template_eu"/> <field name="tax_src_id" ref="btw_9"/> - <field name="tax_dest_id" ref="btw_X0"/> + <field name="tax_dest_id" ref="btw_X0_diensten"/> </record> <record id="position_tax_intracom_3" model="account.fiscal.position.tax.template"> <field name="position_id" ref="fiscal_position_template_eu"/> diff --git a/addons/mail/static/src/js/models/mail_failure.js b/addons/mail/static/src/js/models/mail_failure.js index 390b195f8d9..438430c752f 100644 --- a/addons/mail/static/src/js/models/mail_failure.js +++ b/addons/mail/static/src/js/models/mail_failure.js @@ -83,7 +83,7 @@ var MailFailure = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { documentModel: this._documentModel, id: 'mail_failure', imageSRC: this._moduleIcon, - title: this._recordName, + title: this._modelName, }; return preview; }, diff --git a/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js b/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js index 9a5b5abb4cc..f995b4e09b3 100644 --- a/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js +++ b/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js @@ -94,17 +94,17 @@ var ThreadTypingMixin = { * Get the text to display when some partners are typing something on the * thread: * - * - single typing partner: + * - single typing partner: * - * A is typing... + * A is typing... * - * - two typing partners: + * - two typing partners: * - * A and B are typing... + * A and B are typing... * - * - three or more typing partners: + * - three or more typing partners: * - * A, B and more are typing... + * A, B and more are typing... * * The choice of the members name for display is not random: it displays * the user that have been typing for the longest time. Also, this function diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index 140266285c7..b59d7b91434 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -490,7 +490,7 @@ MailManager.include({ var self = this; var partnerID = data.partner_id; this.call('bus_service', 'sendNotification', data.title, data.message, function ( ){ - self.call('mail_service', 'openDmWindow', partnerID); + self.call('mail_service', 'openDMChatWindowFromBlankThreadWindow', partnerID); }); }, /** diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js index 64cf6bb88c9..c8e05957afb 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -531,9 +531,6 @@ MailManager.include({ * @private */ _repositionThreadWindows: function () { - if (this._areAllThreadWindowsHidden()) { - return; - } this._computeAvailableSlotsForThreadWindows(); this._repositionVisibleThreadWindows(); @@ -575,9 +572,6 @@ MailManager.include({ * the focus is on the thread window. */ _updateThreadWindowsFromMessage: function (message, options) { - if (this._areAllThreadWindowsHidden()) { - return; - } _.each(this._threadWindows, function (threadWindow) { if (_.contains(message.getThreadIDs(), threadWindow.getID())) { threadWindow.update(options); diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js index cd5ffa7db94..57907a55943 100644 --- a/addons/mail/static/src/js/thread_windows/thread_window.js +++ b/addons/mail/static/src/js/thread_windows/thread_window.js @@ -132,9 +132,9 @@ var ThreadWindow = AbstractThreadWindow.extend({ * * Do not display the input in the following cases: * - * - no thread related to this window - * - window of a mailbox (temp: let us have mailboxes in window mode) - * - window of a thread with mass mailing + * - no thread related to this window + * - window of a mailbox (temp: let us have mailboxes in window mode) + * - window of a thread with mass mailing * * Any other threads show the input in the window. * diff --git a/addons/mrp/static/src/js/mrp_bom_report.js b/addons/mrp/static/src/js/mrp_bom_report.js index 76cd0b53fd8..8a96c5d6d67 100644 --- a/addons/mrp/static/src/js/mrp_bom_report.js +++ b/addons/mrp/static/src/js/mrp_bom_report.js @@ -25,7 +25,8 @@ var MrpBomReport = stock_report_generic.extend({ return this._rpc({ model: 'report.mrp.report_bom_structure', method: 'get_html', - args: args + args: args, + context: this.given_context, }) .then(function (result) { self.data = result; diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js index 50872153d8c..74d32c74acc 100644 --- a/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js +++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js @@ -112,7 +112,7 @@ var PartnerAutocompleteMixin = { var self = this; var removeUselessFields = function (company) { - var fields = 'label,description,domain,logo,legal_name'.split(','); + var fields = 'label,description,domain,logo,legal_name,ignored'.split(','); fields.forEach(function (field) { delete company[field]; }); diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 9fa51724678..afed11da38e 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -871,24 +871,25 @@ class PaymentTransaction(models.Model): def create(self, values): # call custom create method if defined (i.e. ogone_create for ogone) acquirer = self.env['payment.acquirer'].browse(values['acquirer_id']) - partner = self.env['res.partner'].browse(values['partner_id']) + if values.get('partner_id'): + partner = self.env['res.partner'].browse(values['partner_id']) - values.update({ - 'partner_name': partner.name, - 'partner_lang': partner.lang or 'en_US', - 'partner_email': partner.email, - 'partner_zip': partner.zip, - 'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''), - 'partner_city': partner.city, - 'partner_country_id': partner.country_id.id or self._get_default_partner_country_id(), - 'partner_phone': partner.phone, - }) + values.update({ + 'partner_name': partner.name, + 'partner_lang': partner.lang or 'en_US', + 'partner_email': partner.email, + 'partner_zip': partner.zip, + 'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''), + 'partner_city': partner.city, + 'partner_country_id': partner.country_id.id or self._get_default_partner_country_id(), + 'partner_phone': partner.phone, + }) # compute fees custom_method_name = '%s_compute_fees' % acquirer.provider if hasattr(acquirer, custom_method_name): fees = getattr(acquirer, custom_method_name)( - values.get('amount', 0.0), values.get('currency_id'), partner.country_id.id) + values.get('amount', 0.0), values.get('currency_id'), values['partner_country_id']) values['fees'] = fees # custom create diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index e4bd3382caa..bca2164dac1 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -149,7 +149,7 @@ class AuthorizeAPI(): root = self._base_tree('createCustomerProfileRequest') profile = etree.SubElement(root, "profile") etree.SubElement(profile, "merchantCustomerId").text = 'ODOO-%s-%s' % (partner.id, uuid4().hex[:8]) - etree.SubElement(profile, "email").text = partner.email + etree.SubElement(profile, "email").text = partner.email or '' payment_profile = etree.SubElement(profile, "paymentProfiles") etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual' billTo = etree.SubElement(payment_profile, "billTo") diff --git a/addons/payment_authorize/tests/test_authorize.py b/addons/payment_authorize/tests/test_authorize.py index 314d4bdd26a..a45275b07d8 100644 --- a/addons/payment_authorize/tests/test_authorize.py +++ b/addons/payment_authorize/tests/test_authorize.py @@ -188,7 +188,7 @@ class AuthorizeForm(AuthorizeCommon): self.assertEqual(tx.state, 'cancel', 'Authorize: erroneous validation did not put tx into error state') -@odoo.tests.tagged('post_install', '-at_install', '-standard', 'external') +@odoo.tests.tagged('post_install', '-at_install', '-standard') class AuthorizeForm(AuthorizeCommon): def test_30_authorize_s2s(self): # be sure not to do stupid thing diff --git a/addons/payment_buckaroo/tests/test_buckaroo.py b/addons/payment_buckaroo/tests/test_buckaroo.py index 925385f402f..e23b1f0c002 100644 --- a/addons/payment_buckaroo/tests/test_buckaroo.py +++ b/addons/payment_buckaroo/tests/test_buckaroo.py @@ -32,12 +32,12 @@ class BuckarooForm(BuckarooCommon): # ---------------------------------------- form_values = { - 'add_returndata': None, + 'add_returndata': '', 'Brq_websitekey': self.buckaroo.brq_websitekey, 'Brq_amount': '2240.0', 'Brq_currency': 'EUR', 'Brq_invoicenumber': 'SO004', - 'Brq_signature': '1b8c10074c622d965272a91a9e88b5b3777d2474', # update me + 'Brq_signature': 'aa6fe072afdd9d1b463d55f43e6df4b272d3529a', # update me 'brq_test': 'True', 'Brq_return': urls.url_join(base_url, BuckarooController._return_url), 'Brq_returncancel': urls.url_join(base_url, BuckarooController._cancel_url), @@ -50,13 +50,14 @@ class BuckarooForm(BuckarooCommon): res = self.buckaroo.render( 'SO004', 2240.0, self.currency_euro.id, partner_id=None, - partner_values=self.buyer_values) - + values=self.buyer_values) # check form result tree = objectify.fromstring(res) - self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') + data_set = tree.xpath("//input[@name='data_set']") + self.assertEqual(len(data_set), 1, 'paypal: Found %d "data_set" input instead of 1' % len(data_set)) + self.assertEqual(data_set[0].get('data-action-url'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') for form_input in tree.input: - if form_input.get('name') in ['submit']: + if form_input.get('name') in ['submit', 'data_set']: continue self.assertEqual( form_input.get('value'), @@ -78,17 +79,18 @@ class BuckarooForm(BuckarooCommon): }) # render the button - res = self.buckaroo_id.render( + res = self.buckaroo.render( 'should_be_erased', 2240.0, self.currency_euro, - tx_id=tx.id, partner_id=None, - partner_values=self.buyer_values) + values=self.buyer_values) # check form result tree = objectify.fromstring(res) - self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') + data_set = tree.xpath("//input[@name='data_set']") + self.assertEqual(len(data_set), 1, 'paypal: Found %d "data_set" input instead of 1' % len(data_set)) + self.assertEqual(data_set[0].get('data-action-url'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') for form_input in tree.input: - if form_input.get('name') in ['submit']: + if form_input.get('name') in ['submit', 'data_set']: continue self.assertEqual( form_input.get('value'), @@ -116,7 +118,7 @@ class BuckarooForm(BuckarooCommon): 'BRQ_SERVICE_PAYPAL_PAYERLASTNAME': u'Tester', 'BRQ_SERVICE_PAYPAL_PAYERMIDDLENAME': u'de', 'BRQ_SERVICE_PAYPAL_PAYERSTATUS': u'verified', - 'Brq_signature': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91', + 'Brq_signature': u'9e75e51cbcb6fbc798b40d1787cc5b7c36557d56', 'BRQ_STATUSCODE': u'190', 'BRQ_STATUSCODE_DETAIL': u'S001', 'BRQ_STATUSMESSAGE': u'Transaction successfully processed', @@ -134,7 +136,7 @@ class BuckarooForm(BuckarooCommon): 'amount': 2240.0, 'acquirer_id': self.buckaroo.id, 'currency_id': self.currency_euro.id, - 'reference': 'SO004-1', + 'reference': 'SO004', 'partner_name': 'Norbert Buyer', 'partner_country_id': self.country_france.id}) @@ -153,14 +155,14 @@ class BuckarooForm(BuckarooCommon): 'partner_name': 'Norbert Buyer', 'partner_country_id': self.country_france.id}) + buckaroo_post_data['BRQ_INVOICENUMBER'] = 'SO004-2' # now buckaroo post is ok: try to modify the SHASIGN - buckaroo_post_data['BRQ_SIGNATURE'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a' + buckaroo_post_data['Brq_signature'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a' with self.assertRaises(ValidationError): tx.form_feedback(buckaroo_post_data, 'buckaroo') - # simulate an error - buckaroo_post_data['BRQ_STATUSCODE'] = 2 - buckaroo_post_data['BRQ_SIGNATURE'] = '4164b52adb1e6a2221d3d8a39d8c3e18a9ecb90b' + buckaroo_post_data['BRQ_STATUSCODE'] = '2' + buckaroo_post_data['Brq_signature'] = '9138e2bf09a708a4eb485ce7777e5406898b689d' tx.form_feedback(buckaroo_post_data, 'buckaroo') # check state diff --git a/addons/payment_buckaroo/views/payment_buckaroo_templates.xml b/addons/payment_buckaroo/views/payment_buckaroo_templates.xml index 95f1aa0ce9e..7b0749923c9 100644 --- a/addons/payment_buckaroo/views/payment_buckaroo_templates.xml +++ b/addons/payment_buckaroo/views/payment_buckaroo_templates.xml @@ -2,24 +2,26 @@ <odoo> <data noupdate="1"> <template id="buckaroo_form"> - <input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/> - <input type="hidden" name="Brq_websitekey" t-att-value="Brq_websitekey"/> - <input type="hidden" name="Brq_amount" t-att-value="Brq_amount"/> - <input type="hidden" name="Brq_currency" t-att-value="Brq_currency"/> - <input type="hidden" name="Brq_invoicenumber" t-att-value="Brq_invoicenumber"/> - <input type="hidden" name="Brq_signature" t-att-value="Brq_signature"/> - <input type="hidden" name="brq_test" t-att-value="brq_test"/> - <input type="hidden" name="Brq_culture" t-att-value="Brq_culture"/> - <!-- URLs --> - <input t-if="Brq_return" type="hidden" name='Brq_return' - t-att-value="Brq_return"/> - <input t-if="Brq_returncancel" type="hidden" name='Brq_returncancel' - t-att-value="Brq_returncancel"/> - <input t-if="Brq_returnerror" type="hidden" name='Brq_returnerror' - t-att-value="Brq_returnerror"/> - <input t-if="Brq_returnreject" type="hidden" name='Brq_returnreject' - t-att-value="Brq_returnreject"/> - <input type="hidden" name='add_returndata' t-att-value="add_returndata"/> + <div> + <input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/> + <input type="hidden" name="Brq_websitekey" t-att-value="Brq_websitekey"/> + <input type="hidden" name="Brq_amount" t-att-value="Brq_amount"/> + <input type="hidden" name="Brq_currency" t-att-value="Brq_currency"/> + <input type="hidden" name="Brq_invoicenumber" t-att-value="Brq_invoicenumber"/> + <input type="hidden" name="Brq_signature" t-att-value="Brq_signature"/> + <input type="hidden" name="brq_test" t-att-value="brq_test"/> + <input type="hidden" name="Brq_culture" t-att-value="Brq_culture"/> + <!-- URLs --> + <input t-if="Brq_return" type="hidden" name='Brq_return' + t-att-value="Brq_return"/> + <input t-if="Brq_returncancel" type="hidden" name='Brq_returncancel' + t-att-value="Brq_returncancel"/> + <input t-if="Brq_returnerror" type="hidden" name='Brq_returnerror' + t-att-value="Brq_returnerror"/> + <input t-if="Brq_returnreject" type="hidden" name='Brq_returnreject' + t-att-value="Brq_returnreject"/> + <input type="hidden" name='add_returndata' t-att-value="add_returndata"/> + </div> </template> </data> </odoo> diff --git a/addons/payment_paypal/tests/test_paypal.py b/addons/payment_paypal/tests/test_paypal.py index b43aed25fcf..e70129f9333 100644 --- a/addons/payment_paypal/tests/test_paypal.py +++ b/addons/payment_paypal/tests/test_paypal.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from odoo import fields from odoo.addons.payment.models.payment_acquirer import ValidationError from odoo.addons.payment.tests.common import PaymentAcquirerCommon from odoo.addons.payment_paypal.controllers.main import PaypalController @@ -182,7 +183,6 @@ class PaypalForm(PaypalCommon): self.assertEqual(tx.state, 'pending', 'paypal: wrong state after receiving a valid pending notification') self.assertEqual(tx.state_message, 'multi_currency', 'paypal: wrong state message after receiving a valid pending notification') self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification') - self.assertFalse(tx.date, 'paypal: validation date should not be updated whenr receiving pending notification') # update tx tx.write({ @@ -196,4 +196,4 @@ class PaypalForm(PaypalCommon): # check self.assertEqual(tx.state, 'done', 'paypal: wrong state after receiving a valid pending notification') self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification') - self.assertEqual(tx.date, '2013-11-18 11:21:19', 'paypal: wrong validation date') + self.assertEqual(fields.Datetime.to_string(tx.date), '2013-11-18 11:21:19', 'paypal: wrong validation date') diff --git a/addons/payment_stripe/tests/test_stripe.py b/addons/payment_stripe/tests/test_stripe.py index 5308f54dbe0..1ef883de93f 100644 --- a/addons/payment_stripe/tests/test_stripe.py +++ b/addons/payment_stripe/tests/test_stripe.py @@ -59,9 +59,6 @@ class StripeTest(StripeCommon): # render the button res = self.stripe.render('SO404', 320.0, self.currency_euro.id, values=self.buyer_values).decode('utf-8') - popup_script_src = 'script src="https://checkout.stripe.com/checkout.js"' - # check form result - self.assertIn(popup_script_src, res, "Stripe: popup script not found in template render") # Generated and received self.assertIn(self.buyer_values.get('partner_email'), res, 'Stripe: email input not found in rendered template') @@ -87,7 +84,7 @@ class StripeTest(StripeCommon): u'id': u'ch_172xfnGMfVJxozLwEjSfpfxD', u'invoice': None, u'livemode': False, - u'metadata': {u'reference': u'SO100'}, + u'metadata': {u'reference': u'SO100-1'}, u'object': u'charge', u'paid': True, u'receipt_email': None, @@ -137,6 +134,7 @@ class StripeTest(StripeCommon): tx.form_feedback(stripe_post_data, 'stripe') self.assertEqual(tx.state, 'done', 'Stripe: validation did not put tx into done state') self.assertEqual(tx.acquirer_reference, stripe_post_data.get('id'), 'Stripe: validation did not update tx id') + stripe_post_data['metadata']['reference'] = u'SO100-2' # reset tx tx = self.env['payment.transaction'].create({ 'amount': 4700, diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 2d17216eadc..a4919e7c17f 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -839,21 +839,18 @@ class PosOrder(models.Model): lots_necessary = True if picking_type: lots_necessary = picking_type and picking_type.use_existing_lots - qty = 0 qty_done = 0 pack_lots = [] pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', move.product_id.id)]) - pack_lot_names = [pos_pack.lot_name for pos_pack in pos_pack_lots] - if pack_lot_names and lots_necessary: - for lot_name in list(set(pack_lot_names)): - stock_production_lot = StockProductionLot.search([('name', '=', lot_name), ('product_id', '=', move.product_id.id)]) + if pos_pack_lots and lots_necessary: + for pos_pack_lot in pos_pack_lots: + stock_production_lot = StockProductionLot.search([('name', '=', pos_pack_lot.lot_name), ('product_id', '=', move.product_id.id)]) if stock_production_lot: + # a serialnumber always has a quantity of 1 product, a lot number takes the full quantity of the order line + qty = 1.0 if stock_production_lot.product_id.tracking == 'lot': - # if a lot nr is set through the frontend it will refer to the full quantity - qty = move.product_uom_qty - else: # serial numbers - qty = 1.0 + qty = pos_pack_lot.pos_order_line_id.qty qty_done += qty pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty}) else: diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 4bf06abdc6b..b82bcb38eb5 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1943,16 +1943,11 @@ var PaymentScreenWidget = ScreenWidget.extend({ $('body').keypress(this.keyboard_handler); // that one comes from the pos, but we prefer to cover all the basis $('body').keydown(this.keyboard_keydown_handler); - // legacy vanilla JS listeners - window.document.body.addEventListener('keypress',this.keyboard_handler); - window.document.body.addEventListener('keydown',this.keyboard_keydown_handler); this._super(); }, hide: function(){ $('body').off('keypress', this.keyboard_handler); $('body').off('keydown', this.keyboard_keydown_handler); - window.document.body.removeEventListener('keypress',this.keyboard_handler); - window.document.body.removeEventListener('keydown',this.keyboard_keydown_handler); this._super(); }, // sets up listeners to watch for order changes diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 69568ff9bea..94858d7a98d 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -156,10 +156,7 @@ class ProductTemplate(models.Model): @api.multi def _compute_currency_id(self): - try: - main_company = self.sudo().env.ref('base.main_company') - except ValueError: - main_company = self.env['res.company'].sudo().search([], limit=1, order="id") + main_company = self.env['res.company']._get_main_company() for template in self: template.currency_id = template.company_id.sudo().currency_id.id or main_company.currency_id.id @@ -929,6 +926,36 @@ class ProductTemplate(models.Model): return result combination = combination[:-1] + @api.multi + def _get_current_company(self, **kwargs): + """Get the most appropriate company for this product. + + If the company is set on the product, directly return it. Otherwise, + fallback to a contextual company. + + :param kwargs: kwargs forwarded to the fallback method. + + :return: the most appropriate company for this product + :rtype: recordset of one `res.company` + """ + self.ensure_one() + return self.company_id or self._get_current_company_fallback(**kwargs) + + @api.multi + def _get_current_company_fallback(self, **kwargs): + """Fallback to get the most appropriate company for this product. + + This should only be called from `_get_current_company` but is defined + separately to allow override. + + The final fallback will be the current user's company. + + :return: the fallback company for this product + :rtype: recordset of one `res.company` + """ + self.ensure_one() + return self.env.user.company_id + @api.model def get_empty_list_help(self, help): self = self.with_context( diff --git a/addons/product/report/product_product_templates.xml b/addons/product/report/product_product_templates.xml index bf8d0acfc10..2a612f556bc 100644 --- a/addons/product/report/product_product_templates.xml +++ b/addons/product/report/product_product_templates.xml @@ -12,7 +12,7 @@ <tr>
<td>
<strong>Price:</strong>
- <strong t-field="product.list_price" t-options="{'widget': 'monetary', 'display_currency': product.company_id.currency_id}"/>
+ <strong t-field="product.lst_price" t-options="{'widget': 'monetary', 'display_currency': product.company_id.currency_id}"/>
</td>
</tr>
<t t-if="product.barcode">
diff --git a/addons/sale/i18n/sale.pot b/addons/sale/i18n/sale.pot index a808ecba1e5..e9f665a9b3b 100644 --- a/addons/sale/i18n/sale.pot +++ b/addons/sale/i18n/sale.pot @@ -1581,6 +1581,11 @@ msgid "Invoicing address:" msgstr "" #. module: sale +#: model_terms:ir.ui.view,arch_db:sale.sale_order_portal_content +msgid "Invoicing and Shipping Address" +msgstr "" + +#. module: sale #: model_terms:ir.ui.view,arch_db:sale.report_saleorder_document msgid "Invoicing and shipping address:" msgstr "" diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index 4da13305464..3691067d106 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -261,7 +261,7 @@ class ProductTemplate(models.Model): if pricelist and pricelist.currency_id != product_template.currency_id: list_price = product_template.currency_id._convert( - list_price, pricelist.currency_id, product_template.company_id, + list_price, pricelist.currency_id, product_template._get_current_company(pricelist=pricelist), fields.Date.today() ) @@ -296,3 +296,11 @@ class ProductTemplate(models.Model): return False combination = self._get_first_possible_combination(parent_combination) return True if combination else self._is_combination_possible(combination, parent_combination) + + @api.multi + def _get_current_company_fallback(self, **kwargs): + """Override: if a pricelist is given, fallback to the company of the + pricelist if it is set, otherwise use the one from parent method.""" + res = super(ProductTemplate, self)._get_current_company_fallback(**kwargs) + pricelist = kwargs.get('pricelist') + return pricelist and pricelist.company_id or res diff --git a/addons/sale/static/src/js/product_configurator_mixin.js b/addons/sale/static/src/js/product_configurator_mixin.js index 458dd5a580c..1a74764aacc 100644 --- a/addons/sale/static/src/js/product_configurator_mixin.js +++ b/addons/sale/static/src/js/product_configurator_mixin.js @@ -281,6 +281,7 @@ var ProductConfiguratorMixin = { /** * Will return a deferred: + * * - If the product already exists, immediately resolves it with the product_id * - If the product does not exist yet ("dynamic" variant creation), this method will * create the product first and then resolve the deferred with the created product's id diff --git a/addons/sale/static/src/js/product_configurator_renderer.js b/addons/sale/static/src/js/product_configurator_renderer.js index 9a613501a2f..899656c8065 100644 --- a/addons/sale/static/src/js/product_configurator_renderer.js +++ b/addons/sale/static/src/js/product_configurator_renderer.js @@ -28,6 +28,7 @@ var ProductConfiguratorFormRenderer = FormRenderer.extend(ProductConfiguratorMix * Renders the product configurator within the form * * Will also: + * * - add events handling for variant changes * - trigger variant change to compute the price and other * variant specific changes diff --git a/addons/sale/tests/test_sale_product_attribute_value_config.py b/addons/sale/tests/test_sale_product_attribute_value_config.py index dd36a916778..95b1814f50e 100644 --- a/addons/sale/tests/test_sale_product_attribute_value_config.py +++ b/addons/sale/tests/test_sale_product_attribute_value_config.py @@ -185,6 +185,11 @@ class TestSaleProductAttributeValueConfig(TestSaleProductAttributeValueSetup): do_test(self) def test_02_get_combination_info(self): + # If using multi-company, company_id will be False, and this code should + # still work. + # The case with a company_id will be implicitly tested on website_sale. + self.computer.company_id = False + computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml index 7f0338d1b6d..4962253e810 100644 --- a/addons/sale/views/sale_portal_templates.xml +++ b/addons/sale/views/sale_portal_templates.xml @@ -381,13 +381,16 @@ </div> <div class="row"> <div class="col-lg-6"> - <strong class="d-block mb-1">Invoicing Address</strong> + <strong t-if="sale_order.partner_shipping_id == sale_order.partner_invoice_id" class="d-block mb-1">Invoicing and Shipping Address</strong> + <strong t-if="sale_order.partner_shipping_id != sale_order.partner_invoice_id" class="d-block mb-1">Invoicing Address</strong> <address t-field="sale_order.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/> </div> - <div id="shipping_address" class="col-lg-6" groups="sale.group_delivery_invoice_address"> - <strong class="d-block mb-1">Shipping Address</strong> - <address t-field="sale_order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/> - </div> + <t t-if="sale_order.partner_shipping_id != sale_order.partner_invoice_id"> + <div id="shipping_address" class="col-lg-6"> + <strong class="d-block mb-1">Shipping Address</strong> + <address t-field="sale_order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/> + </div> + </t> </div> <t t-set="invoices" t-value="[i for i in sale_order.invoice_ids if i.state not in ['draft', 'cancel']]"/> diff --git a/addons/sale_timesheet/static/src/js/timesheet_plan.js b/addons/sale_timesheet/static/src/js/timesheet_plan.js index 7a6de7a3cfe..6bd1896b3da 100644 --- a/addons/sale_timesheet/static/src/js/timesheet_plan.js +++ b/addons/sale_timesheet/static/src/js/timesheet_plan.js @@ -73,7 +73,13 @@ var ProjectPlan = AbstractAction.extend({ * @param {string|html} dom */ _refreshPlan: function (dom) { - this.$('.o_content').html(dom); + // TODO: Not forward port this fix on master + var $dom = $(dom); + $dom.find('div.o_timesheet_plan_sale_timesheet_dashboard > table.table, ' + + 'div.o_timesheet_plan_sale_timesheet_people_time > table.table, ' + + 'div.o_project_plan_project_timesheet_forecast > table.table') + .wrap('<div class="table-responsive"></div>'); + this.$('.o_content').html($dom); }, /** diff --git a/addons/sale_timesheet/static/tests/timesheet_plan_tests.js b/addons/sale_timesheet/static/tests/timesheet_plan_tests.js index c54b892d20e..17e9377afb7 100644 --- a/addons/sale_timesheet/static/tests/timesheet_plan_tests.js +++ b/addons/sale_timesheet/static/tests/timesheet_plan_tests.js @@ -48,7 +48,7 @@ QUnit.module('Timesheet Plan', { mockRPC: function (route) { assert.step(route); if (route === '/timesheet/plan') { - return $.when({html_content: 'Banach-Tarski'}); + return $.when({html_content: '<p>Banach-Tarski</p>'}); } return this._super.apply(this, arguments); }, @@ -81,7 +81,7 @@ QUnit.module('Timesheet Plan', { action: {name: 'Fibonacci'}, mockRPC: function (route) { if (route === '/timesheet/plan') { - return $.when({html_content: 'Banach-Tarski'}); + return $.when({html_content: '<p>Banach-Tarski</p>'}); } return this._super.apply(this, arguments); }, diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 0c45da2985a..5d7ea8a67c0 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -533,6 +533,10 @@ class Picking(models.Model): def action_assign_owner(self): self.move_line_ids.write({'owner_id': self.owner_id.id}) + def action_assign_partner(self): + for picking in self: + picking.move_lines.write({'partner_id': picking.partner_id.id}) + @api.multi def do_print_picking(self): self.write({'printed': True}) diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index cac7000381a..3a75e9744a0 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -248,6 +248,9 @@ <group> <group> <field name="partner_id"/> + <button name="action_assign_partner" string="Assign Partner" type="object" help="Assign the partner to the Operations. Use only if the partner is manually changed!" + attrs="{'invisible': ['|', '|', ('move_line_exist', '=', False), ('state', 'not in', ('draft', 'assigned', 'confirmed')), ('picking_type_code', '=', 'internal')]}" + class="oe_link oe_edit_only"/> <field name="picking_type_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/> <field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/> <field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/> diff --git a/addons/web/i18n/web.pot b/addons/web/i18n/web.pot index a29b279af41..f811ec82198 100644 --- a/addons/web/i18n/web.pot +++ b/addons/web/i18n/web.pot @@ -1573,6 +1573,13 @@ msgid "Group by: %s" msgstr "" #. module: web +#. openerp-web +#: code:addons/web/static/src/js/views/graph/graph_renderer.js:214 +#, python-format +msgid "Grouped" +msgstr "" + +#. module: web #: model:ir.model,name:web.model_ir_http msgid "HTTP Routing" msgstr "" @@ -2965,6 +2972,13 @@ msgstr "" #. module: web #. openerp-web +#: code:addons/web/static/src/js/views/graph/graph_renderer.js:215 +#, python-format +msgid "Stacked" +msgstr "" + +#. module: web +#. openerp-web #: code:addons/web/static/src/js/core/misc.js:14 #, python-format msgid "Still loading..." diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js index bbdee83c4f9..ec99f4fa0f6 100644 --- a/addons/web/static/src/js/fields/field_utils.js +++ b/addons/web/static/src/js/fields/field_utils.js @@ -363,7 +363,7 @@ function formatPercentage(value, field, options) { if (options.humanReadable && options.humanReadable(value * 100)) { return result + "%"; } - return parseFloat(result) + "%"; + return (parseFloat(result) + "%").replace('.', _t.database.parameters.decimal_point); } /** * Returns a string representing the value of the selection. diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index 38c2f02351e..856882a303b 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -215,6 +215,10 @@ return AbstractRenderer.extend({ margin: {left: 80, bottom: 100, top: 80, right: 0}, delay: 100, transition: 10, + controlLabels: { + 'grouped': _t('Grouped'), + 'stacked': _t('Stacked'), + }, showLegend: _.size(data) <= MAX_LEGEND_LENGTH, showXAxis: true, showYAxis: true, 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 32101d0cd00..cfb05792d4c 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -440,7 +440,8 @@ var KanbanRenderer = BasicRenderer.extend({ _setState: function (state) { this.state = state; - var groupByField = state.groupedBy[0]; + // split groupedBy field with ':' as it can be date/datetime field + var groupByField = state.groupedBy.length && state.groupedBy[0].split(":")[0]; var groupByFieldAttrs = state.fields[groupByField]; var groupByFieldInfo = state.fieldsInfo.kanban[groupByField]; // Deactivate the drag'n'drop if the groupedBy field: diff --git a/addons/web/static/src/js/widgets/pie_chart.js b/addons/web/static/src/js/widgets/pie_chart.js index 30944d69584..1054360b3fd 100644 --- a/addons/web/static/src/js/widgets/pie_chart.js +++ b/addons/web/static/src/js/widgets/pie_chart.js @@ -31,6 +31,7 @@ var PieChart = Widget.extend({ Domain.prototype.stringToArray(modifiers.domain || '[]')); var arch = qweb.render('web.PieChart', { modifiers: modifiers, + title: node.attrs.title || modifiers.title || modifiers.measure, }); this.subViewParams = { diff --git a/addons/web/static/src/xml/pie_chart.xml b/addons/web/static/src/xml/pie_chart.xml index 5c3411bc882..d45bef7af6f 100644 --- a/addons/web/static/src/xml/pie_chart.xml +++ b/addons/web/static/src/xml/pie_chart.xml @@ -1,7 +1,7 @@ <template> <t t-name="web.PieChart"> - <graph type="pie" t-att-title="modifiers.title || modifiers.measure"> + <graph type="pie" t-att-title="title"> <field t-att-name="modifiers.groupby.split(':')[0]" t-att-interval="modifiers.groupby.split(':')[1]"/> <field t-if="modifiers.measure" t-att-name="modifiers.measure" type="measure"/> </graph> diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js index 6b7cda13264..940f5674780 100644 --- a/addons/web/static/tests/fields/field_utils_tests.js +++ b/addons/web/static/tests/fields/field_utils_tests.js @@ -135,7 +135,9 @@ QUnit.test('format binary', function (assert) { }); QUnit.test('format percentage', function (assert) { - assert.expect(9); + assert.expect(11); + + var originalParameters = _.clone(core._t.database.parameters); assert.strictEqual(fieldUtils.format.percentage(0), '0%'); assert.strictEqual(fieldUtils.format.percentage(0.5), '50%'); @@ -151,6 +153,16 @@ QUnit.test('format percentage', function (assert) { assert.strictEqual(fieldUtils.format.percentage(50, null, {humanReadable: function (val) {return true;}}), '5k%' ); + + _.extend(core._t.database.parameters, { + grouping: [3, 0], + decimal_point: ',', + thousands_sep: '.' + }); + assert.strictEqual(fieldUtils.format.percentage(0.125), '12,5%'); + assert.strictEqual(fieldUtils.format.percentage(0.666666), '66,67%'); + + core._t.database.parameters = originalParameters; }); QUnit.test('parse float', function(assert) { @@ -262,7 +274,9 @@ QUnit.test('parse monetary', function(assert) { }); QUnit.test('parse percentage', function(assert) { - assert.expect(7); + assert.expect(9); + + var originalParameters = _.clone(core._t.database.parameters); assert.strictEqual(fieldUtils.parse.percentage(""), 0); assert.strictEqual(fieldUtils.parse.percentage("0"), 0); @@ -271,6 +285,18 @@ QUnit.test('parse percentage', function(assert) { assert.strictEqual(fieldUtils.parse.percentage("1"), 1); assert.strictEqual(fieldUtils.parse.percentage("2%"), 0.02); assert.strictEqual(fieldUtils.parse.percentage("100%"), 1); + + _.extend(core._t.database.parameters, { + grouping: [3, 0], + decimal_point: ',', + thousands_sep: '.' + }); + + assert.strictEqual(fieldUtils.parse.percentage("0,04"), 0.04); + assert.strictEqual(fieldUtils.parse.percentage("6,02%"), 0.0602); + + core._t.database.parameters = originalParameters; + }) }); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 6702a9bee37..86538ab5eb0 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -2580,6 +2580,45 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('prevent drag and drop if grouped by date/datetime field', function (assert) { + assert.expect(5); + + this.data.partner.records[0].date = '2017-01-08'; + this.data.partner.records[1].date = '2017-01-09'; + this.data.partner.records[2].date = '2017-02-08'; + this.data.partner.records[3].date = '2017-02-10'; + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test">' + + '<field name="bar"/>' + + '<templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates></kanban>', + groupBy: ['date:month'], + }); + + assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have 2 columns"); + assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2, + "1st column should contain 2 records of January month"); + assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, + "2nd column should contain 2 records of February month"); + + // drag&drop a record in another column + var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); + var $group = kanban.$('.o_kanban_group:nth-child(2)'); + testUtils.dragAndDrop($record, $group); + + // should not drag&drop record + assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length , 2, + "Should remain same records in first column(2 records)"); + assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, + "Should remain same records in 2nd column(2 record)"); + kanban.destroy(); + }); + QUnit.test('kanban view with default_group_by', function (assert) { assert.expect(7); this.data.partner.records.product_id = 1; diff --git a/addons/web/views/report_templates.xml b/addons/web/views/report_templates.xml index c9f78d20394..80479b0bcfa 100644 --- a/addons/web/views/report_templates.xml +++ b/addons/web/views/report_templates.xml @@ -282,7 +282,7 @@ <h4 class="mt0 mb0 text-uppercase" t-field="company.report_header"/> </div> <div class="col-1"> - <ul t-if="report_type != 'html'" class="list-inline pagenumber float-right text-center"> + <ul t-if="report_type == 'pdf'" class="list-inline pagenumber float-right text-center"> <li class="list-inline-item"><strong><span class="page"/></strong></li> </ul> </div> diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js index 194b7e15d4d..9b98dc7b89f 100644 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -773,6 +773,7 @@ var IconWidget = MediaWidget.extend({ this.$('div.font-icons-icons').html( QWeb.render('web_editor.dialog.font-icons.icons', {iconsParser: iconsParser}) ); + return $.when(); }, //-------------------------------------------------------------------------- @@ -1299,6 +1300,7 @@ var MediaDialog = Dialog.extend({ /** * @private */ + ev.preventDefault(); _onSaveRequest: function (ev) { ev.stopPropagation(); this.save(); diff --git a/addons/website/i18n/website.pot b/addons/website/i18n/website.pot index eabf5704b9a..47348bd14aa 100644 --- a/addons/website/i18n/website.pot +++ b/addons/website/i18n/website.pot @@ -533,6 +533,26 @@ msgid "<i class=\"fa fa-trash-o\"/>Remove Slide" msgstr "" #. module: website +#: model_terms:ir.ui.view,arch_db:website.list_website_pages +msgid "<i title=\"Is the page SEO optimized?\" class=\"fa fa-search\"/>" +msgstr "" + +#. module: website +#: model_terms:ir.ui.view,arch_db:website.list_website_pages +msgid "<i title=\"Is the page included in the main menu?\" class=\"fa fa-thumb-tack\"/>" +msgstr "" + +#. module: website +#: model_terms:ir.ui.view,arch_db:website.list_website_pages +msgid "<i title=\"Is the page indexed by search engines?\" class=\"fa fa-globe\"/>" +msgstr "" + +#. module: website +#: model_terms:ir.ui.view,arch_db:website.list_website_pages +msgid "<i title=\"Is the page published?\" class=\"fa fa-eye\"/>" +msgstr "" + +#. module: website #: model_terms:ir.ui.view,arch_db:website.s_comparisons msgid "<i>Instant setup, satisfied or reimbursed.</i>" msgstr "" @@ -570,6 +590,16 @@ msgid "<span class=\"carousel-control-prev-icon\"/>\n" msgstr "" #. module: website +#: model_terms:ir.ui.view,arch_db:website.res_config_settings_view_form +msgid "<span class=\"fa fa-lg fa-globe\" title=\"Values set here are website-specific.\" groups=\"website.group_multi_website\"/>" +msgstr "" + +#. module: website +#: model_terms:ir.ui.view,arch_db:website.company_description +msgid "<span class=\"fa fa-map-marker fa-fw mt16\" role=\"img\" aria-label=\"Address\" title=\"Address\"/>" +msgstr "" + +#. module: website #: model_terms:ir.ui.view,arch_db:website.user_navbar msgid "<span class=\"fa fa-pencil\"/>Edit" msgstr "" @@ -580,6 +610,11 @@ msgid "<span class=\"fa fa-plus\"/>New" msgstr "" #. module: website +#: model_terms:ir.ui.view,arch_db:website.list_website_pages +msgid "<span class=\"fa fa-sort fa-lg\" role=\"img\" aria-label=\"Sort\" title=\"Sort\"/>" +msgstr "" + +#. module: website #: model_terms:ir.ui.view,arch_db:website.s_facebook_page msgid "<span class=\"o_add_facebook_page\">\n" " <i class=\"fa fa-plus-circle\"/> Add Facebook Page\n" @@ -619,6 +654,11 @@ msgstr "" #. module: website #: model_terms:ir.ui.view,arch_db:website.user_navbar +msgid "<span title=\"Mobile preview\" role=\"img\" aria-label=\"Mobile preview\" class=\"fa fa-mobile\"/>" +msgstr "" + +#. module: website +#: model_terms:ir.ui.view,arch_db:website.user_navbar msgid "<span/>\n" " <span class=\"css_publish\">Unpublished</span>\n" " <span class=\"css_unpublish\">Published</span>" diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 1a98713326d..d37852ae0de 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -143,8 +143,12 @@ class View(models.Model): return self.filtered(lambda view: not view.website_id) for view in self: + # specific view: add it if it's for the current website and ignore + # it if it's for another website if view.website_id and view.website_id.id == current_website_id: most_specific_views |= view + # generic view: add it only if, for the current website, there is no + # specific view for this view (based on the same `key` attribute) elif not view.website_id and not any(view.key == view2.key and view2.website_id and view2.website_id.id == current_website_id for view2 in self): most_specific_views |= view @@ -201,6 +205,17 @@ class View(models.Model): @api.model @tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',)) def get_view_id(self, xml_id): + """If a website_id is in the context and the given xml_id is not an int + then try to get the id of the specific view for that website, but + fallback to the id of the generic view if there is no specific. + + If no website_id is in the context, it might randomly return the generic + or the specific view, so it's probably not recommanded to use this + method. `viewref` is probably more suitable. + + Archived views are ignored (unless the active_test context is set, but + then the ormcache_context will not work as expected). + """ if 'website_id' in self._context and not isinstance(xml_id, int): current_website = self.env['website'].browse(self._context.get('website_id')) domain = ['&', ('key', '=', xml_id)] + current_website.website_domain() diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 82709c4663d..4aea35aa481 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -503,12 +503,18 @@ class Website(models.Model): def viewref(self, view_id, raise_if_not_found=True): ''' Given an xml_id or a view_id, return the corresponding view record. In case of website context, return the most specific one. + + If no website_id is in the context, it will return the generic view, + instead of a random one like `get_view_id`. + + Look also for archived views, no matter the context. + :param view_id: either a string xml_id or an integer view_id :param raise_if_not_found: should the method raise an error if no view found :return: The view record or empty recordset ''' View = self.env['ir.ui.view'] - view = None + view = View if isinstance(view_id, str): if 'website_id' in self._context: domain = [('key', '=', view_id)] + self.env['website'].website_domain(self._context.get('website_id')) @@ -520,20 +526,20 @@ class Website(models.Model): if views: view = views.filter_duplicate() else: - view = self.env.ref(view_id) + # we handle the raise below + view = self.env.ref(view_id, raise_if_not_found=False) # self.env.ref might return something else than an ir.ui.view (eg: a theme.ir.ui.view) - if view._name != 'ir.ui.view': - view = None + if not view or view._name != 'ir.ui.view': + # make sure we always return a recordset + view = View elif isinstance(view_id, int): view = View.browse(view_id) else: raise ValueError('Expecting a string or an integer, not a %s.' % (type(view_id))) - if view: - return view - if raise_if_not_found: + if not view and raise_if_not_found: raise ValueError('No record found for unique ID %s. It may have been deleted.' % (view_id)) - return None + return view @api.model def get_template(self, template): @@ -1151,9 +1157,12 @@ class Menu(models.Model): @api.multi def unlink(self): default_menu = self.env.ref('website.main_menu', raise_if_not_found=False) + menus_to_remove = self for menu in self.filtered(lambda m: default_menu and m.parent_id.id == default_menu.id): - self.env['website.menu'].search([('url', '=', menu.url), ('id', '!=', menu.id)]).unlink() - return super(Menu, self).unlink() + menus_to_remove |= self.env['website.menu'].search([('url', '=', menu.url), + ('website_id', '!=', False), + ('id', '!=', menu.id)]) + return super(Menu, menus_to_remove).unlink() @api.one def _compute_visible(self): diff --git a/addons/website/static/src/scss/bootstrap_overridden.scss b/addons/website/static/src/scss/bootstrap_overridden.scss index 8c21c748fe1..b25b9af1f25 100644 --- a/addons/website/static/src/scss/bootstrap_overridden.scss +++ b/addons/website/static/src/scss/bootstrap_overridden.scss @@ -31,3 +31,7 @@ $font-family-sans-serif: nth($o-theme-fonts, $o-theme-font-number); $headings-font-family: nth($o-theme-fonts, $o-theme-headings-font-number); $headings-color: color('h1'); + +// Jumbotron + +$jumbotron-bg: transparent !default; diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index ad5e297a3a2..05febbf1fae 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -847,7 +847,7 @@ <div class="container accordion mb32 mt32" id="debug_infos"> <div class="card" t-if="exception"> <h4 class="card-header"> - <a data-toggle="collapse" data-parent="#debug_infos" href="#error_main">Error</a> + <a data-toggle="collapse" href="#error_main">Error</a> </h4> <div id="error_main" class="collapse in"> <div class="card-body"> @@ -861,7 +861,7 @@ </div> <div class="card" t-if="qweb_exception"> <h4 class="card-header"> - <a data-toggle="collapse" data-parent="#adebug_infos" href="#error_qweb">QWeb</a> + <a data-toggle="collapse" href="#error_qweb">QWeb</a> </h4> <div id="error_qweb" class="collapse"> <div class="card-body"> @@ -881,7 +881,7 @@ </div> <div class="card" t-if="traceback"> <h4 class="card-header"> - <a data-toggle="collapse" data-parent="#adebug_infos" href="#error_traceback">Traceback</a> + <a data-toggle="collapse" href="#error_traceback">Traceback</a> </h4> <div id="error_traceback" class="collapse"> <div class="card-body"> diff --git a/addons/website_blog/views/website_blog_views.xml b/addons/website_blog/views/website_blog_views.xml index 85386651ec1..a3cc6916d43 100644 --- a/addons/website_blog/views/website_blog_views.xml +++ b/addons/website_blog/views/website_blog_views.xml @@ -47,7 +47,7 @@ <field name="name"/> <field name="active" invisible="1"/> <field name="blog_id"/> - <field name="website_id"/> + <field name="website_id" groups="website.group_multi_website"/> <field name="visits"/> <field name="create_uid" invisible="1"/> <field name="write_uid"/> diff --git a/addons/website_event/views/event_views.xml b/addons/website_event/views/event_views.xml index 7e4df97c36a..37391294ebc 100644 --- a/addons/website_event/views/event_views.xml +++ b/addons/website_event/views/event_views.xml @@ -40,6 +40,7 @@ class="oe_stat_button" icon="fa-globe"> <field name="website_published" widget="website_button"/> + <field name="is_published" invisible="1"/> </button> </button> <label for="is_online" position="after"> diff --git a/addons/website_hr/views/hr_employee_views.xml b/addons/website_hr/views/hr_employee_views.xml index e48c6e6b38d..85dc0039676 100644 --- a/addons/website_hr/views/hr_employee_views.xml +++ b/addons/website_hr/views/hr_employee_views.xml @@ -10,6 +10,7 @@ <button class="oe_stat_button" name="website_publish_button" type="object" icon="fa-globe" groups="hr.group_hr_user"> <field name="website_published" widget="website_button"/> + <field name="is_published" invisible="1"/> </button> </button> <xpath expr="//page[@name='public']/group" position="inside"> diff --git a/addons/website_hr_recruitment/views/hr_recruitment_views.xml b/addons/website_hr_recruitment/views/hr_recruitment_views.xml index b74351b389a..059041a31b5 100644 --- a/addons/website_hr_recruitment/views/hr_recruitment_views.xml +++ b/addons/website_hr_recruitment/views/hr_recruitment_views.xml @@ -20,6 +20,7 @@ <button class="oe_stat_button" name="website_publish_button" type="object" icon="fa-globe"> <field name="website_published" widget="website_button"/> + <field name="is_published" invisible="1"/> </button> </div> </field> diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index aec6d3b17ed..0b5ed81a6e5 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -150,9 +150,10 @@ class WebsiteSale(ProductConfiguratorController): return pricelist_context, pricelist def _get_compute_currency(self, pricelist, product=None): - from_currency = (product or request.env.user.company_id).currency_id + company = product and product._get_current_company(pricelist=pricelist, website=request.website) or pricelist.company_id or request.website.company_id + from_currency = (product or request.env['res.company']._get_main_company()).currency_id to_currency = pricelist.currency_id - return lambda price: from_currency._convert(price, to_currency, (product or request.env.user).company_id, fields.Date.today()) + return lambda price: from_currency._convert(price, to_currency, company, fields.Date.today()) def _get_search_order(self, post): # OrderBy will be parsed in orm and so no direct sql injection @@ -312,7 +313,7 @@ class WebsiteSale(ProductConfiguratorController): pricelist = request.website.get_current_pricelist() def compute_currency(price): - return product.currency_id._convert(price, pricelist.currency_id, product.company_id, fields.Date.today()) + return product.currency_id._convert(price, pricelist.currency_id, product._get_current_company(pricelist=pricelist, website=request.website), fields.Date.today()) if not product_context.get('pricelist'): product_context['pricelist'] = pricelist.id diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 8bf6fedcc6a..e0ee166ad39 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -287,6 +287,14 @@ class ProductTemplate(models.Model): """ return self._create_product_variant(self._get_first_possible_combination(), log_warning) + @api.multi + def _get_current_company_fallback(self, **kwargs): + """Override: if a website is set on the product or given, fallback to + the company of the website. Otherwise use the one from parent method.""" + res = super(ProductTemplate, self)._get_current_company_fallback(**kwargs) + website = self.website_id or kwargs.get('website') + return website and website.company_id or res + def _default_website_sequence(self): self._cr.execute("SELECT MIN(website_sequence) FROM %s" % self._table) min_sequence = self._cr.fetchone()[0] diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss index 29e685b8b4a..6a5d755ad64 100644 --- a/addons/website_sale/static/src/scss/website_sale.scss +++ b/addons/website_sale/static/src/scss/website_sale.scss @@ -690,7 +690,7 @@ a.no-decoration { .o_website_sale_animate { opacity: 0.7; - position: absolute; + position: absolute !important; height: 150px; width: 150px; z-index: 1020; diff --git a/addons/website_sale/views/product_views.xml b/addons/website_sale/views/product_views.xml index d3f3fbd2a81..52b57be8deb 100644 --- a/addons/website_sale/views/product_views.xml +++ b/addons/website_sale/views/product_views.xml @@ -88,6 +88,7 @@ <button class="oe_stat_button" name="website_publish_button" type="object" icon="fa-globe" attrs="{'invisible': [('sale_ok','=',False)]}"> <field name="website_published" widget="website_button"/> + <field name="is_published" invisible="1"/> </button> </xpath> <xpath expr="//page[@name='sales']" position="after"> diff --git a/addons/website_sale_comparison/tests/__init__.py b/addons/website_sale_comparison/tests/__init__.py new file mode 100644 index 00000000000..0a54e54e5cf --- /dev/null +++ b/addons/website_sale_comparison/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_website_sale_comparison diff --git a/addons/website_sale_comparison/tests/test_website_sale_comparison.py b/addons/website_sale_comparison/tests/test_website_sale_comparison.py new file mode 100644 index 00000000000..91c68c7508a --- /dev/null +++ b/addons/website_sale_comparison/tests/test_website_sale_comparison.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo.tests + + +@odoo.tests.tagged('-at_install', 'post_install') +class TestWebsiteSaleComparison(odoo.tests.TransactionCase): + def test_01_website_sale_comparison_remove(self): + """ This tour makes sure the product page still works after the module + `website_sale_comparison` has been removed. + + Technically it tests the removal of copied views by the base method + `_remove_copied_views`. The problematic view that has to be removed is + `product_add_to_compare` because it has a reference to `add_to_compare`. + """ + + Website0 = self.env['website'].with_context(website_id=None) + Website1 = self.env['website'].with_context(website_id=1) + + # Create a generic inherited view, with a key not starting with + # `website_sale_comparison` otherwise the unlink will work just based on + # the key, but we want to test also for `MODULE_UNINSTALL_FLAG`. + product_add_to_compare = Website0.viewref('website_sale_comparison.product_add_to_compare') + test_view_key = 'my_test.my_key' + self.env['ir.ui.view'].with_context(website_id=None).create({ + 'name': 'test inherited view', + 'key': test_view_key, + 'inherit_id': product_add_to_compare.id, + 'arch': '<div/>', + }) + + # Retrieve the generic view + product = Website0.viewref('website_sale.product') + # Trigger COW to create specific views of the whole tree + product.with_context(website_id=1).write({'name': 'Trigger COW'}) + + # Verify initial state: the specific views exist + self.assertEquals(Website1.viewref('website_sale.product').website_id.id, 1) + self.assertEquals(Website1.viewref('website_sale_comparison.product_add_to_compare').website_id.id, 1) + self.assertEquals(Website1.viewref(test_view_key).website_id.id, 1) + + # Remove the module (use `module_uninstall` because it is enough to test + # what we want here, no need/can't use `button_immediate_uninstall` + # because it would commit the test transaction) + website_sale_comparison = self.env['ir.module.module'].search([('name', '=', 'website_sale_comparison')]) + website_sale_comparison.module_uninstall() + + # Check that the generic view is correctly removed + self.assertFalse(Website0.viewref('website_sale_comparison.product_add_to_compare', raise_if_not_found=False)) + # Check that the specific view is correctly removed + self.assertFalse(Website1.viewref('website_sale_comparison.product_add_to_compare', raise_if_not_found=False)) + + # Check that the generic inherited view is correctly removed + self.assertFalse(Website0.viewref(test_view_key, raise_if_not_found=False)) + # Check that the specific inherited view is correctly removed + self.assertFalse(Website1.viewref(test_view_key, raise_if_not_found=False)) diff --git a/addons/website_sale_delivery/models/delivery.py b/addons/website_sale_delivery/models/delivery.py index 5a7270c7f1c..6b14ba8004e 100644 --- a/addons/website_sale_delivery/models/delivery.py +++ b/addons/website_sale_delivery/models/delivery.py @@ -9,4 +9,3 @@ class DeliveryCarrier(models.Model): _inherit = ['delivery.carrier', 'website.published.multi.mixin'] website_description = fields.Text(related='product_id.description_sale', string='Description for Online Quotations', readonly=False) - website_published = fields.Boolean(default=False) diff --git a/addons/website_sale_delivery/models/sale_order.py b/addons/website_sale_delivery/models/sale_order.py index 4cf21e3b587..3103fb06ab3 100644 --- a/addons/website_sale_delivery/models/sale_order.py +++ b/addons/website_sale_delivery/models/sale_order.py @@ -75,6 +75,7 @@ class SaleOrder(models.Model): def _get_delivery_methods(self): address = self.partner_shipping_id + # searching on website_published will also search for available website (_search method on computed field) return self.env['delivery.carrier'].sudo().search([('website_published', '=', True)]).available_carriers(address) @api.multi diff --git a/addons/website_sale_delivery/views/website_sale_delivery_views.xml b/addons/website_sale_delivery/views/website_sale_delivery_views.xml index 0e60a35fb70..976b0971d7f 100644 --- a/addons/website_sale_delivery/views/website_sale_delivery_views.xml +++ b/addons/website_sale_delivery/views/website_sale_delivery_views.xml @@ -6,6 +6,9 @@ <field name="model">delivery.carrier</field> <field name="inherit_id" ref="delivery.view_delivery_carrier_form"/> <field name="arch" type="xml"> + <field name="company_id" position='after'> + <field name="website_id" groups="website.group_multi_website" options="{'no_open': True, 'no_create_edit': True}"/> + </field> <xpath expr="//page[@name='destination']" position='after'> <page string="Description"> <field name="website_description" placeholder="Description displayed on the eCommerce and on online quotations."/> @@ -26,6 +29,7 @@ <field name="arch" type="xml"> <field name="delivery_type" position="after"> <field name="website_published"/> + <field name="website_id" groups="website.group_multi_website"/> </field> </field> </record> diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml index 308610c0e90..834ea11918a 100644 --- a/addons/website_slides/views/slide_channel_views.xml +++ b/addons/website_slides/views/slide_channel_views.xml @@ -52,6 +52,7 @@ <button class="oe_stat_button" name="website_publish_button" type="object" icon="fa-globe"> <field name="website_published" widget="website_button"/> + <field name="is_published" invisible="1"/> </button> <button name="toggle_active" type="object" confirm="(Un)archiving a channel automatically (un)archives its slides. Do you want to proceed?" class="oe_stat_button" icon="fa-archive"> <field name="active" widget="boolean_button" options='{"terminology": "archive"}'/> diff --git a/addons/website_slides/views/website_slides_templates.xml b/addons/website_slides/views/website_slides_templates.xml index 9679db83d9c..393123f9e05 100644 --- a/addons/website_slides/views/website_slides_templates.xml +++ b/addons/website_slides/views/website_slides_templates.xml @@ -446,7 +446,7 @@ tabindex="0" data-toggle="popover"> <i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"/> - <t t-esc="slide.likes"/> + <span class="o_wslides_like_dislike_count" t-esc="slide.likes"/> </span> <span class="oe_slide_js_unlike" @@ -457,7 +457,7 @@ tabindex="0" data-toggle="popover"> <i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"/> - <t t-esc="slide.dislikes"/> + <span class="o_wslides_like_dislike_count" t-esc="slide.dislikes"/> </span> </div> </div> @@ -492,7 +492,7 @@ </li> </ul> <div class="tab-content" style="padding: 20px 5px 5px 5px; word-wrap: break-word;"> - <div role="tabpanel" t-att-class="not comments and 'tab-pane fade active in' or 'tab-pane fade'" id="about"> + <div role="tabpanel" t-att-class="not comments and 'tab-pane fade in show active' or 'tab-pane fade'" id="about"> <div t-field="slide.description"/> </div> <div role="tabpanel" class="tab-pane fade" t-if="slide.website_published" id="share"> @@ -513,7 +513,7 @@ The social sharing module will be unlocked when a moderator will allow your publication. </h4> </div> - <div role="tabpanel" t-att-class="comments and 'tab-pane fade active in' or 'tab-pane fade'" id="discuss"> + <div role="tabpanel" t-att-class="comments and 'tab-pane fade in show active' or 'tab-pane fade'" id="discuss"> <t t-call="portal.message_thread"> <t t-set="object" t-value="slide"/> </t> diff --git a/addons/website_theme_install/models/ir_module_module.py b/addons/website_theme_install/models/ir_module_module.py index 00514b8fa3f..55f77590c6b 100644 --- a/addons/website_theme_install/models/ir_module_module.py +++ b/addons/website_theme_install/models/ir_module_module.py @@ -6,6 +6,7 @@ import os from collections import OrderedDict from odoo import api, fields, models +from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG from odoo.exceptions import MissingError from odoo.http import request @@ -206,7 +207,7 @@ class IrModuleModule(models.Model): for model_name in self._theme_model_names: template = self._get_module_data(model_name) - models = template.with_context(active_test=False).mapped('copy_ids').filtered(lambda m: m.website_id == website) + models = template.with_context(**{'active_test': False, MODULE_UNINSTALL_FLAG: True}).mapped('copy_ids').filtered(lambda m: m.website_id == website) models.unlink() self._theme_cleanup(model_name, website) @@ -235,8 +236,9 @@ class IrModuleModule(models.Model): if model_name in ('website.page', 'website.menu'): return model - - orphans = model.with_context(active_test=False).search([ + # use active_test to also unlink archived models + # and use MODULE_UNINSTALL_FLAG to also unlink inherited models + orphans = model.with_context(**{'active_test': False, MODULE_UNINSTALL_FLAG: True}).search([ ('key', '=like', self.name + '.%'), ('website_id', '=', website.id), ('theme_template_id', '=', False), @@ -334,9 +336,6 @@ class IrModuleModule(models.Model): """ Remove any existing theme on the current website and install the theme ``self`` instead. - The removal of the previous theme will however keep the existing data - that will also be used in the new theme. - The actual loading of the theme on the current website will be done automatically on ``write`` thanks to the upgrade and/or install. |
