summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-05-13 11:02:27 +0200
committerChristophe Simonis <chs@odoo.com>2019-05-13 11:02:27 +0200
commit60e71302a38b36214efec1213882f573e3ea453c (patch)
treed6c4f548ac29c94891a52444887a230a7c30d403 /addons
parent3936d655c4bfd845b6ead724cecdc8e7c62816eb (diff)
parent243a4ae5cbc07027cd3eaf0ead036b9d3586dea3 (diff)
[MERGE] forward port branch saas-12.2 up to 243a4ae5cbc
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_bank_statement.py123
-rw-r--r--addons/account/models/account_move.py29
-rw-r--r--addons/base_address_extended/data/base_address_extended_data.xml3
-rw-r--r--addons/base_address_extended/models/base_address_extended.py6
-rw-r--r--addons/base_import/models/base_import.py14
-rw-r--r--addons/base_import/tests/test_csv_magic.py15
-rw-r--r--addons/board/static/tests/dashboard_tests.js12
-rw-r--r--addons/calendar/models/calendar.py5
-rw-r--r--addons/calendar/models/mail_message.py3
-rw-r--r--addons/delivery/models/stock_picking.py4
-rw-r--r--addons/digest/data/digest_template_data.xml2
-rw-r--r--addons/google_account/models/google_service.py8
-rw-r--r--addons/hr_contract/models/hr_contract.py17
-rw-r--r--addons/hr_holidays/models/hr_leave_allocation.py13
-rw-r--r--addons/hr_holidays/models/hr_leave_type.py26
-rw-r--r--addons/hr_holidays/views/hr_leave_allocation_views.xml2
-rw-r--r--addons/hw_blackbox_be/controllers/main.py15
-rw-r--r--addons/l10n_ch/i18n_extra/de.po2
-rw-r--r--addons/mail/models/ir_actions.py7
-rw-r--r--addons/mail/models/mail_channel.py3
-rw-r--r--addons/mail/static/src/js/chatter.js2
-rw-r--r--addons/mail/static/src/scss/followers.scss1
-rw-r--r--addons/mrp/report/mrp_report_bom_structure.py2
-rw-r--r--addons/mrp/tests/test_bom.py86
-rw-r--r--addons/payment/static/src/js/payment_form.js26
-rw-r--r--addons/point_of_sale/models/pos_order.py6
-rw-r--r--addons/point_of_sale/static/src/js/models.js3
-rw-r--r--addons/point_of_sale/tests/test_point_of_sale_flow.py55
-rw-r--r--addons/sale/data/sale_demo.xml1
-rw-r--r--addons/sale/models/res_company.py2
-rw-r--r--addons/sale/models/res_config_settings.py2
-rw-r--r--addons/sale/models/sale.py19
-rw-r--r--addons/sale/static/src/js/sale_portal_sidebar.js4
-rw-r--r--addons/sale_stock/models/sale_order.py3
-rw-r--r--addons/sale_timesheet/models/project_overview.py2
-rw-r--r--addons/stock/models/product.py59
-rw-r--r--addons/survey/views/assets.xml17
-rw-r--r--addons/survey/views/survey_templates.xml1
-rw-r--r--addons/transifex/models/ir_translation.py7
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js14
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js1
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js4
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js4
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_renderer.js10
-rw-r--r--addons/web/static/src/scss/bootstrap_overridden_report.scss2
-rw-r--r--addons/web/static/src/scss/layout_background.scss2
-rw-r--r--addons/web/static/src/scss/layout_boxed.scss2
-rw-r--r--addons/web/static/src/xml/base.xml11
-rw-r--r--addons/web/static/src/xml/menu.xml4
-rw-r--r--addons/web/static/tests/views/abstract_view_banner_tests.js40
-rw-r--r--addons/web/static/tests/views/calendar_tests.js45
-rw-r--r--addons/web/static/tests/views/search_view_tests.js24
-rw-r--r--addons/web/views/report_templates.xml3
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js22
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/media.js2
-rw-r--r--addons/web_editor/static/src/scss/bootstrap_overridden.scss2
-rw-r--r--addons/web_tour/static/src/js/running_tour_action_helper.js2
-rw-r--r--addons/website/static/src/js/backend/dashboard.js10
-rw-r--r--addons/website_sale/controllers/main.py1
-rw-r--r--addons/website_sale/models/sale_order.py2
-rw-r--r--addons/website_sale_delivery/views/website_sale_delivery_templates.xml10
-rw-r--r--addons/website_sale_link_tracker/controllers/backend.py7
-rw-r--r--addons/website_slides/controllers/main.py39
-rw-r--r--addons/website_slides/security/ir.model.access.csv2
-rw-r--r--addons/website_slides/static/src/js/slides_course_quiz.js7
-rw-r--r--addons/website_slides/static/src/xml/slide_quiz.xml2
-rw-r--r--addons/website_slides/views/website_slides_templates_lesson.xml24
67 files changed, 676 insertions, 229 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 487477dee6a..155064820bc 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -446,6 +446,7 @@ class AccountBankStatementLine(models.Model):
""" Prepare the dict of values to balance the move.
:param recordset move: the account.move to link the move line
+ :param dict move: a dict of vals of a account.move which will be created later
:param float amount: the amount of transaction that wasn't already reconciled
"""
company_currency = self.journal_id.company_id.currency_id
@@ -453,19 +454,23 @@ class AccountBankStatementLine(models.Model):
st_line_currency = self.currency_id or statement_currency
amount_currency = False
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
+ if isinstance(move, dict):
+ amount_sum = sum(x[2].get('amount_currency', 0) for x in move['line_ids'])
+ else:
+ amount_sum = sum(x.amount_currency for x in move.line_ids)
# We have several use case here to compare the currency and amount currency of counterpart line to balance the move:
if st_line_currency != company_currency and st_line_currency == statement_currency:
# company in currency A, statement in currency B and transaction in currency B
# counterpart line must have currency B and correct amount is inverse of already existing lines
- amount_currency = -sum([x.amount_currency for x in move.line_ids])
+ amount_currency = -amount_sum
elif st_line_currency != company_currency and statement_currency == company_currency:
# company in currency A, statement in currency A and transaction in currency B
# counterpart line must have currency B and correct amount is inverse of already existing lines
- amount_currency = -sum([x.amount_currency for x in move.line_ids])
+ amount_currency = -amount_sum
elif st_line_currency != company_currency and st_line_currency != statement_currency:
# company in currency A, statement in currency B and transaction in currency C
# counterpart line must have currency B and use rate between B and C to compute correct amount
- amount_currency = -sum([x.amount_currency for x in move.line_ids])/st_line_currency_rate
+ amount_currency = -amount_sum/st_line_currency_rate
elif st_line_currency == company_currency and statement_currency != company_currency:
# company in currency A, statement in currency B and transaction in currency A
# counterpart line must have currency B and amount is computed using the rate between A and B
@@ -474,9 +479,8 @@ class AccountBankStatementLine(models.Model):
# last case is company in currency A, statement in currency A and transaction in currency A
# and in this case counterpart line does not need any second currency nor amount_currency
- return {
+ aml_dict = {
'name': self.name,
- 'move_id': move.id,
'partner_id': self.partner_id and self.partner_id.id or False,
'account_id': amount >= 0 \
and self.statement_id.journal_id.default_credit_account_id.id \
@@ -487,22 +491,71 @@ class AccountBankStatementLine(models.Model):
'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False),
'amount_currency': amount_currency,
}
+ if isinstance(move, self.env['account.move'].__class__):
+ aml_dict['move_id'] = move.id
+ return aml_dict
+ @api.multi
def fast_counterpart_creation(self):
"""This function is called when confirming a bank statement and will allow to automatically process lines without
going in the bank reconciliation widget. By setting an account_id on bank statement lines, it will create a journal
entry using that account to counterpart the bank account
"""
+ payment_list = []
+ move_list = []
+ account_type_receivable = self.env.ref('account.data_account_type_receivable')
+ already_done_stmt_line_ids = [a['statement_line_id'][0] for a in self.env['account.move.line'].read_group([('statement_line_id', 'in', self.ids)], ['statement_line_id'], ['statement_line_id'])]
+ managed_st_line = []
for st_line in self:
# Technical functionality to automatically reconcile by creating a new move line
- if st_line.account_id and not st_line.journal_entry_ids.ids:
- vals = {
+ if st_line.account_id and not st_line.id in already_done_stmt_line_ids:
+ managed_st_line.append(st_line.id)
+ # Create payment vals
+ total = st_line.amount
+ payment_methods = (total > 0) and st_line.journal_id.inbound_payment_method_ids or st_line.journal_id.outbound_payment_method_ids
+ currency = st_line.journal_id.currency_id or st_line.company_id.currency_id
+ partner_type = 'customer' if st_line.account_id.user_type_id == account_type_receivable else 'supplier'
+ payment_list.append({
+ 'payment_method_id': payment_methods and payment_methods[0].id or False,
+ 'payment_type': total > 0 and 'inbound' or 'outbound',
+ 'partner_id': st_line.partner_id.id,
+ 'partner_type': partner_type,
+ 'journal_id': st_line.statement_id.journal_id.id,
+ 'payment_date': st_line.date,
+ 'state': 'reconciled',
+ 'currency_id': currency.id,
+ 'amount': abs(total),
+ 'communication': st_line._get_communication(payment_methods[0] if payment_methods else False),
+ 'name': st_line.statement_id.name or _("Bank Statement %s") % st_line.date,
+ })
+
+ # Create move and move line vals
+ move_vals = st_line._prepare_reconciliation_move(st_line.statement_id.name)
+ aml_dict = {
'name': st_line.name,
'debit': st_line.amount < 0 and -st_line.amount or 0.0,
'credit': st_line.amount > 0 and st_line.amount or 0.0,
'account_id': st_line.account_id.id,
+ 'partner_id': st_line.partner_id.id,
+ 'statement_line_id': st_line.id,
}
- st_line.process_reconciliation(new_aml_dicts=[vals])
+ st_line._prepare_move_line_for_currency(aml_dict, st_line.date or fields.Date.context_today())
+ move_vals['line_ids'] = [(0, 0, aml_dict)]
+ balance_line = self._prepare_reconciliation_move_line(move_vals, st_line.amount)
+ move_vals['line_ids'].append((0, 0, balance_line))
+ move_list.append(move_vals)
+
+ # Creates
+ payment_ids = self.env['account.payment'].create(payment_list)
+ for payment_id, move_vals in zip(payment_ids, move_list):
+ for line in move_vals['line_ids']:
+ line[2]['payment_id'] = payment_id.id
+ move_ids = self.env['account.move'].create(move_list)
+ move_ids.post()
+
+ for move, st_line, payment in zip(move_ids, self.browse(managed_st_line), payment_ids):
+ st_line.write({'move_name': move.name})
+ payment.write({'payment_reference': move.name})
def _get_communication(self, payment_method_id):
return self.name or ''
@@ -630,8 +683,6 @@ class AccountBankStatementLine(models.Model):
# Create move line(s). Either matching an existing journal entry (eg. invoice), in which
# case we reconcile the existing and the new move lines together, or being a write-off.
if counterpart_aml_dicts or new_aml_dicts:
- st_line_currency = self.currency_id or statement_currency
- st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
# Create the move
self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1
@@ -654,32 +705,12 @@ class AccountBankStatementLine(models.Model):
# Complete dicts to create both counterpart move lines and write-offs
to_create = (counterpart_aml_dicts + new_aml_dicts)
- company = self.company_id
date = self.date or fields.Date.today()
for aml_dict in to_create:
aml_dict['move_id'] = move.id
aml_dict['partner_id'] = self.partner_id.id
aml_dict['statement_line_id'] = self.id
- if st_line_currency.id != company_currency.id:
- aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
- aml_dict['currency_id'] = st_line_currency.id
- if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
- # Statement is in company currency but the transaction is in foreign currency
- aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
- aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
- elif self.currency_id and st_line_currency_rate:
- # Statement is in foreign currency and the transaction is in another one
- aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
- aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
- else:
- # Statement is in foreign currency and no extra currency is given for the transaction
- aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
- aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
- elif statement_currency.id != company_currency.id:
- # Statement is in foreign currency but the transaction is in company currency
- prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
- aml_dict['amount_currency'] = prorata_factor * self.amount
- aml_dict['currency_id'] = statement_currency.id
+ self._prepare_move_line_for_currency(aml_dict, date)
# Create write-offs
for aml_dict in new_aml_dicts:
@@ -729,6 +760,36 @@ class AccountBankStatementLine(models.Model):
counterpart_moves.assert_balanced()
return counterpart_moves
+ @api.multi
+ def _prepare_move_line_for_currency(self, aml_dict, date):
+ self.ensure_one()
+ company_currency = self.journal_id.company_id.currency_id
+ statement_currency = self.journal_id.currency_id or company_currency
+ st_line_currency = self.currency_id or statement_currency
+ st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
+ company = self.company_id
+
+ if st_line_currency.id != company_currency.id:
+ aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
+ aml_dict['currency_id'] = st_line_currency.id
+ if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
+ # Statement is in company currency but the transaction is in foreign currency
+ aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
+ aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
+ elif self.currency_id and st_line_currency_rate:
+ # Statement is in foreign currency and the transaction is in another one
+ aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
+ aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
+ else:
+ # Statement is in foreign currency and no extra currency is given for the transaction
+ aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
+ aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
+ elif statement_currency.id != company_currency.id:
+ # Statement is in foreign currency but the transaction is in company currency
+ prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
+ aml_dict['amount_currency'] = prorata_factor * self.amount
+ aml_dict['currency_id'] = statement_currency.id
+
def _check_invoice_state(self, invoice):
if invoice.state == 'in_payment' and all([payment.state == 'reconciled' for payment in invoice.mapped('payment_move_line_ids.payment_id')]):
invoice.write({'state': 'paid'})
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index ac5b23e0988..22c45f3feed 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -87,12 +87,14 @@ class AccountMove(models.Model):
if line.account_id.user_type_id.type in ('receivable', 'payable'):
amount = abs(line.debit - line.credit)
total_amount += amount
- for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
- total_reconciled += partial_line.amount
precision_currency = move.currency_id or move.company_id.currency_id
if float_is_zero(total_amount, precision_rounding=precision_currency.rounding):
move.matched_percentage = 1.0
else:
+ for line in move.line_ids:
+ if line.account_id.user_type_id.type in ('receivable', 'payable'):
+ for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
+ total_reconciled += partial_line.amount
move.matched_percentage = total_reconciled / total_amount
@api.multi
@@ -832,22 +834,21 @@ class AccountMoveLine(models.Model):
In case of full reconciliation, all moves belonging to the reconciliation will belong to the same account_full_reconcile object.
"""
# Get first all aml involved
- part_recs = self.env['account.partial.reconcile'].search(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)])
- amls = self
- todo = set(part_recs)
+ todo = self.env['account.partial.reconcile'].search_read(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)], ['debit_move_id', 'credit_move_id'])
+ amls = set(self.ids)
seen = set()
while todo:
- partial_rec = todo.pop()
- seen.add(partial_rec)
- for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
- if aml not in amls:
- amls += aml
- for x in aml.matched_debit_ids | aml.matched_credit_ids:
- if x not in seen:
- todo.add(x)
- partial_rec_ids = [x.id for x in seen]
+ aml_ids = [rec['debit_move_id'][0] for rec in todo if rec['debit_move_id']] + [rec['credit_move_id'][0] for rec in todo if rec['credit_move_id']]
+ amls |= set(aml_ids)
+ seen |= set([rec['id'] for rec in todo])
+ todo = self.env['account.partial.reconcile'].search_read(['&', '|', ('credit_move_id', 'in', aml_ids), ('debit_move_id', 'in', aml_ids), '!', ('id', 'in', list(seen))], ['debit_move_id', 'credit_move_id'])
+
+ partial_rec_ids = list(seen)
if not amls:
return
+ else:
+ amls = self.browse(list(amls))
+
# If we have multiple currency, we can only base ourselve on debit-credit to see if it is fully reconciled
currency = set([a.currency_id for a in amls if a.currency_id.id != False])
multiple_currency = False
diff --git a/addons/base_address_extended/data/base_address_extended_data.xml b/addons/base_address_extended/data/base_address_extended_data.xml
index 42a5a779589..ae25025fee2 100644
--- a/addons/base_address_extended/data/base_address_extended_data.xml
+++ b/addons/base_address_extended/data/base_address_extended_data.xml
@@ -7,5 +7,8 @@
<record id="base.mx" model="res.country">
<field eval="'%(street_name)s %(street_number)s/%(street_number2)s'" name="street_format" />
</record>
+ <record id="base.hr" model="res.country">
+ <field eval="'%(street_name)s %(street_number)s %(street_number2)s'" name="street_format" />
+ </record>
</data>
</odoo>
diff --git a/addons/base_address_extended/models/base_address_extended.py b/addons/base_address_extended/models/base_address_extended.py
index 0fd99cc8e63..ec788882d09 100644
--- a/addons/base_address_extended/models/base_address_extended.py
+++ b/addons/base_address_extended/models/base_address_extended.py
@@ -6,7 +6,7 @@ import re
from odoo import api, fields, models, _
from odoo.exceptions import UserError
-STREET_FIELDS = ('street_name', 'street_number', 'street_number2')
+STREET_FIELDS = ['street_name', 'street_number', 'street_number2']
class ResCountry(models.Model):
@@ -33,6 +33,10 @@ class Partner(models.Model):
street_number2 = fields.Char('Door', compute='_split_street', help="Door Number",
inverse='_set_street', store=True)
+ def _formatting_address_fields(self):
+ """Returns the list of address fields usable to format addresses."""
+ return super(Partner, self)._formatting_address_fields() + self.get_street_fields()
+
def get_street_fields(self):
"""Returns the fields that can be used in a street format.
Overwrite this function if you want to add your own fields."""
diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py
index 0f964c41f39..8626d76acb9 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
+import codecs
import collections
import unicodedata
@@ -32,6 +33,12 @@ DEFAULT_IMAGE_REGEX = r"(?:http|https)://.*(?:png|jpe?g|tiff?|gif|bmp)"
DEFAULT_IMAGE_CHUNK_SIZE = 32768
IMAGE_FIELDS = ["icon", "image", "logo", "picture"]
_logger = logging.getLogger(__name__)
+BOM_MAP = {
+ 'utf-16le': codecs.BOM_UTF16_LE,
+ 'utf-16be': codecs.BOM_UTF16_BE,
+ 'utf-32le': codecs.BOM_UTF32_LE,
+ 'utf-32be': codecs.BOM_UTF32_BE,
+}
try:
import xlrd
@@ -323,6 +330,13 @@ class Import(models.TransientModel):
encoding = options.get('encoding')
if not encoding:
encoding = options['encoding'] = chardet.detect(csv_data)['encoding'].lower()
+ # some versions of chardet (e.g. 2.3.0 but not 3.x) will return
+ # utf-(16|32)(le|be), which for python means "ignore / don't strip
+ # BOM". We don't want that, so rectify the encoding to non-marked
+ # IFF the guessed encoding is LE/BE and csv_data starts with a BOM
+ bom = BOM_MAP.get(encoding)
+ if bom and csv_data.startswith(bom):
+ encoding = options['encoding'] = encoding[:-2]
if encoding != 'utf-8':
csv_data = csv_data.decode(encoding).encode('utf-8')
diff --git a/addons/base_import/tests/test_csv_magic.py b/addons/base_import/tests/test_csv_magic.py
index 0a35a4731c1..b7975337361 100644
--- a/addons/base_import/tests/test_csv_magic.py
+++ b/addons/base_import/tests/test_csv_magic.py
@@ -26,18 +26,27 @@ class TestEncoding(ImportCase):
options.setdefault('separator', '\t')
test_text = "text\tnumber\tdate\tdatetime\n%s\t1.23.45,67\t\t\n" % text
for encoding in ['utf-8', 'utf-16', 'utf-32', *encodings]:
+ if isinstance(encoding, tuple):
+ encoding, es = encoding
+ else:
+ es = [encoding]
preview = self._make_import(
test_text.encode(encoding)).parse_preview(dict(options))
self.assertIsNone(preview.get('error'))
guessed = preview['options']['encoding']
- self.assertIsNotNone(guessed, encoding)
- self.assertEqual(codecs.lookup(guessed).name, codecs.lookup(encoding).name)
+ self.assertIsNotNone(guessed)
+ self.assertIn(
+ codecs.lookup(guessed).name, [
+ codecs.lookup(e).name
+ for e in es
+ ]
+ )
def test_autodetect_encoding(self):
""" Check that import preview can detect & return encoding
"""
- self._check_text("Iñtërnâtiônàlizætiøn", ['iso-8859-1'])
+ self._check_text("Iñtërnâtiônàlizætiøn", [('iso-8859-1', ['iso-8859-1', 'iso-8859-2'])])
self._check_text("やぶら小路の藪柑子。海砂利水魚の、食う寝る処に住む処、パイポパイポ パイポのシューリンガン。", ['eucjp', 'shift_jis', 'iso2022_jp'])
diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js
index 705c327a08f..2a7cad9a703 100644
--- a/addons/board/static/tests/dashboard_tests.js
+++ b/addons/board/static/tests/dashboard_tests.js
@@ -807,6 +807,12 @@ QUnit.test('save two searches to dashboard', async function (assert) {
await testUtils.dom.click(actionManager.$('.o_apply_filter'));
// Add it to dashboard
await testUtils.dom.click(actionManager.$('.o_favorites_menu_button'));
+ $('.o_search_options .dropdown-menu.o_favorites_menu').one('click', function (ev) {
+ // This handler is on the webClient
+ // But since the test suite doesn't have one
+ // We manually set it here
+ ev.stopPropagation();
+ });
await testUtils.dom.click(actionManager.$('.o_add_to_board'));
await testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button'));
// Remove it
@@ -819,6 +825,12 @@ QUnit.test('save two searches to dashboard', async function (assert) {
await testUtils.dom.click(actionManager.$('.o_apply_filter'));
// Add it to dashboard
await testUtils.dom.click(actionManager.$('.o_favorites_menu_button'));
+ $('.o_search_options .dropdown-menu.o_favorites_menu').one('click', function (ev) {
+ // This handler is on the webClient
+ // But since the test suite doesn't have one
+ // We manually set it here
+ ev.stopPropagation();
+ });
await testUtils.dom.click(actionManager.$('.o_add_to_board'));
await testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button'));
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 2c0c2e32877..957bb1bdc61 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -406,8 +406,9 @@ class AlarmManager(models.AbstractModel):
all_meetings = self.get_next_potential_limit_alarm('notification', partner_id=partner.id)
time_limit = 3600 * 24 # return alarms of the next 24 hours
- for meeting in self.env['calendar.event'].search([('id', 'in', list(all_meetings))]): # cannot browse
- max_delta = all_meetings[meeting.id]['max_duration']
+ for event_id in all_meetings:
+ max_delta = all_meetings[event_id]['max_duration']
+ meeting = self.env['calendar.event'].browse(event_id)
if meeting.recurrency:
b_found = False
last_found = False
diff --git a/addons/calendar/models/mail_message.py b/addons/calendar/models/mail_message.py
index 143bea2d135..58ac99e2825 100644
--- a/addons/calendar/models/mail_message.py
+++ b/addons/calendar/models/mail_message.py
@@ -26,7 +26,6 @@ class Message(models.Model):
def _find_allowed_model_wise(self, doc_model, doc_dict):
if doc_model == 'calendar.event':
order = self._context.get('order', self.env[doc_model]._order)
- records = self.env[doc_model].search([('id', 'in', list(doc_dict))])
- for virtual_id in records.get_recurrent_ids([], order=order):
+ for virtual_id in self.env[doc_model].with_context(active_test=False).search([('id', 'in', list(doc_dict))], order=order).ids:
doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
return super(Message, self)._find_allowed_model_wise(doc_model, doc_dict)
diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py
index 6444f110d38..bdb092ed5c8 100644
--- a/addons/delivery/models/stock_picking.py
+++ b/addons/delivery/models/stock_picking.py
@@ -42,7 +42,7 @@ class StockPicking(models.Model):
@api.one
- @api.depends('move_line_ids')
+ @api.depends('move_line_ids', 'move_line_ids.result_package_id')
def _compute_packages(self):
self.ensure_one()
packs = set()
@@ -52,7 +52,7 @@ class StockPicking(models.Model):
self.package_ids = list(packs)
@api.one
- @api.depends('move_line_ids')
+ @api.depends('move_line_ids', 'move_line_ids.result_package_id', 'move_line_ids.product_uom_id', 'move_line_ids.qty_done')
def _compute_bulk_weight(self):
weight = 0.0
for move_line in self.move_line_ids:
diff --git a/addons/digest/data/digest_template_data.xml b/addons/digest/data/digest_template_data.xml
index e3053b661d1..df64772f469 100644
--- a/addons/digest/data/digest_template_data.xml
+++ b/addons/digest/data/digest_template_data.xml
@@ -116,7 +116,7 @@
<table style="width: 100%; max-width: 600px; margin-top: 5px; border: 1px solid #e7e7e7;">
<tr>
<td style="border-collapse: collapse; background-color: #ffffff; line-height: 21px; padding: 0px 20px;"><br/>
- <div style="color: #3d466e; line-height: 23px;">${ctx['tip_description']|safe}</div>
+ <div style="color: #3d466e; line-height: 23px;">${tips | safe}</div>
</td>
</tr>
</table>
diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py
index 1797dd66bea..5292a490515 100644
--- a/addons/google_account/models/google_service.py
+++ b/addons/google_account/models/google_service.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
+from pprint import pformat
import json
import logging
@@ -183,13 +184,16 @@ class GoogleService(models.TransientModel):
except:
pass
except requests.HTTPError as error:
+ # https://developers.google.com/calendar/v3/errors
if error.response.status_code in (204, 404):
status = error.response.status_code
response = ""
else:
- _logger.exception("Bad google request : %s !", error.response.content)
+ req = json.loads(error.request.body)
+ res = error.response.json()
+ _logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", pformat(req), pformat(res))
if error.response.status_code in (400, 401, 410):
- raise error
+ raise UserError(_("Error while requesting Google Services: %s" % res['error']['message']))
raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google"))
return (status, response, ask_time)
diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py
index bf1c5fbd58f..cf7ec0d6549 100644
--- a/addons/hr_contract/models/hr_contract.py
+++ b/addons/hr_contract/models/hr_contract.py
@@ -29,16 +29,13 @@ class Employee(models.Model):
"""
Returns the contracts of the employee between date_from and date_to
"""
- # a contract is valid if it ends between the given dates
- clause_1 = ['&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)]
- # OR if it starts between the given dates
- clause_2 = ['&', ('date_start', '<=', date_to), ('date_start', '>=', date_from)]
- # OR if it starts before the date_from and finish after the date_end (or never finish)
- clause_3 = ['&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to)]
- clause_final = expression.AND([
- [('employee_id', 'in', self.ids), ('state', 'in', states)],
- expression.OR([clause_1, clause_2, clause_3])])
- return self.env['hr.contract'].search(clause_final)
+ return self.env['hr.contract'].search([
+ '&', '&', '&',
+ ('employee_id', 'in', self.ids),
+ ('state', 'in', states),
+ ('date_start', '<=', date_to),
+ '|', ('date_end', '=', False), ('date_end', '>=', date_from)
+ ])
@api.model
def _get_all_contracts(self, date_from, date_to, states=['open', 'pending']):
diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py
index d505192dfc8..1a1bacbb83b 100644
--- a/addons/hr_holidays/models/hr_leave_allocation.py
+++ b/addons/hr_holidays/models/hr_leave_allocation.py
@@ -12,6 +12,7 @@ from odoo import api, fields, models
from odoo.addons.resource.models.resource import HOURS_PER_DAY
from odoo.exceptions import AccessError, UserError
from odoo.tools.translate import _
+from odoo.tools.float_utils import float_round
_logger = logging.getLogger(__name__)
@@ -72,6 +73,8 @@ class HolidaysAllocation(models.Model):
number_of_hours_display = fields.Float(
'Duration (hours)', compute='_compute_number_of_hours_display',
help="UX field allowing to see and modify the allocation duration, computed in hours.")
+ duration_display = fields.Char('Allocated (Days/Hours)', compute='_compute_duration_display',
+ help="Field allowing to see the allocation duration in days or hours depending on the type_request_unit")
# details
parent_id = fields.Many2one('hr.leave.allocation', string='Parent')
linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests')
@@ -207,6 +210,16 @@ class HolidaysAllocation(models.Model):
allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
@api.multi
+ @api.depends('number_of_hours_display', 'number_of_days_display')
+ def _compute_duration_display(self):
+ for allocation in self:
+ allocation.duration_display = '%g %s' % (
+ (float_round(allocation.number_of_hours_display, precision_digits=2)
+ if allocation.type_request_unit == 'hour'
+ else float_round(allocation.number_of_days_display, precision_digits=2)),
+ _('hours') if allocation.type_request_unit == 'hour' else _('days'))
+
+ @api.multi
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_reset(self):
for allocation in self:
diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py
index 9b92f171d9e..73cb99d98d7 100644
--- a/addons/hr_holidays/models/hr_leave_type.py
+++ b/addons/hr_holidays/models/hr_leave_type.py
@@ -172,10 +172,16 @@ class HolidaysType(models.Model):
for request in requests:
status_dict = result[request.holiday_status_id.id]
- status_dict['virtual_remaining_leaves'] -= request.number_of_days
+ status_dict['virtual_remaining_leaves'] -= (request.number_of_hours_display
+ if request.leave_type_request_unit == 'hour'
+ else request.number_of_days)
if request.state == 'validate':
- status_dict['leaves_taken'] += request.number_of_days
- status_dict['remaining_leaves'] -= request.number_of_days
+ status_dict['leaves_taken'] += (request.number_of_hours_display
+ if request.leave_type_request_unit == 'hour'
+ else request.number_of_days)
+ status_dict['remaining_leaves'] -= (request.number_of_hours_display
+ if request.leave_type_request_unit == 'hour'
+ else request.number_of_days)
for allocation in allocations:
status_dict = result[allocation.holiday_status_id.id]
@@ -183,9 +189,15 @@ class HolidaysType(models.Model):
# note: add only validated allocation even for the virtual
# count; otherwise pending then refused allocation allow
# the employee to create more leaves than possible
- status_dict['virtual_remaining_leaves'] += allocation.number_of_days
- status_dict['max_leaves'] += allocation.number_of_days
- status_dict['remaining_leaves'] += allocation.number_of_days
+ status_dict['virtual_remaining_leaves'] += (allocation.number_of_hours_display
+ if allocation.type_request_unit == 'hour'
+ else allocation.number_of_days)
+ status_dict['max_leaves'] += (allocation.number_of_hours_display
+ if allocation.type_request_unit == 'hour'
+ else allocation.number_of_days)
+ status_dict['remaining_leaves'] += (allocation.number_of_hours_display
+ if allocation.type_request_unit == 'hour'
+ else allocation.number_of_days)
return result
@@ -273,7 +285,7 @@ class HolidaysType(models.Model):
'count': _('%g remaining out of %g') % (
float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0,
float_round(record.max_leaves, precision_digits=2) or 0.0,
- )
+ ) + (_(' hours') if record.request_unit == 'hour' else _(' days'))
}
res.append((record.id, name))
return res
diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml
index aad32a99128..0595df96407 100644
--- a/addons/hr_holidays/views/hr_leave_allocation_views.xml
+++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml
@@ -175,7 +175,7 @@
<field name="department_id"/>
<field name="holiday_status_id"/>
<field name="name"/>
- <field name="number_of_days" string="Allocated Days" sum="Total allocated days"/>
+ <field name="duration_display"/>
<field name="message_needaction" invisible="1"/>
<field name="state"/>
<button string="Approve" name="action_approve" type="object"
diff --git a/addons/hw_blackbox_be/controllers/main.py b/addons/hw_blackbox_be/controllers/main.py
index 36efed12e59..34dc06369e5 100644
--- a/addons/hw_blackbox_be/controllers/main.py
+++ b/addons/hw_blackbox_be/controllers/main.py
@@ -92,7 +92,7 @@ class Blackbox(Thread):
def _send_and_wait_for_ack(self, packet, serial):
ack = 0
- MAX_RETRIES = 1
+ MAX_RETRIES = 1 # no more than 9
while ack != 0x06 and int(chr(packet[4])) < MAX_RETRIES:
serial.write(packet)
@@ -104,7 +104,7 @@ class Blackbox(Thread):
# message so it's safe to do it. Also it would be a pain
# to have to throw this all the way back to js just so it
# can increment the retry counter and then try again.
- packet = packet[:4] + str(int(packet[4]) + 1) + packet[5:]
+ packet[4] += 1
if ack:
ack = ord(ack)
@@ -141,11 +141,11 @@ class Blackbox(Thread):
if stx == chr(0x02) and etx == chr(0x03) and bcc and self._lrc(response) == ord(bcc):
got_response = True
- ser.write(chr(0x06))
+ ser.write(chr(0x06).encode())
else:
_logger.warning("received ACK but not a valid response, sending NACK...")
sent_nacks += 1
- ser.write(chr(0x15))
+ ser.write(chr(0x15).encode())
if not got_response:
_logger.error("sent " + str(MAX_NACKS) + " NACKS without receiving response, giving up.")
@@ -173,4 +173,9 @@ if isfile("/home/pi/registered_blackbox_be"):
@http.route('/hw_proxy/request_serial/', type='json', auth='none', cors='*')
def request_serial(self):
- return subprocess.check_output("ifconfig eth0 | grep 'ether.*' | sed 's/://g' | awk '{print $2}'", shell=True).rstrip()[-7:]
+ try:
+ with open('/sys/class/net/eth0/address', 'rb') as f:
+ return f.read().rstrip().replace(b':', b'')[-7:]
+ except IOError as e:
+ _logger.warning("eth0 network interface MAC address could not be found")
+ return b''
diff --git a/addons/l10n_ch/i18n_extra/de.po b/addons/l10n_ch/i18n_extra/de.po
index 278f2bf8a0f..825220cd143 100644
--- a/addons/l10n_ch/i18n_extra/de.po
+++ b/addons/l10n_ch/i18n_extra/de.po
@@ -1099,7 +1099,7 @@ msgstr "Zu zahlende MwSt. bei 3,7% (LSt)"
#. module: l10n_ch
#: model:account.tax.template,name:l10n_ch.vat_77
msgid "TVA due a 7.7% (TN)"
-msgstr "Zu zahlende MwSt. bei 8,0% (NRZ)"
+msgstr "Zu zahlende MwSt. bei 7,7% (NRZ)"
#. module: l10n_ch
#: model:account.tax.template,name:l10n_ch.vat_77_purchase_return
diff --git a/addons/mail/models/ir_actions.py b/addons/mail/models/ir_actions.py
index 40582b1d803..56f60069024 100644
--- a/addons/mail/models/ir_actions.py
+++ b/addons/mail/models/ir_actions.py
@@ -70,7 +70,7 @@ class ServerActions(models.Model):
@api.model
def run_action_followers_multi(self, action, eval_context=None):
- Model = self.env[action.model_id.model]
+ Model = self.env[action.model_name]
if self.partner_ids or self.channel_ids and hasattr(Model, 'message_subscribe'):
records = Model.browse(self._context.get('active_ids', self._context.get('active_id')))
records.message_subscribe(self.partner_ids.ids, self.channel_ids.ids)
@@ -83,7 +83,7 @@ class ServerActions(models.Model):
When need to know it to skip these steps.
Except if the computed field is supposed to trigger the action
"""
- records = self.env[action.model_id.model].browse(
+ records = self.env[action.model_name].browse(
self._context.get('active_ids', self._context.get('active_id')))
old_values = action._context.get('old_values')
if old_values:
@@ -112,6 +112,7 @@ class ServerActions(models.Model):
# with wrong values in subsequent operations
cleaned_ctx = dict(self.env.context)
cleaned_ctx.pop('default_type', None)
+ cleaned_ctx.pop('default_parent_id', None)
action.template_id.with_context(cleaned_ctx).send_mail(self._context.get('active_id'), force_send=False, raise_exception=False)
return False
@@ -120,7 +121,7 @@ class ServerActions(models.Model):
if not action.activity_type_id or not self._context.get('active_id') or self._is_recompute(action):
return False
- records = self.env[action.model_id.model].browse(self._context.get('active_ids', self._context.get('active_id')))
+ records = self.env[action.model_name].browse(self._context.get('active_ids', self._context.get('active_id')))
vals = {
'summary': action.activity_summary or '',
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index b6bbe82407e..4b860440f6b 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -617,7 +617,8 @@ class Channel(models.Model):
# direct_partner should be removed from channel info since we can find it from members and channel_type
# we keep it know to avoid change tests and javascript
direct_partner = channel_partners.filtered(lambda pc: pc.partner_id.id != self.env.user.partner_id.id)
- info['direct_partner'] = [partner_infos[direct_partner[0].partner_id.id]]
+ if direct_partner:
+ info['direct_partner'] = [partner_infos[direct_partner[0].partner_id.id]]
# add needaction and unread counter, since the user is logged
info['message_needaction_counter'] = channel.message_needaction_counter
info['message_unread_counter'] = channel.message_unread_counter
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index 968242ffe6a..296cf092efb 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -366,7 +366,7 @@ var Chatter = Widget.extend({
this._fetchAttachments().then(this._openAttachmentBox.bind(this));
}
if (this.fields.thread) {
- this.trigger_up('reload', { fieldNames: ['message_attachment_count'] });
+ this.trigger_up('reload', { fieldNames: ['message_attachment_count'], keepChanges: true });
}
},
/**
diff --git a/addons/mail/static/src/scss/followers.scss b/addons/mail/static/src/scss/followers.scss
index ddf5c945994..86cbc38c82c 100644
--- a/addons/mail/static/src/scss/followers.scss
+++ b/addons/mail/static/src/scss/followers.scss
@@ -31,7 +31,6 @@
}
.o_followers_follow_button {
- width: 85px;
padding-right: 0;
color: $body-color;
font-weight: bold;
diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py
index b95c1bcf658..8e709a7df67 100644
--- a/addons/mrp/report/mrp_report_bom_structure.py
+++ b/addons/mrp/report/mrp_report_bom_structure.py
@@ -194,7 +194,7 @@ class ReportBomStructure(models.AbstractModel):
if line._skip_bom_line(product):
continue
if line.child_bom_id:
- qty = line.product_uom_id._compute_quantity(line.product_qty * factor, line.child_bom_id.product_uom_id)
+ qty = line.product_uom_id._compute_quantity(line.product_qty * factor, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty
sub_price = self._get_price(line.child_bom_id, qty, line.product_id)
price += sub_price
else:
diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py
index 6e4ed69d0b0..cacdf65189d 100644
--- a/addons/mrp/tests/test_bom.py
+++ b/addons/mrp/tests/test_bom.py
@@ -628,3 +628,89 @@ class TestBoM(TestMrpCommon):
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=red_car_without_gps.id)
# Same math than before but without GPS
self.assertEqual(report_values['lines']['total'], 210)
+
+ def test_bom_report_recursive_bom(self):
+ """ Test report with recursive BoM and different quantities.
+ BoM 1:
+ product = Finished (units)
+ quantity = 100 units
+ - Semi-Finished 5 kg
+
+ BoM 2:
+ product = Semi-Finished (kg)
+ quantity = 11 kg
+ - Assembly 2 dozens
+
+ BoM 3:
+ product = Assembly (dozens)
+ quantity = 5 dozens
+ - Raw Material 4 litres (product.product 5$/litre)
+
+ Check the Price for 80 units of Finished -> 2.92$:
+ """
+ # Create a products templates
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ uom_kg = self.env.ref('uom.product_uom_kgm')
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ uom_litre = self.env.ref('uom.product_uom_litre')
+
+ finished = self.env['product.product'].create({
+ 'name': 'Finished',
+ 'type': 'product',
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ })
+
+ semi_finished = self.env['product.product'].create({
+ 'name': 'Semi-Finished',
+ 'type': 'product',
+ 'uom_id': uom_kg.id,
+ 'uom_po_id': uom_kg.id,
+ })
+
+ assembly = self.env['product.product'].create({
+ 'name': 'Assembly',
+ 'type': 'product',
+ 'uom_id': uom_dozen.id,
+ 'uom_po_id': uom_dozen.id,
+ })
+
+ raw_material = self.env['product.product'].create({
+ 'name': 'Raw Material',
+ 'type': 'product',
+ 'uom_id': uom_litre.id,
+ 'uom_po_id': uom_litre.id,
+ 'standard_price': 5,
+ })
+
+ #Create bom
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = finished.product_tmpl_id
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = semi_finished
+ line.product_uom_id = uom_kg
+ line.product_qty = 5
+ bom_finished = bom_finished.save()
+
+ bom_semi_finished = Form(self.env['mrp.bom'])
+ bom_semi_finished.product_tmpl_id = semi_finished.product_tmpl_id
+ bom_semi_finished.product_qty = 11
+ with bom_semi_finished.bom_line_ids.new() as line:
+ line.product_id = assembly
+ line.product_uom_id = uom_dozen
+ line.product_qty = 2
+ bom_semi_finished = bom_semi_finished.save()
+
+ bom_assembly = Form(self.env['mrp.bom'])
+ bom_assembly.product_tmpl_id = assembly.product_tmpl_id
+ bom_assembly.product_qty = 5
+ with bom_assembly.bom_line_ids.new() as line:
+ line.product_id = raw_material
+ line.product_uom_id = uom_litre
+ line.product_qty = 4
+ bom_assembly = bom_assembly.save()
+
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_finished.id, searchQty=80)
+
+ self.assertAlmostEqual(report_values['lines']['total'], 2.92)
diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js
index c90b07875df..6036f946005 100644
--- a/addons/payment/static/src/js/payment_form.js
+++ b/addons/payment/static/src/js/payment_form.js
@@ -122,6 +122,18 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
}
},
+ disableButton: function (button) {
+ $(button).attr('disabled', true);
+ $(button).children('.fa-lock').removeClass('fa-lock');
+ $(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
+ },
+
+ enableButton: function (button) {
+ $(button).attr('disabled', false);
+ $(button).children('.fa').addClass('fa-lock');
+ $(button).find('span.o_loader').remove();
+ },
+
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -184,10 +196,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
return;
}
- $(button).attr('disabled', true);
- $(button).children('.fa-plus-circle').removeClass('fa-plus-circle');
- $(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
-
+ this.disableButton(button);
var verify_validity = this.$el.find('input[name="verify_validity"]');
if (verify_validity.length>0) {
@@ -225,14 +234,10 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
}
}
// here we remove the 'processing' icon from the 'add a new payment' button
- $(button).attr('disabled', false);
- $(button).children('.fa').addClass('fa-plus-circle');
- $(button).find('span.o_loader').remove();
+ self.enableButton(button);
}).guardedCatch(function (error) {
// if the rpc fails, pretty obvious
- $(button).attr('disabled', false);
- $(button).children('.fa').addClass('fa-plus-circle');
- $(button).find('span.o_loader').remove();
+ self.enableButton(button);
self.displayError(
_t('Server Error'),
@@ -300,6 +305,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
}
}
else { // if the user is using an old payment then we just submit the form
+ this.disableButton(button);
form.submit();
return new Promise(function () {});
}
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index b4144c4c9cd..52dc13a0a0f 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -237,11 +237,7 @@ class PosOrder(models.Model):
# Oldlin trick
invoice_line = InvoiceLine.sudo().new(inv_line)
invoice_line._onchange_product_id()
- invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id).ids
- fiscal_position_id = line.order_id.fiscal_position_id
- if fiscal_position_id:
- invoice_line.invoice_line_tax_ids = fiscal_position_id.map_tax(invoice_line.invoice_line_tax_ids, line.product_id, line.order_id.partner_id)
- invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.ids
+ invoice_line.invoice_line_tax_ids = [(6, False, line.tax_ids_after_fiscal_position.filtered(lambda t: t.company_id.id == line.order_id.company_id.id).ids)]
# We convert a new id object back to a dictionary to write to
# bridge between old and new api
inv_line = invoice_line._convert_to_write({name: invoice_line[name] for name in invoice_line._cache})
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 56f56dffb7f..10025258bde 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -880,6 +880,9 @@ exports.PosModel = Backbone.Model.extend({
}}).then(function () {
resolveInvoiced();
resolveDone();
+ }).guardedCatch(function (error) {
+ rejectInvoiced({code:401, message:'Backend Invoice', data:{order: order}});
+ rejectDone();
});
} else {
// The order has been pushed separately in batch when
diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py
index d76c660d0fc..30f9f8eae84 100644
--- a/addons/point_of_sale/tests/test_point_of_sale_flow.py
+++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py
@@ -671,3 +671,58 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
self.assertAlmostEqual(a, b)
for a, b in zip(sorted(amount_currency_lines), [-855.0, -777.27, -81.82, -45.0, 1752.75]):
self.assertAlmostEqual(a, b)
+
+ def test_order_to_invoice_no_tax(self):
+ # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl)
+ self.pos_order_pos1 = self.PosOrder.create({
+ 'company_id': self.company_id,
+ 'partner_id': self.partner1.id,
+ 'pricelist_id': self.partner1.property_product_pricelist.id,
+ 'lines': [(0, 0, {
+ 'name': "OL/0001",
+ 'product_id': self.product3.id,
+ 'price_unit': 450,
+ 'discount': 5.0,
+ 'qty': 2.0,
+ 'price_subtotal': 855,
+ 'price_subtotal_incl': 855,
+ }), (0, 0, {
+ 'name': "OL/0002",
+ 'product_id': self.product4.id,
+ 'price_unit': 300,
+ 'discount': 5.0,
+ 'qty': 3.0,
+ 'price_subtotal': 855,
+ 'price_subtotal_incl': 855,
+ })],
+ 'amount_tax': 855 * 2,
+ 'amount_total': 855 * 2,
+ 'amount_paid': 0.0,
+ 'amount_return': 0.0,
+ })
+
+ # I click on the "Make Payment" wizard to pay the PoS order
+ context_make_payment = {"active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id}
+ self.pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({
+ 'amount': 855 * 2,
+ })
+ # I click on the validate button to register the payment.
+ context_payment = {'active_id': self.pos_order_pos1.id}
+ self.pos_make_payment.with_context(context_payment).check()
+
+ # I check that the order is marked as paid and there is no invoice
+ # attached to it
+ self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.")
+ self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.')
+
+ # I generate an invoice from the order
+ res = self.pos_order_pos1.action_pos_order_invoice()
+ self.assertIn('res_id', res, "No invoice created")
+
+ # I test that the total of the attached invoice is correct
+ invoice = self.env['account.invoice'].browse(res['res_id'])
+ self.assertAlmostEqual(
+ invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct")
+
+ for iline in invoice.invoice_line_ids:
+ self.assertFalse(iline.invoice_line_tax_ids)
diff --git a/addons/sale/data/sale_demo.xml b/addons/sale/data/sale_demo.xml
index d83063b9158..0287ef3b43a 100644
--- a/addons/sale/data/sale_demo.xml
+++ b/addons/sale/data/sale_demo.xml
@@ -4,7 +4,6 @@
<!-- We want to activate pay and sign by default for easier demoing. -->
<record id="base.main_company" model="res.company">
- <field name="portal_confirmation_sign" eval="True"/>
<field name="portal_confirmation_pay" eval="True"/>
</record>
diff --git a/addons/sale/models/res_company.py b/addons/sale/models/res_company.py
index 439061adafa..913995fa5c7 100644
--- a/addons/sale/models/res_company.py
+++ b/addons/sale/models/res_company.py
@@ -7,7 +7,7 @@ from odoo import api, fields, models, _
class ResCompany(models.Model):
_inherit = "res.company"
- portal_confirmation_sign = fields.Boolean(string='Online Signature')
+ portal_confirmation_sign = fields.Boolean(string='Online Signature', default=True)
portal_confirmation_pay = fields.Boolean(string='Online Payment')
quotation_validity_days = fields.Integer(default=30, string="Default Quotation Validity (Days)")
diff --git a/addons/sale/models/res_config_settings.py b/addons/sale/models/res_config_settings.py
index 43cdf1d8c15..a07e90db278 100644
--- a/addons/sale/models/res_config_settings.py
+++ b/addons/sale/models/res_config_settings.py
@@ -42,7 +42,7 @@ class ResConfigSettings(models.TransientModel):
oldname='default_deposit_product_id',
help='Default product used for payment advances')
auto_done_setting = fields.Boolean("Lock Confirmed Sales", config_parameter='sale.auto_done_setting')
- module_website_sale_digital = fields.Boolean("Sell digital products - provide downloadable content on your customer portal")
+ module_website_sale_digital = fields.Boolean("Digital Content")
auth_signup_uninvited = fields.Selection([
('b2b', 'On invitation'),
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 044e4600af1..d8556c26190 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -500,6 +500,9 @@ class SaleOrder(models.Model):
invoices_origin = {}
invoices_name = {}
+ # Keep track of the sequences of the lines
+ # To keep lines under their section
+ inv_line_sequence = 0
for order in self:
group_key = order.id if grouped else (order.partner_invoice_id.id, order.currency_id.id)
@@ -508,6 +511,7 @@ class SaleOrder(models.Model):
# Create lines in batch to avoid performance problems
line_vals_list = []
+ # sequence is the natural order of order_lines
for line in order.order_line:
if line.display_type == 'line_section':
pending_section = line
@@ -529,14 +533,21 @@ class SaleOrder(models.Model):
if line.qty_to_invoice > 0 or (line.qty_to_invoice < 0 and final):
if pending_section:
- line_vals_list.extend(pending_section.invoice_line_create_vals(
+ section_invoice = pending_section.invoice_line_create_vals(
invoices[group_key].id,
pending_section.qty_to_invoice
- ))
+ )
+ inv_line_sequence += 1
+ section_invoice[0]['sequence'] = inv_line_sequence
+ line_vals_list.extend(section_invoice)
pending_section = None
- line_vals_list.extend(line.invoice_line_create_vals(
+
+ inv_line_sequence += 1
+ inv_line = line.invoice_line_create_vals(
invoices[group_key].id, line.qty_to_invoice
- ))
+ )
+ inv_line[0]['sequence'] = inv_line_sequence
+ line_vals_list.extend(inv_line)
if references.get(invoices.get(group_key)):
if order not in references[invoices[group_key]]:
diff --git a/addons/sale/static/src/js/sale_portal_sidebar.js b/addons/sale/static/src/js/sale_portal_sidebar.js
index f9ba54ad661..815c8254e18 100644
--- a/addons/sale/static/src/js/sale_portal_sidebar.js
+++ b/addons/sale/static/src/js/sale_portal_sidebar.js
@@ -71,7 +71,7 @@ publicWidget.registry.SalePortalSidebar = PortalSidebar.extend({
if (!text) {
break;
}
- lastLI = $("<li class='nav-item'>").append($('<a class="nav-link" href="#' + id + '"/>').text(text)).appendTo($bsSidenav);
+ lastLI = $("<li class='nav-item'>").append($('<a class="nav-link" style="max-width: 200px;" href="#' + id + '"/>').text(text)).appendTo($bsSidenav);
lastUL = false;
break;
case "h3":
@@ -84,7 +84,7 @@ publicWidget.registry.SalePortalSidebar = PortalSidebar.extend({
if (!lastUL) {
lastUL = $("<ul class='nav flex-column'>").appendTo(lastLI);
}
- $("<li class='nav-item'>").append($('<a class="nav-link" href="#' + id + '"/>').text(text)).appendTo(lastUL);
+ $("<li class='nav-item'>").append($('<a class="nav-link" style="max-width: 200px;" href="#' + id + '"/>').text(text)).appendTo(lastUL);
}
break;
}
diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py
index 0b38a867a4a..bac4703a498 100644
--- a/addons/sale_stock/models/sale_order.py
+++ b/addons/sale_stock/models/sale_order.py
@@ -443,7 +443,8 @@ class SaleOrderLine(models.Model):
def _update_line_quantity(self, values):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
- if self.mapped('qty_delivered') and float_compare(values['product_uom_qty'], max(self.mapped('qty_delivered')), precision_digits=precision) == -1:
+ line_products = self.filtered(lambda l: l.product_id.type in ['product', 'consu'])
+ if line_products.mapped('qty_delivered') and float_compare(values['product_uom_qty'], max(line_products.mapped('qty_delivered')), precision_digits=precision) == -1:
raise UserError(_('You cannot decrease the ordered quantity below the delivered quantity.\n'
'Create a return first.'))
super(SaleOrderLine, self)._update_line_quantity(values)
diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py
index af275ba9a3b..0e12add5f27 100644
--- a/addons/sale_timesheet/models/project_overview.py
+++ b/addons/sale_timesheet/models/project_overview.py
@@ -193,7 +193,7 @@ class Project(models.Model):
rows_sale_line[sale_line_row_key][-1] = 0
rows_sale_order = {} # so -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted]
- rows_sale_order_done_sold = dict.fromkeys(set(map_sol_so.values()) | set([None]), dict(sold=0.0, done=0.0)) # SO id -> {'sold':0.0, 'done': 0.0}
+ rows_sale_order_done_sold = {key : dict(sold=0.0, done=0.0) for key in set(map_sol_so.values()) | set([None])} # SO id -> {'sold':0.0, 'done': 0.0}
for row_key, row_sale_line in rows_sale_line.items():
sale_order_id = row_key[0]
# sale order row
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index 83b62e84d86..e819d9cdfb9 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -4,6 +4,7 @@
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError
+from odoo.osv import expression
from odoo.tools.float_utils import float_round
from datetime import datetime
import operator as py_operator
@@ -179,33 +180,45 @@ class Product(models.Model):
[('location_id.company_id', '=', False), ('location_dest_id.company_id', '=', company_id)],
[('location_id.company_id', '=', company_id), ('location_dest_id.company_id', '=', False),
])
- location_ids = []
- if self.env.context.get('location', False):
- if isinstance(self.env.context['location'], int):
- location_ids = [self.env.context['location']]
- elif isinstance(self.env.context['location'], str):
- domain = [('complete_name', 'ilike', self.env.context['location'])]
- if self.env.context.get('force_company', False):
- domain += [('company_id', '=', self.env.context['force_company'])]
- location_ids = self.env['stock.location'].search(domain).ids
+
+ def _search_ids(model, values, force_company_id):
+ ids = set()
+ domain = []
+ for item in values:
+ if isinstance(item, int):
+ ids.add(item)
+ else:
+ domain = expression.OR([[('name', 'ilike', item)], domain])
+ if force_company_id:
+ domain = expression.AND([[('company_id', '=', force_company_id)], domain])
+ if domain:
+ ids |= set(self.env[model].search(domain).ids)
+ return ids
+
+ # We may receive a location or warehouse from the context, either by explicit
+ # python code or by the use of dummy fields in the search view.
+ # Normalize them into a list.
+ location = self.env.context.get('location')
+ if location and not isinstance(location, list):
+ location = [location]
+ warehouse = self.env.context.get('warehouse')
+ if warehouse and not isinstance(warehouse, list):
+ warehouse = [warehouse]
+ force_company = self.env.context.get('force_company', False)
+ # filter by location and/or warehouse
+ if warehouse:
+ w_ids = set(Warehouse.browse(_search_ids('stock.warehouse', warehouse, force_company)).mapped('view_location_id').ids)
+ if location:
+ l_ids = _search_ids('stock.location', location, force_company)
+ location_ids = w_ids & l_ids
else:
- location_ids = self.env.context['location']
+ location_ids = w_ids
else:
- if self.env.context.get('warehouse', False):
- if isinstance(self.env.context['warehouse'], int):
- wids = [self.env.context['warehouse']]
- elif isinstance(self.env.context['warehouse'], str):
- domain = [('name', 'ilike', self.env.context['warehouse'])]
- if self.env.context.get('force_company', False):
- domain += [('company_id', '=', self.env.context['force_company'])]
- wids = Warehouse.search(domain).ids
- else:
- wids = self.env.context['warehouse']
+ if location:
+ location_ids = _search_ids('stock.location', location, force_company)
else:
- wids = Warehouse.search([]).ids
+ location_ids = set(Warehouse.search([]).mapped('view_location_id').ids)
- for w in Warehouse.browse(wids):
- location_ids.append(w.view_location_id.id)
return self._get_domain_locations_new(location_ids, company_id=self.env.context.get('force_company', False), compute_child=self.env.context.get('compute_child', True))
def _get_domain_locations_new(self, location_ids, company_id=False, compute_child=True):
diff --git a/addons/survey/views/assets.xml b/addons/survey/views/assets.xml
index be87a96529f..6b171b74ebd 100644
--- a/addons/survey/views/assets.xml
+++ b/addons/survey/views/assets.xml
@@ -4,10 +4,7 @@
<!-- survey assets -->
<template id="survey_assets" name="Survey Results assets">
<script src="/web/static/lib/Chart/Chart.js"></script>
- <script type="text/javascript" src="/web/static/src/js/public/public_root.js"/>
- <script type="text/javascript" src="/web/static/src/js/public/public_root_instance.js"/>
- <script type="text/javascript" src="/web/static/src/js/public/public_widget.js"/>
- <script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
+
<script type="text/javascript" src="/web/static/src/js/fields/field_utils.js"></script>
<script type="text/javascript" src="/survey/static/src/js/survey_result.js" />
@@ -18,6 +15,16 @@
<script type="text/javascript" src="/survey/static/src/js/tour_test_certification_failure.js"></script>
<script type="text/javascript" src="/survey/static/src/js/tour_test_survey_prefill.js"></script>
+ <link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
+ <link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
+ </template>
+
+ <template id="survey_web_assets" name="Survey Web Assets">
+ <script type="text/javascript" src="/web/static/src/js/public/public_root.js"/>
+ <script type="text/javascript" src="/web/static/src/js/public/public_root_instance.js"/>
+ <script type="text/javascript" src="/web/static/src/js/public/public_widget.js"/>
+ <script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
+
<t t-call="web._assets_helpers">
<t t-call="web._assets_frontend_helpers"/>
</t>
@@ -25,8 +32,6 @@
<link rel="stylesheet" type="text/less" href="/web/static/src/scss/fonts.scss" />
<!-- Some helper classes (like mt- mb-) are required for survey templates -->
<link rel="stylesheet" type="text/less" href="/web_editor/static/src/scss/web_editor.common.scss"/>
- <link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
- <link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
<link rel="stylesheet" type="text/scss" href="/survey/static/src/scss/survey_templates.scss"/>
</template>
diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml
index 26ac5981768..48156259f28 100644
--- a/addons/survey/views/survey_templates.xml
+++ b/addons/survey/views/survey_templates.xml
@@ -142,6 +142,7 @@
};
</script>
<t t-call-assets="web.assets_common"/>
+ <t t-call-assets="survey.survey_web_assets"/>
<t t-call-assets="survey.survey_assets"/>
</t>
<body>
diff --git a/addons/transifex/models/ir_translation.py b/addons/transifex/models/ir_translation.py
index c19f66c39c1..51edae39206 100644
--- a/addons/transifex/models/ir_translation.py
+++ b/addons/transifex/models/ir_translation.py
@@ -84,8 +84,7 @@ class IrTranslation(models.Model):
'project': project,
'lang': lang_code,
'module': translation.module,
- 'src': werkzeug.url_quote_plus(
- "text:'" +
- translation.source[:50].replace("'", "\\'") +
- "'"),
+ 'src': "text:'" + werkzeug.url_quote_plus(
+ translation.source[:50].replace("\n", "").replace("'", "")
+ ) + "'",
}
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 68ac157868d..c27311d4df5 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -31,7 +31,9 @@ var qweb = core.qweb;
var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMixin, {
dependencies: ['notification'],
- events: _.extend(KeyboardNavigationMixin.events, {}),
+ events: _.extend({}, KeyboardNavigationMixin.events, {
+ 'click .o_search_options .dropdown-menu': '_onClickDropDownMenu',
+ }),
custom_events: {
clear_uncommitted_changes: function (e) {
this.clear_uncommitted_changes().then(e.data.callback);
@@ -334,6 +336,16 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
//--------------------------------------------------------------------------
/**
+ * When clicking inside a dropdown to modify search options
+ * prevents the bootstrap dropdown to close on itself
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onClickDropDownMenu: function (ev) {
+ ev.stopPropagation();
+ },
+ /**
* Whenever the connection is lost, we need to notify the user.
*
* @private
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index c7a80864a15..607f49c8031 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -495,7 +495,6 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
additional_context: _.extend({}, data.context)
});
}
- return false;
},
/**
* FIXME: this logic should be rethought
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index dda3978635b..61a966972aa 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -204,7 +204,9 @@ var CalendarController = AbstractController.extend({
* @param {OdooEvent} event
*/
_onDropRecord: function (event) {
- this._updateRecord(event.data);
+ this._updateRecord(_.extend({}, event.data, {
+ 'drop': true,
+ }));
},
/**
* @private
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index ba338a96e4d..0ad1b61e4d7 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -105,7 +105,9 @@ return AbstractModel.extend({
}
if (this.mapping.date_delay) {
- data[this.mapping.date_delay] = (end.diff(start) <= 0 ? end.endOf('day').diff(start) : end.diff(start)) / 1000 / 3600;
+ if (this.data.scale !== 'month' || (this.data.scale === 'month' && !event.drop)) {
+ data[this.mapping.date_delay] = (end.diff(start) <= 0 ? end.endOf('day').diff(start) : end.diff(start)) / 1000 / 3600;
+ }
}
return data;
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
index a1da2925eb6..5ce53b20806 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
@@ -18,7 +18,6 @@ var ControlPanelRenderer = Renderer.extend({
get_action_info: '_onGetActionInfo',
},
events: _.extend({}, Renderer.prototype.events, {
- 'click.bs.dropdown .o_search_options .dropdown-menu': '_onDropdownClicked',
'click .o_searchview_more': '_onMore',
}),
@@ -368,15 +367,6 @@ var ControlPanelRenderer = Renderer.extend({
//--------------------------------------------------------------------------
/**
- * Prevent the search dropdowns from closing when clicking inside them.
- *
- * @private
- * @param {MouseEvent} ev
- */
- _onDropdownClicked: function (ev) {
- ev.stopPropagation();
- },
- /**
* Toggle the search menus visibility.
*
* @private
diff --git a/addons/web/static/src/scss/bootstrap_overridden_report.scss b/addons/web/static/src/scss/bootstrap_overridden_report.scss
new file mode 100644
index 00000000000..a6e1cd23b32
--- /dev/null
+++ b/addons/web/static/src/scss/bootstrap_overridden_report.scss
@@ -0,0 +1,2 @@
+// remove Noto Color Emoji
+$font-family-sans-serif: "Noto", "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
diff --git a/addons/web/static/src/scss/layout_background.scss b/addons/web/static/src/scss/layout_background.scss
index caeb1f0c2bf..93ac948b2ad 100644
--- a/addons/web/static/src/scss/layout_background.scss
+++ b/addons/web/static/src/scss/layout_background.scss
@@ -1,5 +1,5 @@
.o_background_footer, .o_background_header, .o_report_layout_background {
- font-family: 'Lato';
+ font-family: 'Lato', 'Noto';
color: gray('700');
}
.o_background_header {
diff --git a/addons/web/static/src/scss/layout_boxed.scss b/addons/web/static/src/scss/layout_boxed.scss
index bf19b6fb7ae..2129f583b21 100644
--- a/addons/web/static/src/scss/layout_boxed.scss
+++ b/addons/web/static/src/scss/layout_boxed.scss
@@ -1,5 +1,5 @@
.o_boxed_footer, .o_boxed_header, .o_report_layout_boxed {
- font-family: 'Lato';
+ font-family: 'Lato' 'Noto';
color: gray('700');
font-size: 12px;
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index f9db8432275..564bbc8582e 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -1369,13 +1369,14 @@
<div role="menuitem" class="dropdown-item-text o_favorite_name">
<input type="text" class="o_input"></input>
</div>
+ <t t-set="favId" t-value="_.uniqueId('_checkbox_')" />
<div class="custom-control custom-checkbox">
- <input type="checkbox" id="o_favorite_use_by_default" class="custom-control-input"/>
- <label for="o_favorite_use_by_default" class="custom-control-label">Use by default</label>
+ <input type="checkbox" t-attf-id="o_favorite_use_by_default#{favId}" class="custom-control-input"/>
+ <label t-attf-for="o_favorite_use_by_default#{favId}" class="custom-control-label">Use by default</label>
</div>
<div class="custom-control custom-checkbox">
- <input type="checkbox" id="o_favorite_share_all_users" class="custom-control-input"/>
- <label for="o_favorite_share_all_users" class="custom-control-label">Share with all users <span class="fa fa-users" role="img" aria-label="Users" title="Users"/></label>
+ <input type="checkbox" t-attf-id="o_favorite_share_all_users#{favId}" class="custom-control-input"/>
+ <label t-attf-for="o_favorite_share_all_users#{favId}" class="custom-control-label">Share with all users <span class="fa fa-users" role="img" aria-label="Users" title="Users"/></label>
</div>
<div class="dropdown-item-text o_save_favorite">
<button type="button" class="btn btn-primary">Save</button>
@@ -1559,7 +1560,7 @@
<t t-name="SwitchCompanyMenu">
<li class="o_switch_company_menu">
- <a role="button" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" href="#" aria-label="Dropdown menu" title="Dropdown menu">
+ <a role="button" class="dropdown-toggle" data-toggle="dropdown" data-display="static" aria-expanded="false" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span t-attf-class="#{widget.isMobile ? 'fa fa-building-o' : 'oe_topbar_name'}"/>
</a>
<div class="dropdown-menu dropdown-menu-right" role="menu"/>
diff --git a/addons/web/static/src/xml/menu.xml b/addons/web/static/src/xml/menu.xml
index 3838cc6c226..6a4749d1c25 100644
--- a/addons/web/static/src/xml/menu.xml
+++ b/addons/web/static/src/xml/menu.xml
@@ -15,7 +15,7 @@
-->
<t t-name="AppsMenu">
<li class="dropdown">
- <a class="full" data-toggle="dropdown" href="#">
+ <a class="full" data-toggle="dropdown" data-display="static" href="#">
<i class="fa fa-th-large"/>
</a>
<div class="dropdown-menu" role="menu">
@@ -77,7 +77,7 @@
</t>
<t t-else="">
<li>
- <a href="#" class="dropdown-toggle o-no-caret o_menu_header_lvl_1" t-att-data-menu-xmlid="second_level_menu.xmlid" data-toggle="dropdown" role="button" aria-expanded="false">
+ <a href="#" class="dropdown-toggle o-no-caret o_menu_header_lvl_1" t-att-data-menu-xmlid="second_level_menu.xmlid" data-toggle="dropdown" data-display="static" role="button" aria-expanded="false">
<t t-esc="second_level_menu.name"/>
</a>
<div class="dropdown-menu" role="menu">
diff --git a/addons/web/static/tests/views/abstract_view_banner_tests.js b/addons/web/static/tests/views/abstract_view_banner_tests.js
index ab668dd8c0d..e5aa14faccc 100644
--- a/addons/web/static/tests/views/abstract_view_banner_tests.js
+++ b/addons/web/static/tests/views/abstract_view_banner_tests.js
@@ -40,13 +40,33 @@ QUnit.module('Views', {
QUnit.test("The banner should be fetched from the route", function (assert) {
var done = assert.async();
- assert.expect(5);
+ assert.expect(6);
- var banner_html =
- '<div>' +
- '<link type="text/css" href="' + test_css_url + '" rel="stylesheet">' +
- '<div class="hello_banner"/>' +
- '</div>';
+ var banner_html =`
+ <div class="modal o_onboarding_modal o_technical_modal" tabindex="-1" role="dialog">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-footer">
+ <a type="action" class="btn btn-primary" data-dismiss="modal"
+ data-toggle="collapse" href=".o_onboarding_container">
+ Remove
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="o_onboarding_container collapse show">
+ <div class="o_onboarding_wrap">
+ <a href="#" data-toggle="modal" data-target=".o_onboarding_modal"
+ class="float-right o_onboarding_btn_close">
+ <i class="fa fa-times" title="Close the onboarding panel" />
+ </a>
+ </div>
+ <div>
+ <link type="text/css" href="` + test_css_url + `" rel="stylesheet">
+ <div class="hello_banner">Here is the banner</div>
+ </div>
+ </div>`;
createView({
View: TestView,
@@ -60,7 +80,7 @@ QUnit.module('Views', {
}
return this._super(route, args);
},
- }).then(function (view) {
+ }).then(async function (view) {
var $banner = view.$('.hello_banner');
assert.strictEqual($banner.length, 1,
"The view should contain the response from the controller.");
@@ -73,6 +93,12 @@ QUnit.module('Views', {
var $banner_link = $('link[href$="' + test_css_url + '"]', $banner);
assert.strictEqual($banner_link.length, 0,
"The stylesheet should have been removed from the banner.");
+
+ await testUtils.dom.click(view.$('.o_onboarding_btn_close')); // click on close to remove banner
+ await testUtils.dom.click(view.$('.o_technical_modal .btn-primary:contains("Remove")')); // click on button remove from techinal modal
+ assert.strictEqual(view.$('.o_onboarding_container.show').length, 0,
+ "Banner should be removed from the view");
+
view.destroy();
done();
});
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 1a10b487c3d..34e09541d54 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -42,6 +42,7 @@ QUnit.module('Views', {
stop_date: {string: "stop date", type: "date"},
start: {string: "start datetime", type: "datetime"},
stop: {string: "stop datetime", type: "datetime"},
+ delay: {string: "delay", type: "float"},
allday: {string: "allday", type: "boolean"},
partner_ids: {string: "attendees", type: "one2many", relation: 'partner', default: [[6, 0, [1]]]},
type: {string: "type", type: "integer"},
@@ -2383,6 +2384,50 @@ QUnit.module('Views', {
await testUtils.nextTick();
assert.verifySteps(["2017-01-07 00:00:00"]);
assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "01/07/2017");
+ calendar.destroy();
+ });
+
+ QUnit.test('drag and drop on month mode with date_start and date_delay', async function (assert) {
+ assert.expect(1);
+
+ var calendar = await createCalendarView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar date_start="start" date_delay="delay" mode="month">'+
+ '<field name="name"/>'+
+ '<field name="start"/>'+
+ '<field name="delay"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ mockRPC: function (route, args) {
+ if (args.method === "write") {
+ // delay should not be written at drag and drop
+ assert.equal(args.args[1].delay, undefined)
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // Create event (on 20 december)
+ var $cell = calendar.$('.fc-day-grid .fc-row:eq(3) .fc-day:eq(2)');
+ testUtils.triggerMouseEvent($cell, "mousedown");
+ testUtils.triggerMouseEvent($cell, "mouseup");
+ await testUtils.nextTick();
+ var $input = $('.modal-body input:first');
+ await testUtils.fields.editInput($input, "An event");
+ await testUtils.dom.click($('.modal button.btn:contains(Create)'));
+ await testUtils.nextTick();
+
+ // Move event to another day (on 27 november)
+ await testUtils.dragAndDrop(
+ calendar.$('.fc-event').first(),
+ calendar.$('.fc-day-top').first()
+ );
calendar.destroy();
});
diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js
index 4279f5b9c39..43282b52f33 100644
--- a/addons/web/static/tests/views/search_view_tests.js
+++ b/addons/web/static/tests/views/search_view_tests.js
@@ -1280,7 +1280,7 @@ QUnit.module('Search View', {
});
QUnit.test('Customizing filter does not close the filter dropdown', async function (assert) {
- assert.expect(3);
+ assert.expect(5);
var self = this;
_.each(this.data.partner.records.slice(), function (rec) {
@@ -1304,6 +1304,13 @@ QUnit.module('Search View', {
'partner,false,search': '<search><field name="date_field"/></search>',
},
res_id: 1,
+ intercepts: {
+ create_filter: function (ev) {
+ var data = ev.data;
+ assert.strictEqual(data.filter.name, 'Fire on the bayou');
+ assert.strictEqual(data.filter.is_default, true);
+ },
+ },
});
await testUtils.fields.many2one.clickOpenDropdown('bar');
@@ -1327,9 +1334,22 @@ QUnit.module('Search View', {
$input.click();
await testUtils.nextTick();
});
-
assert.isVisible($filterDropdown);
+ // Favorites Menu
+ var $modal = $('.modal');
+ await testUtils.dom.click($modal.find('.o_favorites_menu_button'));
+ await testUtils.dom.click($modal.find('.o_add_favorite'));
+ $modal.find('.o_search_options .dropdown-menu').one('click', function (ev) {
+ // This handler is on the webClient
+ // But since the test suite doesn't have one
+ // We manually set it here
+ ev.stopPropagation();
+ });
+ await testUtils.fields.editInput($modal.find('div.o_favorite_name input'), 'Fire on the bayou');
+ await testUtils.dom.click($modal.find('.o_add_favorite ~ div label:contains(Use by default)'));
+ await testUtils.dom.click($modal.find('.o_save_favorite button'));
+
form.destroy();
});
});
diff --git a/addons/web/views/report_templates.xml b/addons/web/views/report_templates.xml
index ea963fb926b..4865160be0e 100644
--- a/addons/web/views/report_templates.xml
+++ b/addons/web/views/report_templates.xml
@@ -5,7 +5,8 @@
<template id="report_assets_common">
<t t-call="web._assets_helpers">
<!-- FIXME web_editor ? -->
- <link rel="stylesheet" type="text/scss" href="/web_editor/static/src/scss/bootstrap_overridden.scss"/>
+ <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/bootstrap_overridden_report.scss"/>
+ <link rel="stylesheet" type="text/scss" href="/web_editor/static/src/scss/bootstrap_overridden.scss"/>
</t>
<t t-call="web._assets_bootstrap"/>
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js b/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
index 74703b90e45..255561b02be 100644
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
+++ b/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
@@ -19,6 +19,7 @@ var DropzonePlugin = Plugins.dropzone.extend({
attachDragAndDropEvent: function () {
this._super.apply(this, arguments);
this.$dropzone.off('drop');
+ this.$dropzone.on('dragenter', this._onDragenter.bind(this));
this.$dropzone.on('drop', this._onDrop.bind(this));
},
@@ -71,7 +72,7 @@ var DropzonePlugin = Plugins.dropzone.extend({
if (range.so >= dom.nodeLength(range.sc)) {
$(range.sc).append(spinner);
} else {
- $(range.sc).before(range.sc.childNodes[range.so]);
+ $(range.sc.childNodes[range.so]).before(spinner);
}
} else {
range.sc.splitText(range.so);
@@ -151,7 +152,7 @@ var DropzonePlugin = Plugins.dropzone.extend({
route: '/web_editor/add_image_base64',
params: {
res_model: options.res_model,
- res_id: options.res_id,
+ res_id: options.res_id || 0,
image_base64: imageBase64.split(';base64,').pop(),
filename: fileName,
},
@@ -161,9 +162,26 @@ var DropzonePlugin = Plugins.dropzone.extend({
* @private
* @param {JQueryEvent} e
*/
+ _onDragenter: function (e) {
+ var range = this.context.invoke('editor.createRange');
+ if (!this.editable.contains(range.sc)) {
+ var node = this.editable;
+ if (node && node.firstChild) {
+ node = node.firstChild;
+ }
+ range = this.context.invoke('editor.setRange', node, 0, node, 0);
+ range.select();
+ }
+ },
+ /**
+ * @private
+ * @param {JQueryEvent} e
+ */
_onDrop: function (e) {
e.preventDefault();
+ this.$editor.removeClass('dragover');
+
if (this.options.disableDragAndDrop) {
return;
}
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
index 85ac5f222f4..e6a598a2813 100644
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
+++ b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
@@ -43,6 +43,8 @@ var MediaPlugin = AbstractPlugin.extend({
'dblclick .note-editable': '_onDblclick',
},
+ mousePosition: {},
+
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
diff --git a/addons/web_editor/static/src/scss/bootstrap_overridden.scss b/addons/web_editor/static/src/scss/bootstrap_overridden.scss
index 4517852076f..d1644a1174e 100644
--- a/addons/web_editor/static/src/scss/bootstrap_overridden.scss
+++ b/addons/web_editor/static/src/scss/bootstrap_overridden.scss
@@ -55,5 +55,3 @@ $gray-700: gray('700') !default;
$gray-800: gray('800') !default;
$gray-900: gray('900') !default;
$black: gray('black') !default;
-
-$font-family-sans-serif: "Noto", "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
diff --git a/addons/web_tour/static/src/js/running_tour_action_helper.js b/addons/web_tour/static/src/js/running_tour_action_helper.js
index 479f3a2ea78..a9bc1c220c0 100644
--- a/addons/web_tour/static/src/js/running_tour_action_helper.js
+++ b/addons/web_tour/static/src/js/running_tour_action_helper.js
@@ -87,7 +87,7 @@ var RunningTourActionHelper = core.Class.extend({
$options.prop("selected", false).removeProp("selected");
var $selectedOption = $options.filter(function () { return $(this).val() === text; });
if ($selectedOption.length === 0) {
- $selectedOption = $options.filter(function () { return $(this).text() === text; });
+ $selectedOption = $options.filter(function () { return $(this).text().trim() === text; });
}
$selectedOption.prop("selected", true);
this._click(values);
diff --git a/addons/website/static/src/js/backend/dashboard.js b/addons/website/static/src/js/backend/dashboard.js
index 83c16278b6d..dda40102904 100644
--- a/addons/website/static/src/js/backend/dashboard.js
+++ b/addons/website/static/src/js/backend/dashboard.js
@@ -45,8 +45,8 @@ var Dashboard = AbstractAction.extend({
this._super(parent, context);
this.date_range = 'week'; // possible values : 'week', 'month', year'
- this.date_from = moment().subtract(1, 'week');
- this.date_to = moment();
+ this.date_from = moment.utc().subtract(1, 'week');
+ this.date_to = moment.utc();
this.dashboards_templates = ['website.dashboard_header', 'website.dashboard_content'];
this.graphs = [];
@@ -303,13 +303,13 @@ var Dashboard = AbstractAction.extend({
on_date_range_button: function(date_range) {
if (date_range === 'week') {
this.date_range = 'week';
- this.date_from = moment().subtract(1, 'weeks');
+ this.date_from = moment.utc().subtract(1, 'weeks');
} else if (date_range === 'month') {
this.date_range = 'month';
- this.date_from = moment().subtract(1, 'months');
+ this.date_from = moment.utc().subtract(1, 'months');
} else if (date_range === 'year') {
this.date_range = 'year';
- this.date_from = moment().subtract(1, 'years');
+ this.date_from = moment.utc().subtract(1, 'years');
} else {
console.log('Unknown date range. Choose between [week, month, year]');
return;
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 131ec4adaff..0429f21de5f 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -973,6 +973,7 @@ class WebsiteSale(http.Controller):
return request.redirect('/shop')
if order and not order.amount_total and not tx:
+ order.with_context(send_email=True).action_confirm()
return request.redirect(order.get_portal_url())
# clean context and session, then redirect to the confirmation page
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index dfe942d2dd8..8228027fced 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -325,7 +325,7 @@ class SaleOrder(models.Model):
'view_id': composer_form_view_id,
'target': 'new',
'context': {
- 'default_composition_mode': 'mass_mail',
+ 'default_composition_mode': 'mass_mail' if len(self.ids) > 1 else 'comment',
'default_res_id': self.ids[0],
'default_model': 'sale.order',
'default_use_template': bool(template_id),
diff --git a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
index d0c692177ba..573acdf0bc4 100644
--- a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
+++ b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
@@ -27,8 +27,14 @@
<label class="label-optional" t-field="delivery.name"/>
<t t-set='badge_class' t-value="(delivery_nb != 1 and 'float-right ' or '') + 'badge badge-secondary'" />
<t t-if="delivery.delivery_type == 'fixed'">
- <span t-if="delivery.fixed_price > 0.0" t-att-class="badge_class" t-field="delivery.fixed_price" t-options='{"widget": "monetary", "from_currency": delivery.product_id.company_id.currency_id, "display_currency": website_sale_order.currency_id}'/>
- <span t-else="" t-att-class="badge_class">Free</span>
+ <span t-if="delivery.fixed_price > 0.0" t-att-class="badge_class">
+ <t t-set="monetary_options" t-value='{"widget": "monetary",
+ "from_currency": delivery.product_id.company_id.currency_id,
+ "display_currency": website_sale_order.currency_id}'/>
+ <t t-if="delivery.free_over" t-esc="delivery.rate_shipment(website_sale_order)['price']" t-options='monetary_options'/>
+ <t t-else="" t-esc="delivery.fixed_price" t-options='monetary_options'/>
+ </span>
+ <span t-else="" t-att-class="badge_class">Free</span>
</t>
<t t-else="">
<span t-attf-class="#{badge_class} o_wsale_delivery_badge_price">Select to compute delivery rate</span>
diff --git a/addons/website_sale_link_tracker/controllers/backend.py b/addons/website_sale_link_tracker/controllers/backend.py
index 79e3aa586f3..34d45f3fa29 100644
--- a/addons/website_sale_link_tracker/controllers/backend.py
+++ b/addons/website_sale_link_tracker/controllers/backend.py
@@ -1,8 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime, time
+
from odoo import http
from odoo.addons.website_sale.controllers.backend import WebsiteSaleBackend
+from odoo.fields import Date
from odoo.http import request
@@ -10,8 +13,10 @@ class WebsiteSaleLinkTrackerBackend(WebsiteSaleBackend):
@http.route()
def fetch_dashboard_data(self, website_id, date_from, date_to):
+ datetime_from = datetime.combine(Date.from_string(date_from), time.min)
+ datetime_to = datetime.combine(Date.from_string(date_to), time.max)
results = super(WebsiteSaleLinkTrackerBackend, self).fetch_dashboard_data(website_id, date_from, date_to)
- results['dashboards']['sales']['utm_graph'] = self.fetch_utm_data(date_from, date_to)
+ results['dashboards']['sales']['utm_graph'] = self.fetch_utm_data(datetime_from, datetime_to)
return results
def fetch_utm_data(self, date_from, date_to):
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index edb00cc0e7b..b18987a2f10 100644
--- a/addons/website_slides/controllers/main.py
+++ b/addons/website_slides/controllers/main.py
@@ -119,9 +119,25 @@ class WebsiteSlides(WebsiteProfile):
return values
- def _get_slide_quiz_info(self, slide, quiz_done=False):
+ def _get_slide_quiz_partner_info(self, slide, quiz_done=False):
return slide._compute_quiz_info(request.env.user.partner_id, quiz_done=quiz_done)[slide.id]
+ def _get_slide_quiz_data(self, slide):
+ slide_completed = slide.user_membership_id.sudo().completed
+ values = {
+ 'slide_questions': [{
+ 'id': question.id,
+ 'question': question.question,
+ 'answers': [{
+ 'id': answer.id,
+ 'text_value': answer.text_value,
+ 'is_correct': answer.is_correct if slide_completed else None
+ } for answer in question.sudo().answer_ids],
+ } for question in slide.question_ids]
+ }
+ values.update(self._get_slide_quiz_partner_info(slide))
+ return values
+
# CHANNEL UTILITIES
# --------------------------------------------------
@@ -489,7 +505,7 @@ class WebsiteSlides(WebsiteProfile):
values = self._get_slide_detail(slide)
# quiz-specific: update with karma and quiz information
if slide.question_ids:
- values.update(self._get_slide_quiz_info(slide))
+ values.update(self._get_slide_quiz_data(slide))
# sidebar: update with user channel progress
values['channel_progress'] = self._get_channel_progress(slide.channel_id, include_quiz=True)
@@ -623,22 +639,7 @@ class WebsiteSlides(WebsiteProfile):
if fetch_res.get('error'):
return fetch_res
slide = fetch_res['slide']
- quiz_info = self._get_slide_quiz_info(slide)
- slide_completed = slide.user_membership_id.sudo().completed
- return {
- 'questions': [{
- 'id': question.id,
- 'question': question.question,
- 'answers': [{
- 'id': answer.id,
- 'text_value': answer.text_value,
- 'is_correct': answer.is_correct if slide_completed else None
- } for answer in question.answer_ids],
- } for question in slide.question_ids],
- 'quizAttemptsCount': quiz_info['quiz_attempts_count'],
- 'quizKarmaGain': quiz_info['quiz_karma_gain'],
- 'quizKarmaWon': quiz_info['quiz_karma_won'],
- }
+ return self._get_slide_quiz_data(slide)
@http.route('/slides/slide/quiz/submit', type="json", auth="public", website=True)
def slide_quiz_submit(self, slide_id, answer_ids):
@@ -662,7 +663,7 @@ class WebsiteSlides(WebsiteProfile):
user_good_answers = user_answers - user_bad_answers
self._set_viewed_slide(slide, quiz_attempts_inc=True)
- quiz_info = self._get_slide_quiz_info(slide, quiz_done=True)
+ quiz_info = self._get_slide_quiz_partner_info(slide, quiz_done=True)
rank_progress = {}
if not user_bad_answers:
diff --git a/addons/website_slides/security/ir.model.access.csv b/addons/website_slides/security/ir.model.access.csv
index ce9bcf2593d..82fbf8312ea 100644
--- a/addons/website_slides/security/ir.model.access.csv
+++ b/addons/website_slides/security/ir.model.access.csv
@@ -5,7 +5,7 @@ access_slide_slide_partner_all,slide.slide.partner.all,model_slide_slide_partner
access_slide_slide_partner_system,slide.slide.partner.system,model_slide_slide_partner,website.group_website_publisher,1,1,1,1
access_slide_question_all,slide.question.all,model_slide_question,,1,0,0,0
access_slide_question_publisher,slide.question.publisher,model_slide_question,website.group_website_publisher,1,1,1,1
-access_slide_answer_all,slide.answer.all,model_slide_answer,,1,0,0,0
+access_slide_answer_all,slide.answer.all,model_slide_answer,,0,0,0,0
access_slide_answer_publisher,slide.answer.publisher,model_slide_answer,website.group_website_publisher,1,1,1,1
access_slide_tag_all,slide.tag.all,model_slide_tag,,1,0,0,0
access_slide_tag_publisher,slide.tag.publisher,model_slide_tag,website.group_website_publisher,1,1,1,1
diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js
index c166e24a782..8b02ee4beae 100644
--- a/addons/website_slides/static/src/js/slides_course_quiz.js
+++ b/addons/website_slides/static/src/js/slides_course_quiz.js
@@ -106,7 +106,12 @@ odoo.define('website_slides.quiz', function (require) {
'slide_id': self.slide.id,
}
}).then(function (quiz_data) {
- self.quiz = quiz_data;
+ self.quiz = {
+ questions: quiz_data.slide_questions,
+ quizAttemptsCount: quiz_data.quiz_attempts_count,
+ quizKarmaGain: quiz_data.quiz_karma_gain,
+ quizKarmaWon: quiz_data.quiz_karma_won
+ };
});
},
diff --git a/addons/website_slides/static/src/xml/slide_quiz.xml b/addons/website_slides/static/src/xml/slide_quiz.xml
index ef884ab2007..0f1f3626c7e 100644
--- a/addons/website_slides/static/src/xml/slide_quiz.xml
+++ b/addons/website_slides/static/src/xml/slide_quiz.xml
@@ -60,7 +60,7 @@
<b t-else="" class="my-0 h5">Done !</b>
<span class="my-0 h5" style="line-height: 1">
<span role="button" title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2">
- + <t t-esc="widget.quiz.quizKarmaGain"/> XP
+ + <t t-if="! widget.slide.completed" t-esc="widget.quiz.quizKarmaGain"/><t t-else="" t-esc="widget.quiz.quizKarmaWon"/> XP
</span>
</span>
</div>
diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml
index 5978d63e8b5..6676cc4932f 100644
--- a/addons/website_slides/views/website_slides_templates_lesson.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson.xml
@@ -426,27 +426,27 @@
t-att-data-quiz-karma-won="quiz_karma_won"
t-att-data-has-next="1 if next_slide else 0"
t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None">
- <div t-foreach="slide.question_ids" t-as="question"
- t-att-class="'o_wslides_js_lesson_quiz_question mt-3 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question.id" t-att-data-title="question.question">
+ <div t-foreach="slide_questions" t-as="question"
+ t-att-class="'o_wslides_js_lesson_quiz_question mt-3 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
<div class="h4">
- <small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question.question"/>
+ <small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question['question']"/>
</div>
<div class="list-group">
- <t t-foreach="question.answer_ids" t-as="answer">
- <a t-att-data-answer-id="answer.id" href="#"
- t-att-data-text="answer.text_value"
- t-att-class="'o_wslides_quiz_answer list-group-item list-group-item-action d-flex align-items-center %s' % ('list-group-item-success' if slide_completed and answer.is_correct else '')">
+ <t t-foreach="question['answers']" t-as="answer">
+ <a t-att-data-answer-id="answer['id']" href="#"
+ t-att-data-text="answer['text_value']"
+ t-att-class="'o_wslides_quiz_answer list-group-item list-group-item-action d-flex align-items-center %s' % ('list-group-item-success' if slide_completed and answer['is_correct'] else '')">
<label class="my-0 d-flex align-items-center justify-content-center mr-2">
<input type="radio"
- t-att-name="question.id"
- t-att-value="answer.id"
+ t-att-name="question['id']"
+ t-att-value="answer['id']"
class="d-none"
t-att-disabled="True if not slide.channel_id.is_member or slide_completed else ''"/>
- <i t-att-class="'fa fa-circle text-400 %s' % ('d-none' if slide_completed and answer.is_correct else '')"/>
+ <i t-att-class="'fa fa-circle text-400 %s' % ('d-none' if slide_completed and answer['is_correct'] else '')"/>
<i class="fa fa-times-circle text-danger d-none"></i>
- <i t-att-class="'fa fa-check-circle text-success %s' % ('d-none' if not (slide_completed and answer.is_correct) else '')"></i>
+ <i t-att-class="'fa fa-check-circle text-success %s' % ('d-none' if not (slide_completed and answer['is_correct']) else '')"></i>
</label>
- <span t-esc="answer.text_value"/>
+ <span t-esc="answer['text_value']"/>
</a>
</t>
</div>