summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
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_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/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/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_stock/static/src/js/tour.js68
-rw-r--r--addons/sale_timesheet/controllers/main.py2
-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/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/calendar_tests.js44
-rw-r--r--addons/web/static/tests/views/search_view_tests.js24
-rw-r--r--addons/web/views/report_templates.xml1
-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/models/sale_order.py2
-rw-r--r--addons/website_sale_delivery/static/src/js/website_sale_delivery.js2
-rw-r--r--addons/website_sale_delivery/views/website_sale_delivery_templates.xml10
-rw-r--r--addons/website_sale_link_tracker/controllers/backend.py7
52 files changed, 559 insertions, 173 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index aff0ef405e1..9ab3bf91e69 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -435,6 +435,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
@@ -442,19 +443,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
@@ -463,9 +468,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 \
@@ -476,22 +480,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 ''
@@ -611,8 +664,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
@@ -633,32 +684,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:
@@ -708,6 +739,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 983bd9014ea..a2f95a4624d 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -54,12 +54,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.one
@@ -731,22 +733,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 5e75af21f91..36e76ef7f04 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
@@ -321,6 +328,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 bb0663e1468..af559f423c1 100644
--- a/addons/board/static/tests/dashboard_tests.js
+++ b/addons/board/static/tests/dashboard_tests.js
@@ -806,6 +806,12 @@ QUnit.test('save two searches to dashboard', function (assert) {
testUtils.dom.click(actionManager.$('.o_apply_filter'));
// Add it to dashboard
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();
+ });
testUtils.dom.click(actionManager.$('.o_add_to_board'));
testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button'));
// Remove it
@@ -818,6 +824,12 @@ QUnit.test('save two searches to dashboard', function (assert) {
testUtils.dom.click(actionManager.$('.o_apply_filter'));
// Add it to dashboard
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();
+ });
testUtils.dom.click(actionManager.$('.o_add_to_board'));
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 4d76e598d43..876e48698d0 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -409,8 +409,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 accc102c2b7..3e4e0e37c17 100644
--- a/addons/delivery/models/stock_picking.py
+++ b/addons/delivery/models/stock_picking.py
@@ -64,7 +64,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()
@@ -74,7 +74,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_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py
index 2b9e27aba96..c80a4fc9968 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 3dd3c5bb871..9bac31afca3 100644
--- a/addons/hr_holidays/models/hr_leave_type.py
+++ b/addons/hr_holidays/models/hr_leave_type.py
@@ -138,10 +138,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]
@@ -149,9 +155,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
@@ -226,7 +238,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 3b1da9e3cbe..d3bfee4ca0b 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/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index 5f9b11198e3..dc23354fe7b 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -365,7 +365,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 934df9b7acc..6ac15cb7c9d 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 544c4aaa009..529b12e72a4 100644
--- a/addons/mrp/report/mrp_report_bom_structure.py
+++ b/addons/mrp/report/mrp_report_bom_structure.py
@@ -197,7 +197,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 8ccb48ec0de..97f49ddf2f2 100644
--- a/addons/payment/static/src/js/payment_form.js
+++ b/addons/payment/static/src/js/payment_form.js
@@ -36,6 +36,18 @@ odoo.define('payment.payment_form', function (require) {
$('[data-toggle="tooltip"]').tooltip();
},
+ 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();
+ },
+
payEvent: function (ev) {
ev.preventDefault();
var form = this.el;
@@ -90,10 +102,7 @@ odoo.define('payment.payment_form', function (require) {
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) {
@@ -128,14 +137,10 @@ odoo.define('payment.payment_form', function (require) {
}
}
// 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);
}).fail(function (error, event) {
// 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'),
@@ -200,6 +205,7 @@ odoo.define('payment.payment_form', function (require) {
}
}
else { // if the user is using an old payment then we just submit the form
+ this.disableButton(button);
form.submit();
return $.Deferred();
}
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index ea6b5b89758..8959b1dd9a5 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 55a6f65eb82..a7b5aeb755f 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -887,6 +887,9 @@ exports.PosModel = Backbone.Model.extend({
}}).done(function () {
invoiced.resolve();
done.resolve();
+ }).fail(function (error) {
+ invoiced.reject({code:401, message:'Backend Invoice', data:{order: order}});
+ done.reject();
});
} 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/models/res_config_settings.py b/addons/sale/models/res_config_settings.py
index 9af48d1a011..7cf532f2b8c 100644
--- a/addons/sale/models/res_config_settings.py
+++ b/addons/sale/models/res_config_settings.py
@@ -47,7 +47,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 d2d3b8b1e8d..e980aba5fcf 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -499,6 +499,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)
@@ -507,6 +510,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
@@ -528,14 +532,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 162a4ec5b2d..f61220f0d58 100644
--- a/addons/sale/static/src/js/sale_portal_sidebar.js
+++ b/addons/sale/static/src/js/sale_portal_sidebar.js
@@ -88,7 +88,7 @@ var 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":
@@ -101,7 +101,7 @@ var 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 41cd9b7ccc6..51c1d66a606 100644
--- a/addons/sale_stock/models/sale_order.py
+++ b/addons/sale_stock/models/sale_order.py
@@ -446,7 +446,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_stock/static/src/js/tour.js b/addons/sale_stock/static/src/js/tour.js
index d27080305d5..cc64f95fecd 100644
--- a/addons/sale_stock/static/src/js/tour.js
+++ b/addons/sale_stock/static/src/js/tour.js
@@ -1,43 +1,51 @@
-odoo.define("sale_stock.tour", function (require) {
+var deps = ['web_tour.tour'];
+
+if (_.contains(odoo._modules, 'sale_management')) {
+ deps.push('sale.product_configurator_tour');
+ deps.push('sale.product_configurator_pricelist_tour');
+}
+
+odoo.define("sale_stock.tour", deps, function (require) {
"use strict";
var tour = require('web_tour.tour');
- require('sale.tour');
- var quotation_product_selected_step_index = _.findIndex(tour.tours.sale_product_configurator_tour.steps, function (step) {
- return (step.id === "quotation_product_selected");
- });
+ if (_.has(odoo.__DEBUG__.services, 'sale.product_configurator_tour')) {
+ var quotation_product_selected_step_index = _.findIndex(tour.tours.sale_product_configurator_tour.steps, function (step) {
+ return (step.id === "quotation_product_selected");
+ });
- tour.tours.sale_product_configurator_tour.steps[quotation_product_selected_step_index].run = function (actions) {
- actions.auto();
- };
+ tour.tours.sale_product_configurator_tour.steps[quotation_product_selected_step_index].run = function (actions) {
+ actions.auto();
+ };
- tour.tours.sale_product_configurator_tour.steps.splice(quotation_product_selected_step_index+1, 0, {
- trigger: ".o_dialog_warning + .modal-footer .btn-primary",
- auto: true,
- run: function (actions) {
- actions.auto('.o_dialog_warning + .modal-footer .btn-primary');
- }
- }, {
- trigger: ".o_dialog_warning + .modal-footer .btn-primary",
- auto: true,
- run: function (actions) {
- actions.auto('.o_dialog_warning + .modal-footer .btn-primary');
- }
- }, {
- trigger: "body:not(:has(.o_dialog_warning))",
- auto: true,
- in_modal: false,
- run: function (actions) {
- if ($('.modal-footer .btn-primary').length){
- actions.auto('.modal-footer .btn-primary');
+ tour.tours.sale_product_configurator_tour.steps.splice(quotation_product_selected_step_index+1, 0, {
+ trigger: ".o_dialog_warning + .modal-footer .btn-primary",
+ auto: true,
+ run: function (actions) {
+ actions.auto('.o_dialog_warning + .modal-footer .btn-primary');
+ }
+ }, {
+ trigger: ".o_dialog_warning + .modal-footer .btn-primary",
+ auto: true,
+ run: function (actions) {
+ actions.auto('.o_dialog_warning + .modal-footer .btn-primary');
}
- },
- });
+ }, {
+ trigger: "body:not(:has(.o_dialog_warning))",
+ auto: true,
+ in_modal: false,
+ run: function (actions) {
+ if ($('.modal-footer .btn-primary').length){
+ actions.auto('.modal-footer .btn-primary');
+ }
+ },
+ });
+ }
// Check if sale_management is installed since sale_stock is adding an extra
// step to add to SO (not enough inventory)
- if ('sale.product_configurator_pricelist_tour' in odoo.__DEBUG__.services) {
+ if (_.has(odoo.__DEBUG__.services, 'sale.product_configurator_pricelist_tour')) {
var steps = tour.tours.sale_product_configurator_pricelist_tour.steps;
for (var k=0; k<steps.length; k++) {
if (steps[k].content === "add to SO") {
diff --git a/addons/sale_timesheet/controllers/main.py b/addons/sale_timesheet/controllers/main.py
index 48596805796..4330d9d3f73 100644
--- a/addons/sale_timesheet/controllers/main.py
+++ b/addons/sale_timesheet/controllers/main.py
@@ -200,7 +200,7 @@ class SaleTimesheetController(http.Controller):
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/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 7082f26e69d..2516b0a7230 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);
@@ -333,6 +335,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/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index b40da7ca0a5..ea1ec1591ea 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 782f31a1bd2..fa2931a6d19 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 665fe73bf7d..8d38e081375 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',
}),
@@ -363,15 +362,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 49b03ce6a3a..74e3eb9382f 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -1326,13 +1326,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>
@@ -1516,7 +1517,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/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 01c97f4d723..80f0a0a9eba 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -44,6 +44,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"},
@@ -2394,6 +2395,49 @@ QUnit.module('Views', {
calendar.destroy();
});
+ QUnit.test('drag and drop on month mode with date_start and date_delay', function (assert) {
+ assert.expect(1);
+
+ var calendar = createView({
+ 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");
+ var $input = $('.modal-body input:first');
+ $input.val("An event").trigger('input');
+ $('.modal button.btn:contains(Create)').trigger('click');
+
+ // Move event to another day (on 27 november)
+ testUtils.dragAndDrop(
+ calendar.$('.fc-event').first(),
+ calendar.$('.fc-day-top').first()
+ );
+
+ calendar.destroy();
+ });
+
QUnit.test('form_view_id attribute works (for creating events)', function (assert) {
assert.expect(1);
diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js
index cbc9fe2d0ef..90bdeb0040d 100644
--- a/addons/web/static/tests/views/search_view_tests.js
+++ b/addons/web/static/tests/views/search_view_tests.js
@@ -1272,7 +1272,7 @@ QUnit.module('Search View', {
});
QUnit.test('Customizing filter does not close the filter dropdown', function (assert) {
- assert.expect(3);
+ assert.expect(5);
var self = this;
_.each(this.data.partner.records.slice(), function (rec) {
@@ -1296,6 +1296,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);
+ },
+ },
});
testUtils.fields.many2one.clickOpenDropdown('bar');
@@ -1318,9 +1325,22 @@ QUnit.module('Search View', {
$input.click();
$input.click();
});
-
assert.isVisible($filterDropdown);
+ // Favorites Menu
+ var $modal = $('.modal');
+ testUtils.dom.click($modal.find('.o_favorites_menu_button'));
+ 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();
+ });
+ testUtils.fields.editInput($modal.find('div.o_favorite_name input'), 'Fire on the bayou');
+ testUtils.dom.click($modal.find('.o_add_favorite ~ div label:contains(Use by default)'));
+ 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 6b5a6a95b82..f3ad4ef34ff 100644
--- a/addons/web/views/report_templates.xml
+++ b/addons/web/views/report_templates.xml
@@ -5,6 +5,7 @@
<template id="report_assets_common">
<t t-call="web._assets_helpers">
<!-- FIXME web_editor ? -->
+ <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>
diff --git a/addons/web_editor/static/src/scss/bootstrap_overridden.scss b/addons/web_editor/static/src/scss/bootstrap_overridden.scss
index 849a891392b..5c64d275d56 100644
--- a/addons/web_editor/static/src/scss/bootstrap_overridden.scss
+++ b/addons/web_editor/static/src/scss/bootstrap_overridden.scss
@@ -83,5 +83,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 1c4df6c27db..ef958ef1d1f 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 1e102d63682..743859fef0f 100644
--- a/addons/website/static/src/js/backend/dashboard.js
+++ b/addons/website/static/src/js/backend/dashboard.js
@@ -33,8 +33,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 = [];
@@ -229,13 +229,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/models/sale_order.py b/addons/website_sale/models/sale_order.py
index 562d6803b9c..dbf8c03766a 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -356,7 +356,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/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
index 67a45b1aa28..7acaf9f1260 100644
--- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
+++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
@@ -43,7 +43,7 @@ sAnimations.registry.websiteSaleDelivery = sAnimations.Class.extend({
var $amountUntaxed = $('#order_total_untaxed span.oe_currency_value');
var $amountTax = $('#order_total_taxes span.oe_currency_value');
var $amountTotal = $('#order_total span.oe_currency_value');
- var $carrierBadge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .badge.d-none');
+ var $carrierBadge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .badge:not(.o_delivery_compute)');
var $computeBadge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .o_delivery_compute');
var $discount = $('#order_discounted');
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 5394f344e5a..59f3c72f42c 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-att-for="'delivery_%i' % delivery.id"/>
<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} d-none" 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}'/>
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):