summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_bank_statement.py5
-rw-r--r--addons/account/models/account_invoice.py14
-rw-r--r--addons/account/models/account_move.py19
-rw-r--r--addons/account/models/account_payment.py5
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js4
-rw-r--r--addons/account/tests/test_account_customer_invoice.py63
-rw-r--r--addons/account/tests/test_reconciliation.py337
-rw-r--r--addons/account/views/account_view.xml2
-rw-r--r--addons/barcodes/static/src/js/barcode_field.js4
-rw-r--r--addons/delivery/models/stock_picking.py9
-rw-r--r--addons/hr_holidays/i18n/hr_holidays.pot5
-rw-r--r--addons/hr_holidays/models/hr_leave.py5
-rw-r--r--addons/hr_holidays/models/hr_leave_type.py27
-rw-r--r--addons/hr_holidays/views/hr_leave_type_views.xml6
-rw-r--r--addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py10
-rw-r--r--addons/l10n_nl/data/account_fiscal_position_tax_template.xml2
-rw-r--r--addons/mail/static/src/js/models/mail_failure.js2
-rw-r--r--addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js12
-rw-r--r--addons/mail/static/src/js/services/mail_notification_manager.js2
-rw-r--r--addons/mail/static/src/js/services/mail_window_manager.js6
-rw-r--r--addons/mail/static/src/js/thread_windows/thread_window.js6
-rw-r--r--addons/mrp/static/src/js/mrp_bom_report.js3
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js2
-rw-r--r--addons/payment/models/payment_acquirer.py25
-rw-r--r--addons/payment_authorize/models/authorize_request.py2
-rw-r--r--addons/payment_authorize/tests/test_authorize.py2
-rw-r--r--addons/payment_buckaroo/tests/test_buckaroo.py36
-rw-r--r--addons/payment_buckaroo/views/payment_buckaroo_templates.xml38
-rw-r--r--addons/payment_paypal/tests/test_paypal.py4
-rw-r--r--addons/payment_stripe/tests/test_stripe.py6
-rw-r--r--addons/point_of_sale/models/pos_order.py15
-rw-r--r--addons/point_of_sale/static/src/js/screens.js5
-rw-r--r--addons/product/models/product_template.py35
-rw-r--r--addons/product/report/product_product_templates.xml2
-rw-r--r--addons/sale/i18n/sale.pot5
-rw-r--r--addons/sale/models/product_template.py10
-rw-r--r--addons/sale/static/src/js/product_configurator_mixin.js1
-rw-r--r--addons/sale/static/src/js/product_configurator_renderer.js1
-rw-r--r--addons/sale/tests/test_sale_product_attribute_value_config.py5
-rw-r--r--addons/sale/views/sale_portal_templates.xml13
-rw-r--r--addons/sale_timesheet/static/src/js/timesheet_plan.js8
-rw-r--r--addons/sale_timesheet/static/tests/timesheet_plan_tests.js4
-rw-r--r--addons/stock/models/stock_picking.py4
-rw-r--r--addons/stock/views/stock_picking_views.xml3
-rw-r--r--addons/web/i18n/web.pot14
-rw-r--r--addons/web/static/src/js/fields/field_utils.js2
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js4
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js3
-rw-r--r--addons/web/static/src/js/widgets/pie_chart.js1
-rw-r--r--addons/web/static/src/xml/pie_chart.xml2
-rw-r--r--addons/web/static/tests/fields/field_utils_tests.js30
-rw-r--r--addons/web/static/tests/views/kanban_tests.js39
-rw-r--r--addons/web/views/report_templates.xml2
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js2
-rw-r--r--addons/website/i18n/website.pot40
-rw-r--r--addons/website/models/ir_ui_view.py15
-rw-r--r--addons/website/models/website.py29
-rw-r--r--addons/website/static/src/scss/bootstrap_overridden.scss4
-rw-r--r--addons/website/views/website_templates.xml6
-rw-r--r--addons/website_blog/views/website_blog_views.xml2
-rw-r--r--addons/website_event/views/event_views.xml1
-rw-r--r--addons/website_hr/views/hr_employee_views.xml1
-rw-r--r--addons/website_hr_recruitment/views/hr_recruitment_views.xml1
-rw-r--r--addons/website_sale/controllers/main.py7
-rw-r--r--addons/website_sale/models/product.py8
-rw-r--r--addons/website_sale/static/src/scss/website_sale.scss2
-rw-r--r--addons/website_sale/views/product_views.xml1
-rw-r--r--addons/website_sale_comparison/tests/__init__.py4
-rw-r--r--addons/website_sale_comparison/tests/test_website_sale_comparison.py57
-rw-r--r--addons/website_sale_delivery/models/delivery.py1
-rw-r--r--addons/website_sale_delivery/models/sale_order.py1
-rw-r--r--addons/website_sale_delivery/views/website_sale_delivery_views.xml4
-rw-r--r--addons/website_slides/views/slide_channel_views.xml1
-rw-r--r--addons/website_slides/views/website_slides_templates.xml8
-rw-r--r--addons/website_theme_install/models/ir_module_module.py11
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.