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):