diff options
Diffstat (limited to 'addons')
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> </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> </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): |
