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