diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 932791ef0d..0205589755 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -4925,7 +4925,7 @@ class AccountMoveLine(models.Model): # ==== Create entries for cash basis taxes ==== - is_cash_basis_needed = account.user_type_id.type in ('receivable', 'payable') + is_cash_basis_needed = account.company_id.tax_exigibility and account.user_type_id.type in ('receivable', 'payable') if is_cash_basis_needed and not self._context.get('move_reverse_cancel'): tax_cash_basis_moves = partials._create_tax_cash_basis_moves() results['tax_cash_basis_moves'] = tax_cash_basis_moves diff --git a/addons/account/tests/test_account_move_entry.py b/addons/account/tests/test_account_move_entry.py index ffdc73ecd8..1025650b96 100644 --- a/addons/account/tests/test_account_move_entry.py +++ b/addons/account/tests/test_account_move_entry.py @@ -511,6 +511,7 @@ class TestAccountMove(AccountTestInvoicingCommon): 'company_id': self.company_data['company'].id, }) self.env.company.account_cash_basis_base_account_id = tax_base_amount_account + self.env.company.tax_exigibility = True tax_tags = defaultdict(dict) for line_type, repartition_type in [(l, r) for l in ('invoice', 'refund') for r in ('base', 'tax')]: tax_tags[line_type][repartition_type] = self.env['account.account.tag'].create({ diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py index 127d6f1512..c8be262a91 100644 --- a/addons/account/tests/test_account_move_in_invoice.py +++ b/addons/account/tests/test_account_move_in_invoice.py @@ -1865,6 +1865,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): 'company_id': self.company_data['company'].id, }) self.env.company.account_cash_basis_base_account_id = tax_base_amount_account + self.env.company.tax_exigibility = True tax_tags = defaultdict(dict) for line_type, repartition_type in [(l, r) for l in ('invoice', 'refund') for r in ('base', 'tax')]: tax_tags[line_type][repartition_type] = self.env['account.account.tag'].create({ diff --git a/addons/account/tests/test_account_move_in_refund.py b/addons/account/tests/test_account_move_in_refund.py index e786acdf57..4c1aa600a2 100644 --- a/addons/account/tests/test_account_move_in_refund.py +++ b/addons/account/tests/test_account_move_in_refund.py @@ -1024,6 +1024,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon): 'company_id': self.company_data['company'].id, }) self.env.company.account_cash_basis_base_account_id = tax_base_amount_account + self.env.company.tax_exigibility = True tax_tags = defaultdict(dict) for line_type, repartition_type in [(l, r) for l in ('invoice', 'refund') for r in ('base', 'tax')]: tax_tags[line_type][repartition_type] = self.env['account.account.tag'].create({ diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index 093f85cfc6..0ceac6ab0f 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -3284,6 +3284,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): 'company_id': self.company_data['company'].id, }) self.env.company.account_cash_basis_base_account_id = tax_base_amount_account + self.env.company.tax_exigibility = True tax_tags = defaultdict(dict) for line_type, repartition_type in [(l, r) for l in ('invoice', 'refund') for r in ('base', 'tax')]: tax_tags[line_type][repartition_type] = self.env['account.account.tag'].create({ diff --git a/addons/account/tests/test_account_move_out_refund.py b/addons/account/tests/test_account_move_out_refund.py index c68efd014c..8e00ca4d6d 100644 --- a/addons/account/tests/test_account_move_out_refund.py +++ b/addons/account/tests/test_account_move_out_refund.py @@ -1006,6 +1006,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon): 'company_id': self.company_data['company'].id, }) self.env.company.account_cash_basis_base_account_id = tax_base_amount_account + self.env.company.tax_exigibility = True tax_tags = defaultdict(dict) for line_type, repartition_type in [(l, r) for l in ('invoice', 'refund') for r in ('base', 'tax')]: tax_tags[line_type][repartition_type] = self.env['account.account.tag'].create({ diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 1e55c5fd85..de5405f3e6 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -1295,6 +1295,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): - Check there is no rounding issue when making the percentage. - Check there is no lost cents when the journal entry is fully reconciled. ''' + self.env.company.tax_exigibility = True cash_basis_move = self.env['account.move'].create({ 'move_type': 'entry', 'date': '2016-01-01', @@ -1502,6 +1503,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): def test_reconcile_cash_basis_workflow_multi_currency(self): ''' Same as before with a foreign currency. ''' + self.env.company.tax_exigibility = True currency_id = self.currency_data['currency'].id taxes = self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount @@ -1730,6 +1732,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): ''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer account is not a reconcile one. ''' + self.env.company.tax_exigibility = True currency_id = self.currency_data['currency'].id # Rate 1/3 in 2016. @@ -1863,6 +1866,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): ''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer account is not a reconcile one. ''' + self.env.company.tax_exigibility = True currency_id = self.setup_multi_currency_data(default_values={ 'name': 'bitcoin', 'symbol': 'bc', @@ -1950,6 +1954,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): ''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer account is not a reconcile one. ''' + self.env.company.tax_exigibility = True currency_id = self.setup_multi_currency_data(default_values={ 'name': 'bitcoin', 'symbol': 'bc', @@ -2036,6 +2041,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): ''' Test the generation of the exchange difference for a tax cash basis journal entry when the tax account is a reconcile one. ''' + self.env.company.tax_exigibility = True currency_id = self.currency_data['currency'].id cash_basis_transition_account = self.env['account.account'].create({ 'code': '209.01.01', @@ -2149,6 +2155,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): def test_reconcile_cash_basis_revert(self): ''' Ensure the cash basis journal entry can be reverted. ''' + self.env.company.tax_exigibility = True self.cash_basis_transfer_account.reconcile = True self.cash_basis_tax_a_third_amount.cash_basis_transition_account_id = self.tax_account_1 @@ -2232,6 +2239,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): self.assertFullReconcile(reversed_taxes_full_reconcile, reversed_taxes_lines) def test_reconcile_cash_basis_tax_grid_refund(self): + self.env.company.tax_exigibility = True invoice_move = self.env['account.move'].create({ 'move_type': 'entry', 'date': '2016-01-01', @@ -2343,6 +2351,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): def test_reconcile_cash_basis_tax_grid_multi_taxes(self): ''' Test the tax grid when reconciling an invoice with multiple taxes/tax repartition. ''' + self.env.company.tax_exigibility = True base_taxes = self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount base_tags = self.tax_tags[0] + self.tax_tags[4] @@ -2434,6 +2443,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): # Make the tax account reconcilable self.tax_account_1.reconcile = True + self.env.company.tax_exigibility = True # Create a tax using the same accounts as the CABA one non_caba_tax = self.env['account.tax'].create({ @@ -2516,6 +2526,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): """ # Make the tax account reconcilable self.tax_account_1.reconcile = True + self.env.company.tax_exigibility = True # Create an invoice with a CABA tax using 'Include in analytic cost' move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice', account_predictive_bills_disable_prediction=True)) @@ -2563,6 +2574,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): def test_caba_double_tax_negative_line(self): """ Tests making a cash basis invoice with 2 lines using the same tax: a positive and a negative one. """ + self.env.company.tax_exigibility = True invoice = self.init_invoice('in_invoice', amounts=[300, -60], post=True, taxes=self.cash_basis_tax_a_third_amount) pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({ @@ -2595,6 +2607,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): # Make the tax account reconcilable self.tax_account_1.reconcile = True + self.env.company.tax_exigibility = True # Create an invoice with a CABA tax using the same tax account and pay half of it caba_inv = self.init_invoice('in_invoice', amounts=[900], post=True, taxes=self.cash_basis_tax_a_third_amount) diff --git a/addons/base_import/static/src/js/import_action.js b/addons/base_import/static/src/js/import_action.js index 089ec6526c..b8aaaed23b 100644 --- a/addons/base_import/static/src/js/import_action.js +++ b/addons/base_import/static/src/js/import_action.js @@ -174,9 +174,8 @@ var DataImport = AbstractAction.extend({ this.setup_date_format_picker(); this.setup_sheets_picker(); - return Promise.all([ - this._super(), - self.create_model().then(function (id) { + return this._super().then(function () { + return self.create_model().then(function (id) { self.id = id; self.$('input[name=import_id]').val(id); @@ -185,8 +184,8 @@ var DataImport = AbstractAction.extend({ cp_content: {$buttons: self.$buttons}, }; self.updateControlPanel(status); - }), - ]); + }); + }); }, create_model: function() { return this._rpc({ diff --git a/addons/google_calendar/models/google_sync.py b/addons/google_calendar/models/google_sync.py index cc01d872f2..0fc8b8a9b8 100644 --- a/addons/google_calendar/models/google_sync.py +++ b/addons/google_calendar/models/google_sync.py @@ -199,8 +199,7 @@ class GoogleSync(models.AbstractModel): return with google_calendar_token(self.env.user.sudo()) as token: if token: - send_updates = self._context.get('send_updates', True) - google_id = google_service.with_context(send_updates=send_updates).insert(values, token=token, timeout=timeout) + google_id = google_service.insert(values, token=token, timeout=timeout) self.write({ 'google_id': google_id, 'need_sync': False, diff --git a/addons/google_calendar/models/res_users.py b/addons/google_calendar/models/res_users.py index 9443d93232..39ceadf03a 100644 --- a/addons/google_calendar/models/res_users.py +++ b/addons/google_calendar/models/res_users.py @@ -96,14 +96,13 @@ class User(models.Model): synced_events = self.env['calendar.event']._sync_google2flectra(events - recurrences, default_reminders=default_reminders) # Flectra -> Google - send_updates = not full_sync recurrences = self.env['calendar.recurrence']._get_records_to_sync(full_sync=full_sync) recurrences -= synced_recurrences - recurrences.with_context(send_updates=send_updates)._sync_flectra2google(calendar_service) + recurrences._sync_flectra2google(calendar_service) synced_events |= recurrences.calendar_event_ids - recurrences._get_outliers() synced_events |= synced_recurrences.calendar_event_ids - synced_recurrences._get_outliers() events = self.env['calendar.event']._get_records_to_sync(full_sync=full_sync) - (events - synced_events).with_context(send_updates=send_updates)._sync_flectra2google(calendar_service) + (events - synced_events)._sync_flectra2google(calendar_service) return bool(events | synced_events) or bool(recurrences | synced_recurrences) diff --git a/addons/google_calendar/utils/google_calendar.py b/addons/google_calendar/utils/google_calendar.py index 5ee175b1b0..ce0ba241ea 100644 --- a/addons/google_calendar/utils/google_calendar.py +++ b/addons/google_calendar/utils/google_calendar.py @@ -58,8 +58,7 @@ class GoogleCalendarService(): @requires_auth_token def insert(self, values, token=None, timeout=TIMEOUT): - send_updates = self._context.get('send_updates', True) - url = "/calendar/v3/calendars/primary/events?sendUpdates=%s" % ("all" if send_updates else "none") + url = "/calendar/v3/calendars/primary/events?sendUpdates=all" headers = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % token} if not values.get('id'): values['id'] = uuid4().hex diff --git a/addons/hr_expense/wizard/account_payment_register.py b/addons/hr_expense/wizard/account_payment_register.py index 2e8d2cf380..25078bc52f 100644 --- a/addons/hr_expense/wizard/account_payment_register.py +++ b/addons/hr_expense/wizard/account_payment_register.py @@ -16,7 +16,7 @@ class AccountPaymentRegister(models.TransientModel): res = super()._get_line_batch_key(line) expense_sheet = self.env['hr.expense.sheet'].search([('payment_mode', '=', 'own_account'), ('account_move_id', 'in', line.move_id.ids)]) if expense_sheet and not line.move_id.partner_bank_id: - res['partner_bank_id'] = expense_sheet.employee_id.bank_account_id.id or line.partner_id.bank_ids and line.partner_id.bank_ids.ids[0] + res['partner_bank_id'] = expense_sheet.employee_id.sudo().bank_account_id.id or line.partner_id.bank_ids and line.partner_id.bank_ids.ids[0] return res def _init_payments(self, to_process, edit_mode=False): diff --git a/addons/l10n_es/data/account_data.xml b/addons/l10n_es/data/account_data.xml index 1d807b27e1..8d07946c7c 100644 --- a/addons/l10n_es/data/account_data.xml +++ b/addons/l10n_es/data/account_data.xml @@ -66,5 +66,8 @@ IVA no deducible + + IVA 5% + diff --git a/addons/l10n_es/data/account_tax_data.xml b/addons/l10n_es/data/account_tax_data.xml index 8bb342361d..b5e6bc875a 100644 --- a/addons/l10n_es/data/account_tax_data.xml +++ b/addons/l10n_es/data/account_tax_data.xml @@ -3,6 +3,7 @@ © 2011 Ignacio Ibeas - Acysos © 2014 Pablo Cayuela - Aserti Global Solutions © 2014 Ángel Moya - Domatix + © 2017 Ángel Moya - PESOL © 2015 Carlos Liébana - Factor Libre © 2015 Albert Cabedo - GAFIC consultores © 2015 Vicent Cubells @@ -1458,6 +1459,41 @@ }), ]"/> + + + purchase + 5% IVA soportado + + + percent + + + + purchase @@ -1963,6 +1999,41 @@ }), ]"/> + + + sale + IVA 5% + + + percent + + + + sale diff --git a/addons/microsoft_calendar/models/microsoft_sync.py b/addons/microsoft_calendar/models/microsoft_sync.py index 6b9d1d012d..d573aa58d9 100644 --- a/addons/microsoft_calendar/models/microsoft_sync.py +++ b/addons/microsoft_calendar/models/microsoft_sync.py @@ -78,26 +78,22 @@ class MicrosoftSync(models.AbstractModel): def write(self, vals): if 'ms_universal_event_id' in vals: self._from_uids.clear_cache(self) - synced_fields = self._get_microsoft_synced_fields() - if 'need_sync_m' not in vals and vals.keys() & synced_fields and self.ms_organizer_event_id: - fields_to_sync = [x for x in vals.keys() if x in synced_fields] - if fields_to_sync: - vals['need_sync_m'] = True - else: - fields_to_sync = [x for x in vals.keys() if x in synced_fields] + + fields_to_sync = [x for x in vals.keys() if x in self._get_microsoft_synced_fields()] + if fields_to_sync and 'need_sync_m' not in vals: + vals['need_sync_m'] = True result = super().write(vals) - need_delete = 'active' in vals.keys() and not vals.get('active') - for record in self.filtered('need_sync_m'): - if record.ms_universal_event_id: - if need_delete: - # We need to delete the event. Cancel is not sufficant. Errors may occurs - record._microsoft_delete(record._get_organizer(), record.ms_organizer_event_id, timeout=3) - elif fields_to_sync: - values = record._microsoft_values(fields_to_sync) - if not values: - continue - record._microsoft_patch(record._get_organizer(), record.ms_organizer_event_id, values, timeout=3) + + for record in self.filtered(lambda e: e.need_sync_m and e.ms_organizer_event_id): + if not vals.get('active', True): + # We need to delete the event. Cancel is not sufficant. Errors may occurs + record._microsoft_delete(record._get_organizer(), record.ms_organizer_event_id, timeout=3) + elif fields_to_sync: + values = record._microsoft_values(fields_to_sync) + if not values: + continue + record._microsoft_patch(record._get_organizer(), record.ms_organizer_event_id, values, timeout=3) return result diff --git a/addons/microsoft_calendar/tests/common.py b/addons/microsoft_calendar/tests/common.py index fce5ecee66..38a084cc5c 100644 --- a/addons/microsoft_calendar/tests/common.py +++ b/addons/microsoft_calendar/tests/common.py @@ -422,6 +422,18 @@ class TestCommon(HttpCase): ) ) + # a group of events + self.several_events = self.env["calendar.event"].search([("name", "like", "event%")]) + if not self.several_events: + self.several_events = self.env["calendar.event"].with_user(self.organizer_user).create([ + dict( + self.simple_event_values, + name=f"event{i}", + microsoft_id=combine_ids(f"e{i}", f"u{i}"), + ) + for i in range(1, 4) + ]) + # a recurrent event with 7 occurrences self.recurrent_base_event = self.env["calendar.event"].search( [("name", "=", "recurrent_event")], diff --git a/addons/microsoft_calendar/tests/test_delete_events.py b/addons/microsoft_calendar/tests/test_delete_events.py index ea9b3469ab..7ddccc7074 100644 --- a/addons/microsoft_calendar/tests/test_delete_events.py +++ b/addons/microsoft_calendar/tests/test_delete_events.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from unittest.mock import patch, ANY +from unittest.mock import patch, ANY, call from flectra.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService from flectra.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent @@ -81,6 +81,24 @@ class TestDeleteEvents(TestCommon): timeout=ANY ) + @patch.object(MicrosoftCalendarService, 'delete') + def test_archive_several_events_at_once(self, mock_delete): + """ + Archive several events at once should not produce any exception. + """ + # act + self.several_events.action_archive() + self.call_post_commit_hooks() + self.several_events.invalidate_cache() + + # assert + self.assertFalse(all(e.active for e in self.several_events)) + + mock_delete.assert_has_calls([ + call(e.ms_organizer_event_id, token=ANY, timeout=ANY) + for e in self.several_events + ]) + @patch.object(MicrosoftCalendarService, 'get_events') def test_cancel_simple_event_from_outlook_organizer_calendar(self, mock_get_events): """ diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 5e196dbacb..8b706eb2e2 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -598,6 +598,16 @@ class PosOrder(models.Model): 'amount_paid': 0, } + def _prepare_mail_values(self, name, message, client, receipt): + return { + 'subject': _('Receipt %s', name), + 'body_html': message, + 'author_id': self.env.user.partner_id.id, + 'email_from': self.env.company.email or self.env.user.email_formatted, + 'email_to': client['email'], + 'attachment_ids': [(4, receipt.id)], + } + def refund(self): """Create a copy of order for refund order""" refund_orders = self.env['pos.order'] @@ -644,14 +654,8 @@ class PosOrder(models.Model): 'res_id': self.ids[0], 'mimetype': 'image/jpeg', }) - mail_values = { - 'subject': _('Receipt %s', name), - 'body_html': message, - 'author_id': self.env.user.partner_id.id, - 'email_from': self.env.company.email or self.env.user.email_formatted, - 'email_to': client['email'], - 'attachment_ids': [(4, receipt.id)], - } + + mail_values = self._prepare_mail_values(name, message, client, receipt) if self.mapped('account_move'): report = self.env.ref('point_of_sale.pos_invoice_report')._render_qweb_pdf(self.ids[0]) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index a4af4caa93..2aa3d19f4c 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -2117,7 +2117,7 @@ exports.Orderline = Backbone.Model.extend({ can_be_merged_with: function(orderline){ var price = parseFloat(round_di(this.price || 0, this.pos.dp['Product Price']).toFixed(this.pos.dp['Product Price'])); var order_line_price = orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity()); - order_line_price = orderline.compute_fixed_price(order_line_price); + order_line_price = round_di(orderline.compute_fixed_price(order_line_price), this.pos.currency.decimals); if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged return false; }else if(!this.get_unit() || !this.get_unit().is_pos_groupable){ diff --git a/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py index 552737b4d1..334a63bd03 100644 --- a/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py +++ b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py @@ -214,6 +214,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCommon): def test_reconcile_cash_basis_bill(self): ''' Test the generation of the CABA move after bill payment ''' + self.env.company.tax_exigibility = True cash_basis_base_account = self.env['account.account'].create({ 'code': 'cash_basis_base_account', 'name': 'cash_basis_base_account', diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 6d51f382c6..dbee2ec9b6 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1721,8 +1721,9 @@ class SaleOrderLine(models.Model): vals['product_uom'] = self.product_id.uom_id vals['product_uom_qty'] = self.product_uom_qty or 1.0 + lang = get_lang(self.env, self.order_id.partner_id.lang).code product = self.product_id.with_context( - lang=get_lang(self.env, self.order_id.partner_id.lang).code, + lang=lang, partner=self.order_id.partner_id, quantity=vals.get('product_uom_qty') or self.product_uom_qty, date=self.order_id.date_order, @@ -1730,7 +1731,7 @@ class SaleOrderLine(models.Model): uom=self.product_uom.id ) - vals.update(name=self.get_sale_order_line_multiline_description_sale(product)) + vals.update(name=self.with_context(lang=lang).get_sale_order_line_multiline_description_sale(product)) self._compute_tax_id() @@ -1945,12 +1946,12 @@ class SaleOrderLine(models.Model): # display the no_variant attributes, except those that are also # displayed by a custom (avoid duplicate description) for ptav in (no_variant_ptavs - custom_ptavs): - name += "\n" + ptav.with_context(lang=self.order_id.partner_id.lang).display_name + name += "\n" + ptav.display_name # Sort the values according to _order settings, because it doesn't work for virtual records in onchange custom_values = sorted(self.product_custom_attribute_value_ids, key=lambda r: (r.custom_product_template_attribute_value_id.id, r.id)) # display the is_custom values for pacv in custom_values: - name += "\n" + pacv.with_context(lang=self.order_id.partner_id.lang).display_name + name += "\n" + pacv.display_name return name diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index b1af334770..a2b4934fc6 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -185,7 +185,8 @@ class SaleOrderLine(models.Model): if self.product_id and self.order_id.sale_order_template_id: for line in self.order_id.sale_order_template_id.sale_order_template_line_ids: if line.product_id == self.product_id: - self.name = line.with_context(lang=self.order_id.partner_id.lang).name + self._get_sale_order_line_multiline_description_variants() + lang = self.order_id.partner_id.lang + self.name = line.with_context(lang=lang).name + self.with_context(lang=lang)._get_sale_order_line_multiline_description_variants() break return domain diff --git a/addons/sale_product_configurator/controllers/main.py b/addons/sale_product_configurator/controllers/main.py index 4a5c72876c..c7689dfbd4 100644 --- a/addons/sale_product_configurator/controllers/main.py +++ b/addons/sale_product_configurator/controllers/main.py @@ -8,7 +8,7 @@ from flectra.http import request class ProductConfiguratorController(http.Controller): @http.route(['/sale_product_configurator/configure'], type='json', auth="user", methods=['POST']) def configure(self, product_template_id, pricelist_id, **kw): - add_qty = int(kw.get('add_qty', 1)) + add_qty = float(kw.get('add_qty', 1)) product_template = request.env['product.template'].browse(int(product_template_id)) pricelist = self._get_pricelist(pricelist_id) @@ -39,7 +39,7 @@ class ProductConfiguratorController(http.Controller): return self._optional_product_items(product_id, pricelist, **kw) def _optional_product_items(self, product_id, pricelist, **kw): - add_qty = int(kw.get('add_qty', 1)) + add_qty = float(kw.get('add_qty', 1)) product = request.env['product.product'].browse(int(product_id)) parent_combination = product.product_template_attribute_value_ids @@ -64,7 +64,7 @@ class ProductConfiguratorController(http.Controller): if not has_optional_products: return False - add_qty = int(kw.get('add_qty', 1)) + add_qty = float(kw.get('add_qty', 1)) no_variant_attribute_values = combination.filtered( lambda product_template_attribute_value: product_template_attribute_value.attribute_id.create_variant == 'no_variant' diff --git a/addons/sale_product_configurator/static/src/js/product_configurator_controller.js b/addons/sale_product_configurator/static/src/js/product_configurator_controller.js index 5908f3e9cf..026fa72eab 100644 --- a/addons/sale_product_configurator/static/src/js/product_configurator_controller.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_controller.js @@ -115,12 +115,13 @@ var ProductConfiguratorFormController = FormController.extend({ var initialProduct = this.initialState.data.product_template_id; var changed = initialProduct && initialProduct.data.id !== productTemplateId; var data = this.renderer.state.data; + var quantity = initialProduct.context && initialProduct.context.default_quantity ? initialProduct.context.default_quantity : data.quantity; return this._rpc({ route: '/sale_product_configurator/configure', params: { product_template_id: productTemplateId, pricelist_id: this.renderer.pricelistId, - add_qty: data.quantity, + add_qty: quantity, product_template_attribute_value_ids: changed ? [] : this._getAttributeValueIds( data.product_template_attribute_value_ids ), diff --git a/addons/sale_product_configurator/static/src/js/product_configurator_modal.js b/addons/sale_product_configurator/static/src/js/product_configurator_modal.js index a8a34ec5ce..9d71ac3824 100644 --- a/addons/sale_product_configurator/static/src/js/product_configurator_modal.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_modal.js @@ -183,7 +183,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { var products = [this.rootProduct]; this.$modal.find('.js_product.in_cart:not(.main_product)').each(function () { var $item = $(this); - var quantity = parseInt($item.find('input[name="add_qty"]').val(), 10); + var quantity = parseFloat($item.find('input[name="add_qty"]').val().replace(',', '.') || 1); var parentUniqueId = this.dataset.parentUniqueId; var uniqueId = this.dataset.uniqueId; var productCustomVariantValues = self.getCustomVariantValues($(this)); @@ -476,7 +476,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { if (this.$modal.find('.js_price_total').length) { var price = 0; this.$modal.find('.js_product.in_cart').each(function () { - var quantity = parseInt($(this).find('input[name="add_qty"]').first().val(), 10); + var quantity = parseFloat($(this).find('input[name="add_qty"]').first().val().replace(',', '.') || 1); price += parseFloat($(this).find('.js_raw_price').html()) * quantity; }); diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 8a07e3f0fc..d4daf3e9b3 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -521,7 +521,7 @@ class SaleOrderLine(models.Model): 'route_ids': self.route_id, 'warehouse_id': self.order_id.warehouse_id or False, 'partner_id': self.order_id.partner_shipping_id.id, - 'product_description_variants': self._get_sale_order_line_multiline_description_variants(), + 'product_description_variants': self.with_context(lang=self.order_id.partner_id.lang)._get_sale_order_line_multiline_description_variants(), 'company_id': self.order_id.company_id, 'sequence': self.sequence, }) diff --git a/addons/web/static/src/js/control_panel/custom_filter_item.js b/addons/web/static/src/js/control_panel/custom_filter_item.js index 9cad30f57b..60c092fe9b 100644 --- a/addons/web/static/src/js/control_panel/custom_filter_item.js +++ b/addons/web/static/src/js/control_panel/custom_filter_item.js @@ -124,8 +124,13 @@ flectra.define('web.CustomFilterItem', function (require) { } break; case 'selection': - const [firstValue] = this.fields[condition.field].selection[0]; - condition.value = firstValue; + if (this.fields[condition.field].selection.length) { + const [firstValue] = this.fields[condition.field].selection[0]; + condition.value = firstValue; + } + else { + condition.value = ""; + } break; default: condition.value = ""; diff --git a/addons/web/static/tests/control_panel/custom_filter_item_tests.js b/addons/web/static/tests/control_panel/custom_filter_item_tests.js index 1d2ea36024..81165eb073 100644 --- a/addons/web/static/tests/control_panel/custom_filter_item_tests.js +++ b/addons/web/static/tests/control_panel/custom_filter_item_tests.js @@ -156,6 +156,38 @@ flectra.define('web.filter_menu_generator_tests', function (require) { cfi.destroy(); }); + QUnit.test('selection field: no value', async function (assert) { + assert.expect(2); + + this.fields.color.selection = []; + let expectedFilters; + class MockedSearchModel extends ActionModel { + dispatch(method, ...args) { + assert.strictEqual(method, 'createNewFilters'); + const preFilters = args[0]; + assert.deepEqual(preFilters, expectedFilters); + } + } + const searchModel = new MockedSearchModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { searchModel }, + }); + + // Default value + expectedFilters = [{ + description: 'Color is ""', + domain: '[["color","=",""]]', + type: 'filter', + }]; + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'color'); + await cpHelpers.applyFilter(cfi); + + cfi.destroy(); + }) QUnit.test('adding a simple filter works', async function (assert) { assert.expect(6); diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 4f57a2a04f..e68ef71ac8 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -70,7 +70,10 @@ class View(models.Model): # We need to consider inactive views when handling multi-website cow # feature (to copy inactive children views, to search for specific # views, ...) - for view in self.with_context(active_test=False): + # Website-specific views need to be updated first because they might + # be relocated to new ids by the cow if they are involved in the + # inheritance tree. + for view in self.with_context(active_test=False).sorted(key='website_id', reverse=True): # Make sure views which are written in a website context receive # a value for their 'key' field if not view.key and not vals.get('key'): diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index b2786a6472..80e39aec1d 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -736,7 +736,7 @@ class TestCowViewSaving(TestViewSavingCommon): when copying them on the new specific tree. Correct order is the same as the one when applying view arch: PRIORITY, ID - And not the default one from ir.ui.view (NAME, PRIORIRTY, ID). + And not the default one from ir.ui.view (NAME, PRIORITY, ID). """ self.inherit_view.copy({ 'name': 'alphabetically before "Extension"', @@ -746,6 +746,34 @@ class TestCowViewSaving(TestViewSavingCommon): # Next line should not crash, COW loop on inherit_children_ids should be sorted correctly self.base_view.with_context(website_id=1).write({'name': 'Product (W1)'}) + def test_write_order_vs_cow_inherit_children_order(self): + """ When both a specific inheriting view and a non-specific base view + are written simultaneously, the specific inheriting base view + must be updated even though its id will change during the COW of + the base view. + """ + View = self.env['ir.ui.view'] + self.inherit_view.with_context(website_id=1).write({'name': 'Specific Inherited View Changed First'}) + specific_view = View.search([('name', '=', 'Specific Inherited View Changed First')]) + views = View.browse([self.base_view.id, specific_view.id]) + views.with_context(website_id=1).write({'active': False}) + new_specific_view = View.search([('name', '=', 'Specific Inherited View Changed First')]) + self.assertTrue(specific_view.id != new_specific_view.id, "Should have a new id") + self.assertFalse(new_specific_view.active, "Should have been deactivated") + + def test_write_order_vs_cow_inherit_children_order_alt(self): + """ Same as the previous test, but requesting the update in the + opposite order. + """ + View = self.env['ir.ui.view'] + self.inherit_view.with_context(website_id=1).write({'name': 'Specific Inherited View Changed First'}) + specific_view = View.search([('name', '=', 'Specific Inherited View Changed First')]) + views = View.browse([specific_view.id, self.base_view.id]) + views.with_context(website_id=1).write({'active': False}) + new_specific_view = View.search([('name', '=', 'Specific Inherited View Changed First')]) + self.assertTrue(specific_view.id != new_specific_view.id, "Should have a new id") + self.assertFalse(new_specific_view.active, "Should have been deactivated") + def test_module_new_inherit_view_on_parent_already_forked(self): """ If a generic parent view is copied (COW) and that another module creates a child view for that generic parent, all the COW views diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index e9cd30a227..b03e4a0c2a 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -365,7 +365,7 @@ class SaleOrderLine(models.Model): name_short = fields.Char(compute="_compute_name_short") - linked_line_id = fields.Many2one('sale.order.line', string='Linked Order Line', domain="[('order_id', '=', order_id)]", ondelete='cascade', copy=False) + linked_line_id = fields.Many2one('sale.order.line', string='Linked Order Line', domain="[('order_id', '=', order_id)]", ondelete='cascade', copy=False, index=True) option_line_ids = fields.One2many('sale.order.line', 'linked_line_id', string='Options Linked') def get_sale_order_line_multiline_description_sale(self, product):