diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 723e5fa67d..cf2ff01a1d 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -3626,7 +3626,6 @@ class AccountMoveLine(models.Model): if tax.price_include: amount_currency += tax_res['amount'] - tax_included_division_is_zero = any(tax.price_include and tax.amount == 100 and tax.amount_type == 'division' for tax in taxes) and amount_currency == 0 discount_factor = 1 - (discount / 100.0) if amount_currency and discount_factor: # discount != 100% @@ -3641,9 +3640,8 @@ class AccountMoveLine(models.Model): 'discount': 0.0, 'price_unit': amount_currency / (quantity or 1.0), } - elif not discount_factor or tax_included_division_is_zero: - # balance of line is 0, but discount == 100% or taxes (price included) == 100%, - # so we display the normal unit_price + elif not discount_factor: + # balance of line is 0, but discount == 100% so we display the normal unit_price vals = {} else: # balance is 0, so unit price is 0 as well diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index a71cad3947..093f85cfc6 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -3391,24 +3391,3 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): 'credit': value['debit'], }) self.assertRecordValues(reversed_caba_move.line_ids, expected_values) - - def test_changeprice_unit_to_zero(self): - move = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'invoice_date': fields.Date.from_string('2017-01-01'), - 'invoice_line_ids': [ - (0, None, { - 'product_id': self.product_a.id, - 'product_uom_id': self.product_a.uom_id.id, - 'quantity': 1.0, - 'price_unit': 1000.0, - 'tax_ids': [(6, 0, [])], - })] - }) - - move.write({'invoice_line_ids': [(1, move.invoice_line_ids.ids[0], { - 'price_unit': 0.0, - })]}) - - self.assertTrue(all(line.price_unit == 0 for line in move.line_ids)) diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py index e2f872dfe1..eeac01b81b 100644 --- a/addons/account/tests/test_invoice_taxes.py +++ b/addons/account/tests/test_invoice_taxes.py @@ -711,41 +711,3 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon): self.assertRecordValues(invoice.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable'), [{ 'balance': 686.54 }]) - - def test_invoice_with_100_percent_division_tax_incl(self): - """ - Check if the price unit remain the same after affecting a tax - 100%, price_include and amount_type 'division' - """ - invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'currency_id': self.currency_data['currency'].id, - 'invoice_date': '2018-01-01', - 'date': '2018-01-01', - 'invoice_line_ids': [(0, 0, { - 'name': 'xxxx', - 'quantity': 1, - 'price_unit': 500, - })] - }) - - division_tax_4_incl = self.env['account.tax'].create({ - 'name': '100% incl', - 'amount_type': 'division', - 'amount': 100, - 'price_include': True, - 'include_base_amount': True, - 'sequence': 50, - }) - - self.assertRecordValues(invoice.invoice_line_ids, [ - {'price_unit': 500} - ]) - with Form(invoice) as inv_form: - with inv_form.invoice_line_ids.edit(0) as line_form: - line_form.tax_ids.add(division_tax_4_incl) - - self.assertRecordValues(invoice.invoice_line_ids, [ - {'price_unit': 500} - ]) diff --git a/addons/mail/static/src/components/follow_button/follow_button.js b/addons/mail/static/src/components/follow_button/follow_button.js index 3a7f035c00..e51d344d6a 100644 --- a/addons/mail/static/src/components/follow_button/follow_button.js +++ b/addons/mail/static/src/components/follow_button/follow_button.js @@ -55,8 +55,9 @@ class FollowButton extends Component { * @private * @param {MouseEvent} ev */ - _onClickUnfollow(ev) { - this.thread.unfollow(); + async _onClickUnfollow(ev) { + await this.thread.unfollow(); + this.trigger('reload', { fieldNames: ['message_follower_ids'], keepChanges: true }); } /** diff --git a/addons/mail/static/src/components/follower/follower.js b/addons/mail/static/src/components/follower/follower.js index 5e9a8b4f78..2fbec37848 100644 --- a/addons/mail/static/src/components/follower/follower.js +++ b/addons/mail/static/src/components/follower/follower.js @@ -7,6 +7,7 @@ const components = { const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { markEventHandled } = require('mail/static/src/utils/utils.js'); const { Component } = owl; class Follower extends Component { @@ -61,8 +62,11 @@ class Follower extends Component { * @private * @param {MouseEvent} ev */ - _onClickRemove(ev) { - this.follower.remove(); + async _onClickRemove(ev) { + markEventHandled(ev, 'Follower.clickRemove'); + await this.follower.remove(); + this.trigger('reload', { fieldNames:['message_follower_ids'], keepChanges: true }); + this.trigger('o-hide-follower-list-menu'); } } diff --git a/addons/mail/static/src/components/follower_list_menu/follower_list_menu.js b/addons/mail/static/src/components/follower_list_menu/follower_list_menu.js index 3b9c89b583..76247d2a68 100644 --- a/addons/mail/static/src/components/follower_list_menu/follower_list_menu.js +++ b/addons/mail/static/src/components/follower_list_menu/follower_list_menu.js @@ -6,6 +6,7 @@ const components = { }; const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { isEventHandled } = require('mail/static/src/utils/utils.js'); const { Component } = owl; const { useRef, useState } = owl.hooks; @@ -133,6 +134,9 @@ class FollowerListMenu extends Component { * @param {MouseEvent} ev */ _onClickFollower(ev) { + if (isEventHandled(ev, 'Follower.clickRemove')) { + return; + } this._hide(); } } diff --git a/addons/mail/static/src/components/follower_list_menu/follower_list_menu.xml b/addons/mail/static/src/components/follower_list_menu/follower_list_menu.xml index ad712cb4cf..cf6cfbdfe8 100644 --- a/addons/mail/static/src/components/follower_list_menu/follower_list_menu.xml +++ b/addons/mail/static/src/components/follower_list_menu/follower_list_menu.xml @@ -26,6 +26,7 @@ class="o_FollowerMenu_follower dropdown-item" followerLocalId="follower.localId" t-on-click="_onClickFollower" + t-on-o-hide-follower-list-menu="_hide" /> diff --git a/addons/mail/static/src/models/thread_cache/thread_cache.js b/addons/mail/static/src/models/thread_cache/thread_cache.js index 5cd2eade00..fbfe9a617a 100644 --- a/addons/mail/static/src/models/thread_cache/thread_cache.js +++ b/addons/mail/static/src/models/thread_cache/thread_cache.js @@ -53,7 +53,7 @@ function factory(dependencies) { } const messageIds = this.fetchedMessages.map(message => message.id); const fetchedMessages = this._loadMessages({ - extraDomain: [['id', '>', Math.max(...messageIds)]], + extraDomain: [['id', '>', Math.max(...messageIds, 0)]], limit: false, }); for (const threadView of this.threadViews) { diff --git a/addons/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py index 74656ccbba..facb6c3533 100644 --- a/addons/sale_project/models/sale_order.py +++ b/addons/sale_project/models/sale_order.py @@ -17,7 +17,7 @@ class SaleOrder(models.Model): project_id = fields.Many2one( 'project.project', 'Project', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help='Select a non billable project on which tasks can be created.') - project_ids = fields.Many2many('project.project', compute="_compute_project_ids", string='Projects', copy=False, groups="project.group_project_user", help="Projects used in this sales order.") + project_ids = fields.Many2many('project.project', compute="_compute_project_ids", string='Projects', copy=False, groups="project.group_project_manager", help="Projects used in this sales order.") @api.depends('order_line.product_id.project_id') def _compute_tasks_ids(self): diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index 67fbfcf226..e625b628ad 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -1442,6 +1442,13 @@ $ribbon-padding: 100px; :not(.o_cookies_popup) { bottom: 0; } + .o_cookies_discrete { + .js_close_popup.o_cookies_bar_text_button, .o_cookies_bar_text_policy { + @include media-breakpoint-down(md) { + margin-bottom: 1rem; + } + } + } } .o_website_btn_loading { diff --git a/addons/website/static/src/snippets/s_popup/001.scss b/addons/website/static/src/snippets/s_popup/001.scss index a8200adf4e..76c0361ed0 100644 --- a/addons/website/static/src/snippets/s_popup/001.scss +++ b/addons/website/static/src/snippets/s_popup/001.scss @@ -10,6 +10,9 @@ .modal-dialog { height: auto; min-height: 100%; + @include media-breakpoint-down(xs) { + min-height: calc(100% - 2 * #{$modal-dialog-margin}); + } } // Close icon diff --git a/addons/website_event_sale/models/product.py b/addons/website_event_sale/models/product.py index cd4c1f4539..4a8f2ca86c 100644 --- a/addons/website_event_sale/models/product.py +++ b/addons/website_event_sale/models/product.py @@ -8,3 +8,9 @@ class Product(models.Model): _inherit = 'product.product' event_ticket_ids = fields.One2many('event.event.ticket', 'product_id', string='Event Tickets') + + def _is_add_to_cart_allowed(self): + # Allow adding event tickets to the cart regardless of product's rules + self.ensure_one() + res = super()._is_add_to_cart_allowed() + return res or any(event.website_published for event in self.event_ticket_ids.event_id) diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 231122a8fa..cbff52fd07 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -478,3 +478,7 @@ class Product(models.Model): # [1:] to remove the main image from the template, we only display # the template extra images here return variant_images + self.product_tmpl_id._get_images()[1:] + + def _is_add_to_cart_allowed(self): + self.ensure_one() + return self.user_has_groups('base.group_system') or (self.sale_ok and self.website_published) diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 84dc12b23a..e9f5ee0509 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -153,7 +153,10 @@ class SaleOrder(models.Model): SaleOrderLineSudo = self.env['sale.order.line'].sudo().with_context(product_context) # change lang to get correct name of attributes/values product_with_context = self.env['product.product'].with_context(product_context) - product = product_with_context.browse(int(product_id)) + product = product_with_context.browse(int(product_id)).exists() + + if not product or not product._is_add_to_cart_allowed(): + raise UserError(_("The given product does not exist therefore it cannot be added to cart.")) try: if add_qty: @@ -175,9 +178,6 @@ class SaleOrder(models.Model): # Create line if no line with product_id can be located if not order_line: - if not product: - raise UserError(_("The given product does not exist therefore it cannot be added to cart.")) - no_variant_attribute_values = kwargs.get('no_variant_attribute_values') or [] received_no_variant_values = product.env['product.template.attribute.value'].browse([int(ptav['value']) for ptav in no_variant_attribute_values]) received_combination = product.product_template_attribute_value_ids | received_no_variant_values diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index b37b19e1b9..6392d2b672 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -307,6 +307,9 @@ class Website(models.Model): request.session['sale_order_id'] = sale_order.id + # The order was created with SUPERUSER_ID, revert back to request user. + sale_order = sale_order.with_user(self.env.user).sudo() + # case when user emptied the cart if not request.session.get('sale_order_id'): request.session['sale_order_id'] = sale_order.id diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss index 25f6d4f449..9d0cf76c19 100644 --- a/addons/website_sale/static/src/scss/website_sale.scss +++ b/addons/website_sale/static/src/scss/website_sale.scss @@ -422,11 +422,11 @@ ul.wizard { max-width: 500px; min-width: 250px; max-height: 90%; - overflow: auto; + overflow-x: hidden; + overflow-y: auto; .cart_line { border-bottom: 1px #EEE solid; - max-width: 90%; } } diff --git a/addons/website_sale/tests/__init__.py b/addons/website_sale/tests/__init__.py index ee0a4e1bbc..b7ea7d16d7 100644 --- a/addons/website_sale/tests/__init__.py +++ b/addons/website_sale/tests/__init__.py @@ -5,6 +5,7 @@ from . import test_customize from . import test_sale_process from . import test_sitemap from . import test_website_sale_cart_recovery +from . import test_website_sale_cart from . import test_website_sale_mail from . import test_website_sale_pricelist from . import test_website_sale_product_attribute_value_config diff --git a/addons/website_sale/tests/test_sale_process.py b/addons/website_sale/tests/test_sale_process.py index 774a299be1..1461ef04a5 100644 --- a/addons/website_sale/tests/test_sale_process.py +++ b/addons/website_sale/tests/test_sale_process.py @@ -122,7 +122,11 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo): 'partner_id': partner_id, 'website_id': self.website.id, 'order_line': [(0, 0, { - 'product_id': self.env['product.product'].create({'name': 'Product A', 'list_price': 100}).id, + 'product_id': self.env['product.product'].create({ + 'name': 'Product A', + 'list_price': 100, + 'website_published': True, + 'sale_ok': True}).id, 'name': 'Product A', })] }) diff --git a/addons/website_sale/tests/test_website_sale_cart.py b/addons/website_sale/tests/test_website_sale_cart.py new file mode 100644 index 0000000000..6a85fd3d1b --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_cart.py @@ -0,0 +1,49 @@ +# coding: utf-8 + +from flectra.addons.website_sale.controllers.main import WebsiteSale +from flectra.addons.website.tools import MockRequest +from flectra.exceptions import UserError +from flectra.tests.common import SavepointCase, tagged + +@tagged('post_install', '-at_install') +class WebsiteSaleCart(SavepointCase): + + @classmethod + def setUpClass(cls): + super(WebsiteSaleCart, cls).setUpClass() + cls.website = cls.env['website'].browse(1) + cls.WebsiteSaleController = WebsiteSale() + cls.public_user = cls.env.ref('base.public_user') + + def test_add_cart_deleted_product(self): + # Create a published product then unlink it + product = self.env['product.product'].create({ + 'name': 'Test Product', + 'sale_ok': True, + 'website_published': True, + }) + product_id = product.id + product.unlink() + + with self.assertRaises(UserError): + with MockRequest(product.with_user(self.public_user).env, website=self.website.with_user(self.public_user)): + self.WebsiteSaleController.cart_update_json(product_id=product_id, add_qty=1) + + def test_add_cart_unpublished_product(self): + # Try to add an unpublished product + product = self.env['product.product'].create({ + 'name': 'Test Product', + 'sale_ok': True, + }) + + with self.assertRaises(UserError): + with MockRequest(product.with_user(self.public_user).env, website=self.website.with_user(self.public_user)): + self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1) + + # public but remove sale_ok + product.sale_ok = False + product.website_published = True + + with self.assertRaises(UserError): + with MockRequest(product.with_user(self.public_user).env, website=self.website.with_user(self.public_user)): + self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1)