[REF] website_sale*: cart lines creation & update

Clean whole flow of cart updates (new lines, modified quantities, ...)

* Use clean hooks to differentiate line update and creation, quantity verification, ...
* Avoid write calls with unmodified values, useless recomputations, queries, ...
* Create new lines directly with the correct quantity and values
* Delegate fields computation logic to sale as much as possible (price, discount, name, ...)

Part-of: odoo/odoo#82374
This commit is contained in:
Victor Feyens
2022-02-21 08:41:33 +00:00
parent 945bbe6894
commit a647a3323d
9 changed files with 367 additions and 336 deletions
@@ -29,17 +29,17 @@ class WebsiteEventBoothController(WebsiteEventController):
return json.dumps({'error': 'boothCategoryError'})
booth_values = self._prepare_booth_registration_values(event, kwargs)
order = request.website.sale_get_order(force_create=True)
order._cart_update(
order_sudo = request.website.sale_get_order(force_create=True)
order_sudo._cart_update(
product_id=booth_category.product_id.id,
set_qty=1,
event_booth_pending_ids=booths.ids,
registration_values=booth_values,
)
if order.amount_total:
if order_sudo.amount_total:
return json.dumps({'redirect': '/shop/checkout'})
elif order:
order.action_confirm()
elif order_sudo:
order_sudo.action_confirm()
request.website.sale_reset()
return self._prepare_booth_registration_success_values(event.name, booth_values)
@@ -8,51 +8,69 @@ from odoo import models, _
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _cart_find_product_line(self, product_id=None, line_id=None, **kwargs):
def _cart_find_product_line(
self, product_id=None, line_id=None,
event_booth_pending_ids=None, **kwargs
):
"""Check if there is another sale order line which already contains the requested event_booth_pending_ids
to overwrite it with the newly requested booths to avoid having multiple so_line related to the same booths"""
self.ensure_one()
lines = super(SaleOrder, self)._cart_find_product_line(product_id, line_id)
if line_id:
lines = super()._cart_find_product_line(product_id, line_id, **kwargs)
if not event_booth_pending_ids or line_id:
return lines
event_booth_pending_ids = kwargs.get('event_booth_pending_ids')
if event_booth_pending_ids:
lines = lines.filtered(
lambda line: any(booth.id in event_booth_pending_ids for booth in line.event_booth_pending_ids)
)
return lines
def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs):
values = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty, **kwargs)
event_booth_pending_ids = kwargs.get('event_booth_pending_ids')
if event_booth_pending_ids:
order_line = self.env['sale.order.line'].sudo().search([
('id', 'in', self.order_line.ids),
('event_booth_pending_ids', 'in', event_booth_pending_ids)])
booths = self.env['event.booth'].browse(event_booth_pending_ids)
new_registration_ids = [Command.create({
'event_booth_id': booth.id,
**kwargs.get('registration_values'),
}) for booth in booths]
if order_line:
event_booth_registration_ids = [Command.delete(reg.id)
for reg in order_line.event_booth_registration_ids] + new_registration_ids
else:
event_booth_registration_ids = new_registration_ids
return lines.filtered(
lambda line: any(booth.id in event_booth_pending_ids for booth in line.event_booth_pending_ids)
)
values.update(
event_id=booths.event_id.id,
event_booth_registration_ids=event_booth_registration_ids,
name=booths._get_booth_multiline_description,
)
return values
def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kwargs):
values = {}
def _verify_updated_quantity(self, order_line, product_id, new_qty, **kwargs):
"""Forbid quantity updates on event booth lines."""
product = self.env['product.product'].browse(product_id)
if product.detailed_type == 'event_booth' and set_qty > 1:
set_qty = 1
values['warning'] = _('You cannot manually change the quantity of an Event Booth product.')
values.update(super(SaleOrder, self)._cart_update(product_id, line_id, add_qty, set_qty, **kwargs))
if product.detailed_type == 'event_booth' and new_qty > 1:
return 1, _('You cannot manually change the quantity of an Event Booth product.')
return super()._verify_updated_quantity(order_line, product_id, new_qty, **kwargs)
def _prepare_order_line_values(
self, product_id, quantity, event_booth_pending_ids=False, registration_values=None,
**kwargs
):
"""Add corresponding event to the SOline creation values (if booths are provided)."""
values = super()._prepare_order_line_values(product_id, quantity, **kwargs)
if not event_booth_pending_ids:
return values
booths = self.env['event.booth'].browse(event_booth_pending_ids)
values['event_id'] = booths.event_id.id
values['event_booth_registration_ids'] = [
Command.create({
'event_booth_id': booth.id,
**registration_values,
}) for booth in booths
]
return values
# FIXME VFE investigate if it ever happens.
# Probably not
def _prepare_order_line_update_values(
self, order_line, quantity, event_booth_pending_ids=False, registration_values=None,
**kwargs
):
"""Delete existing booth registrations and create new ones with the update values."""
values = super()._prepare_order_line_update_values(order_line, quantity, **kwargs)
if not event_booth_pending_ids:
return values
booths = self.env['event.booth'].browse(event_booth_pending_ids)
values['event_booth_registration_ids'] = [
Command.delete(registration.id)
for registration in order_line.event_booth_registration_ids
] + [
Command.create({
'event_booth_id': booth.id,
**registration_values,
}) for booth in booths
]
+25 -7
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import _
from odoo.http import request, route
@@ -27,15 +28,32 @@ class WebsiteEventSaleController(WebsiteEventController):
def _create_attendees_from_registration_post(self, event, registration_data):
# we have at least one registration linked to a ticket -> sale mode activate
if any(info.get('event_ticket_id') for info in registration_data):
order = request.website.sale_get_order(force_create=True)
if not any(info.get('event_ticket_id') for info in registration_data):
return super()._create_attendees_from_registration_post(event, registration_data)
order_sudo = request.website.sale_get_order(force_create=True)
for info in [r for r in registration_data if r.get('event_ticket_id')]:
ticket = request.env['event.event.ticket'].sudo().browse(info['event_ticket_id'])
cart_values = order.with_context(event_ticket_id=ticket.id, fixed_price=True)._cart_update(product_id=ticket.product_id.id, add_qty=1)
info['sale_order_id'] = order.id
info['sale_order_line_id'] = cart_values.get('line_id')
tickets_data = defaultdict(int)
for data in registration_data:
event_ticket_id = data.get('event_ticket_id')
if event_ticket_id:
tickets_data[event_ticket_id] += 1
cart_data = {}
for ticket_id, count in tickets_data.items():
ticket_sudo = request.env['event.event.ticket'].sudo().browse(ticket_id)
cart_values = order_sudo._cart_update(
product_id=ticket_sudo.product_id.id,
add_qty=count,
event_ticket_id=ticket_id,
)
cart_data[ticket_id] = cart_values['line_id']
for data in registration_data:
event_ticket_id = data.get('event_ticket_id')
if event_ticket_id:
data['sale_order_id'] = order_sudo.id
data['sale_order_line_id'] = cart_data[event_ticket_id]
return super()._create_attendees_from_registration_post(event, registration_data)
+70 -83
View File
@@ -7,104 +7,91 @@ from odoo.exceptions import UserError
class SaleOrder(models.Model):
_inherit = "sale.order"
def _cart_find_product_line(self, product_id=None, line_id=None, **kwargs):
self.ensure_one()
lines = super(SaleOrder, self)._cart_find_product_line(product_id, line_id)
if line_id:
def _cart_find_product_line(self, product_id=None, line_id=None, event_ticket_id=False, **kwargs):
lines = super()._cart_find_product_line(product_id, line_id, **kwargs)
if line_id or not event_ticket_id:
return lines
domain = [('id', 'in', lines.ids)]
if self.env.context.get("event_ticket_id"):
domain.append(('event_ticket_id', '=', self.env.context.get("event_ticket_id")))
return self.env['sale.order.line'].sudo().search(domain)
def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs):
order = self.env['sale.order'].sudo().browse(order_id)
if self._context.get('pricelist') != order.pricelist_id.id:
self = self.with_context(pricelist=order.pricelist_id.id)
return lines.filtered(
lambda line: line.event_ticket_id.id == event_ticket_id
)
values = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty, **kwargs)
event_ticket_id = None
if self.env.context.get("event_ticket_id"):
event_ticket_id = self.env.context.get("event_ticket_id")
else:
product = self.env['product.product'].browse(product_id)
if product.event_ticket_ids:
event_ticket_id = product.event_ticket_ids[0].id
def _verify_updated_quantity(self, order_line, product_id, new_qty, event_ticket_id=False, **kwargs):
"""Restrict quantity updates for event tickets according to available seats."""
new_qty, warning = super()._verify_updated_quantity(order_line, product_id, new_qty, **kwargs)
if event_ticket_id:
ticket = self.env['event.event.ticket'].browse(event_ticket_id)
if product_id != ticket.product_id.id:
raise UserError(_("The ticket doesn't match with this product."))
values['product_id'] = ticket.product_id.id
values['event_id'] = ticket.event_id.id
values['event_ticket_id'] = ticket.id
if order.pricelist_id.discount_policy == 'without_discount':
values['price_unit'] = ticket.price
if not event_ticket_id:
if not order_line.event_ticket_id or new_qty < order_line.product_uom_qty:
return new_qty, warning
else:
values['price_unit'] = ticket.price_reduce
values['name'] = ticket._get_ticket_multiline_description()
return order_line.product_uom_qty, _("You cannot raise manually the event ticket quantity in your cart")
# avoid writing related values that end up locking the product record
values.pop('event_ok', None)
# Adding new ticket to the cart (might be automatically linked to an existing line)
ticket = self.env['event.event.ticket'].browse(event_ticket_id).exists()
if not ticket:
raise UserError(_("The provided ticket doesn't exist"))
# TODO TDE consider full cart qty and not only added qty
# if event seats are not auto confirmed.
# Since created registrations are automatically reserved
# We should only consider new added qty and not full quantity
# when checking for seat availability
existing_qty = order_line.product_uom_qty if order_line else 0
qty_added = new_qty - existing_qty
warning = ''
if ticket.seats_limited and ticket.seats_available <= 0:
# Remove existing line if exists and do not add a new one
# if no ticket is available anymore
new_qty = existing_qty
warning = _(
'Sorry, The %(ticket)s tickets for the %(event)s event are sold out.',
ticket=ticket.name,
event=ticket.event_id.name,
)
elif ticket.seats_limited and qty_added > ticket.seats_available:
new_qty = existing_qty + ticket.seats_available
warning = _(
'Sorry, only %(remaining_seats)d seats are still available for the %(ticket)s ticket for the %(event)s event.',
remaining_seats=ticket.seats_available,
ticket=ticket.name,
event=ticket.event_id.name,
)
return new_qty, warning
def _prepare_order_line_values(self, product_id, quantity, event_ticket_id=False, **kwargs):
"""Add corresponding event to the SOline creation values (if ticket is provided)."""
values = super()._prepare_order_line_values(product_id, quantity, **kwargs)
if not event_ticket_id:
return values
ticket = self.env['event.event.ticket'].browse(event_ticket_id)
if ticket.product_id.id != product_id:
raise UserError(_("The ticket doesn't match with this product."))
values['event_id'] = ticket.event_id.id
values['event_ticket_id'] = ticket.id
return values
def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kwargs):
OrderLine = self.env['sale.order.line']
def _update_cart_line_values(self, order_line, update_values):
"""Remove event registrations on quantity decrease."""
old_qty = order_line.product_uom_qty
try:
if add_qty:
add_qty = float(add_qty)
except ValueError:
add_qty = 1
try:
if set_qty:
set_qty = float(set_qty)
except ValueError:
set_qty = 0
super()._update_cart_line_values(order_line, update_values)
if not order_line.event_ticket_id:
return
if line_id:
line = OrderLine.browse(line_id)
ticket = line.event_ticket_id
old_qty = int(line.product_uom_qty)
if ticket.id:
self = self.with_context(event_ticket_id=ticket.id, fixed_price=1)
else:
line = None
ticket = self.env['event.event.ticket'].search([('product_id', '=', product_id)], limit=1)
old_qty = 0
new_qty = set_qty if set_qty else (add_qty or 0 + old_qty)
# case: buying tickets for a sold out ticket
values = {}
if ticket and ticket.seats_limited and ticket.seats_available <= 0:
values['warning'] = _('Sorry, The %(ticket)s tickets for the %(event)s event are sold out.') % {
'ticket': ticket.name,
'event': ticket.event_id.name}
new_qty, set_qty, add_qty = 0, 0, -old_qty
# case: buying tickets, too much attendees
elif ticket and ticket.seats_limited and new_qty > ticket.seats_available:
values['warning'] = _('Sorry, only %(remaining_seats)d seats are still available for the %(ticket)s ticket for the %(event)s event.') % {
'remaining_seats': ticket.seats_available,
'ticket': ticket.name,
'event': ticket.event_id.name}
new_qty, set_qty, add_qty = ticket.seats_available, ticket.seats_available, 0
values.update(super(SaleOrder, self)._cart_update(product_id, line_id, add_qty, set_qty, **kwargs))
# removing attendees
if ticket and new_qty < old_qty:
new_qty = order_line.product_uom_qty
if new_qty < old_qty:
attendees = self.env['event.registration'].search([
('state', '!=', 'cancel'),
('sale_order_id', 'in', self.ids), # To avoid break on multi record set
('event_ticket_id', '=', ticket.id),
('sale_order_id', '=', self.id),
('event_ticket_id', '=', order_line.event_ticket_id.id),
], offset=new_qty, limit=(old_qty - new_qty), order='create_date asc')
attendees.action_cancel()
# adding attendees
elif ticket and new_qty > old_qty:
# do not do anything, attendees will be created at SO confirmation if not given previously
pass
return values
class SaleOrderLine(models.Model):
@@ -58,19 +58,14 @@ tour.register('event_buy_tickets', {
content: "go to cart",
trigger: 'a:contains(Return to Cart)',
},
{
content: "Modify the cart to add 1 unit of `VIP` ticket type",
extra_trigger: "#cart_products:contains(Standard):contains(VIP)",
trigger: "#cart_products tr:contains(VIP) .fa-plus",
},
{
content: "Now click on `Process Checkout`",
extra_trigger: 'a:has(.my_cart_quantity):contains(4),#cart_products input.js_quantity[value="3"]',
extra_trigger: 'a:has(.my_cart_quantity):contains(3),#cart_products input.js_quantity[value="3"]',
trigger: '.btn-primary:contains("Process Checkout")'
},
{
content: "Check that the subtotal is 5,500.00 USD", // this test will fail if the currency of the main company is not USD
trigger: '#order_total_untaxed .oe_currency_value:contains("5,500.00")',
content: "Check that the subtotal is 4,000.00 USD", // this test will fail if the currency of the main company is not USD
trigger: '#order_total_untaxed .oe_currency_value:contains("4,000.00")',
run: function () {}, // it's a check
},
{
+168 -169
View File
@@ -1,16 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import random
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import api, models, fields, _
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.http import request
from odoo.osv import expression
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
@@ -80,78 +78,6 @@ class SaleOrder(models.Model):
return abandoned_domain
return expression.distribute_not(['!'] + abandoned_domain) # negative domain
def _cart_find_product_line(self, product_id=None, line_id=None, **kwargs):
"""Find the cart line matching the given parameters.
If a product_id is given, the line will match the product only if the
line also has the same special attributes: `no_variant` attributes and
`is_custom` values.
"""
self.ensure_one()
product = self.env['product.product'].browse(product_id)
# split lines with the same product if it has untracked attributes
if product and (product.product_tmpl_id.has_dynamic_attributes() or product.product_tmpl_id._has_no_variant_attributes()) and not line_id:
return self.env['sale.order.line']
domain = [('order_id', '=', self.id), ('product_id', '=', product_id)]
if line_id:
domain += [('id', '=', line_id)]
else:
domain += [('product_custom_attribute_value_ids', '=', False)]
return self.env['sale.order.line'].sudo().search(domain)
def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs):
order = self.sudo().browse(order_id)
order = order.with_company(order.company_id)
product = order.env['product.product'].browse(product_id)
discount = 0
order_date = order.date_order or fields.Date.today()
qty = qty or 1.0
pricelist = order.pricelist_id
pricelist_price, pricelist_rule_id = pricelist._get_product_price_rule(product, qty, date=order_date)
# TODO VFE factorize discount computation or delegate discount computation to sale computes
if pricelist.discount_policy == 'without_discount':
# This part is pretty much a copy-paste of the method '_onchange_discount' of
# 'sale.order.line'.
pricelist_base_price, currency = request.env['sale.order.line']._get_real_price_currency(
product, pricelist_rule_id, qty, product.uom_id, date=order_date)
if pricelist_base_price != 0:
if order.currency_id != currency:
# we need new_list_price in the same currency as price, which is in the SO's pricelist's currency
pricelist_base_price = currency._convert(
pricelist_base_price, order.currency_id, order.company_id, order_date)
price = pricelist_base_price
discount = (pricelist_base_price - pricelist_price) / pricelist_base_price * 100
if discount < 0:
# In case the discount is negative, we don't want to show it to the customer,
# but we still want to use the price defined on the pricelist
discount = 0
price = pricelist_price
else:
# In case the price_unit equal 0 and therefore not able to calculate the discount,
# we fallback on the price defined on the pricelist.
price = pricelist_price
else:
price = pricelist_price
if order.pricelist_id and order.partner_id:
order_line = order._cart_find_product_line(product.id)
if order_line:
price = self.env['account.tax']._fix_tax_included_price_company(
price, product.taxes_id, order_line[0].tax_id, order.company_id)
return {
'product_id': product_id,
'product_uom_qty': qty,
'order_id': order_id,
'price_unit': price,
'discount': discount,
}
def _cart_update_pricelist(self, pricelist_id=None, update_pricelist=False):
self.ensure_one()
@@ -166,122 +92,195 @@ class SaleOrder(models.Model):
if update_pricelist or previous_pricelist_id != self.pricelist_id.id:
self.update_prices()
def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kwargs):
def _cart_update(self, product_id, line_id=None, add_qty=0, set_qty=0, **kwargs):
""" Add or set product quantity, add_qty can be negative """
self.ensure_one()
self = self.with_company(self.company_id)
SaleOrderLineSudo = self.env['sale.order.line'].sudo()
product = self.env['product.product'].browse(int(product_id))
if self.state != 'draft':
request.session['sale_order_id'] = None
raise UserError(_('It is forbidden to modify a sales order which is not in draft status.'))
product = self.env['product.product'].browse(product_id).exists()
if not product:
raise UserError(_("The given product does not exist therefore it cannot be added to cart."))
if line_id is not False:
order_line = self._cart_find_product_line(product_id, line_id, **kwargs)[:1]
else:
order_line = self.env['sale.order.line']
try:
if add_qty:
add_qty = int(add_qty)
except ValueError:
add_qty = 1
try:
if set_qty:
set_qty = int(set_qty)
except ValueError:
set_qty = 0
quantity = 0
order_line = False
if self.state != 'draft':
request.session['sale_order_id'] = None
raise UserError(_('It is forbidden to modify a sales order which is not in draft status.'))
if line_id is not False:
order_line = self._cart_find_product_line(product_id, line_id, **kwargs)[:1]
# 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
product_template = product.product_tmpl_id
# handle all cases where incorrect or incomplete data are received
combination = product_template._get_closest_possible_combination(received_combination)
# get or create (if dynamic) the correct variant
product = product_template._create_product_variant(combination)
if not product:
raise UserError(_("The given combination does not exist therefore it cannot be added to cart."))
product_id = product.id
values = self._website_product_id_change(self.id, product_id, qty=1, **kwargs)
# add no_variant attributes that were not received
for ptav in combination.filtered(lambda ptav: ptav.attribute_id.create_variant == 'no_variant' and ptav not in received_no_variant_values):
no_variant_attribute_values.append({
'value': ptav.id,
})
# save no_variant attributes values
if no_variant_attribute_values:
values['product_no_variant_attribute_value_ids'] = [
(6, 0, [int(attribute['value']) for attribute in no_variant_attribute_values])
]
# add is_custom attribute values that were not received
custom_values = kwargs.get('product_custom_attribute_values') or []
received_custom_values = product.env['product.template.attribute.value'].browse([int(ptav['custom_product_template_attribute_value_id']) for ptav in custom_values])
for ptav in combination.filtered(lambda ptav: ptav.is_custom and ptav not in received_custom_values):
custom_values.append({
'custom_product_template_attribute_value_id': ptav.id,
'custom_value': '',
})
# save is_custom attributes values
if custom_values:
values['product_custom_attribute_value_ids'] = [(0, 0, {
'custom_product_template_attribute_value_id': custom_value['custom_product_template_attribute_value_id'],
'custom_value': custom_value['custom_value']
}) for custom_value in custom_values]
# create the line
order_line = SaleOrderLineSudo.create(values)
try:
order_line._compute_tax_id()
except ValidationError as e:
# The validation may occur in backend (eg: taxcloud) but should fail silently in frontend
_logger.debug("ValidationError occurs during tax compute. %s" % (e))
if add_qty:
add_qty -= 1
# compute new quantity
if set_qty:
quantity = set_qty
elif add_qty is not None:
quantity = order_line.product_uom_qty + (add_qty or 0)
if order_line:
quantity = order_line.product_uom_qty + (add_qty or 0)
else:
quantity = add_qty or 0
# Remove zero of negative lines
if quantity <= 0:
linked_line = order_line.linked_line_id
order_line.unlink()
if quantity > 0:
quantity, warning = self._verify_updated_quantity(
order_line,
product_id,
quantity,
**kwargs,
)
else:
# update line
no_variant_attributes_price_extra = [ptav.price_extra for ptav in order_line.product_no_variant_attribute_value_ids]
values = self.with_context(no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra))._website_product_id_change(self.id, product_id, qty=quantity, **kwargs)
# If the line will be removed anyway, there is no need to verify
# the requested quantity update.
warning = ''
order_line.write(values)
if order_line and quantity <= 0:
# Remove zero or negative lines
order_line.unlink()
order_line = self.env['sale.order.line']
elif order_line:
# Update existing line
update_values = self._prepare_order_line_update_values(order_line, quantity, **kwargs)
if update_values:
self._update_cart_line_values(order_line, update_values)
elif quantity >= 0:
# Create new line
order_line_values = self._prepare_order_line_values(product_id, quantity, **kwargs)
order_line = self.env['sale.order.line'].sudo().create(order_line_values)
# link a product to the sales order
if kwargs.get('linked_line_id'):
linked_line = SaleOrderLineSudo.browse(kwargs['linked_line_id'])
order_line.write({
'linked_line_id': linked_line.id,
})
return {
'line_id': order_line.id,
'quantity': quantity,
'option_ids': list(set(order_line.option_line_ids.filtered(lambda l: l.order_id == order_line.order_id).ids)),
'warning': warning,
}
option_lines = self.order_line.filtered(lambda l: l.linked_line_id.id == order_line.id)
def _cart_find_product_line(self, product_id, line_id=None, **kwargs):
"""Find the cart line matching the given parameters.
return {'line_id': order_line.id, 'quantity': quantity, 'option_ids': list(set(option_lines.ids))}
If a product_id is given, the line will match the product only if the
line also has the same special attributes: `no_variant` attributes and
`is_custom` values.
"""
self.ensure_one()
SaleOrderLine = self.env['sale.order.line']
if not self.order_line:
return SaleOrderLine
product = self.env['product.product'].browse(product_id)
if not line_id and (
product.product_tmpl_id.has_dynamic_attributes()
or product.product_tmpl_id._has_no_variant_attributes()
):
return SaleOrderLine
domain = [('order_id', '=', self.id), ('product_id', '=', product_id)]
if line_id:
domain += [('id', '=', line_id)]
else:
domain += [('product_custom_attribute_value_ids', '=', False)]
return SaleOrderLine.search(domain)
# hook to be overridden
def _verify_updated_quantity(self, order_line, product_id, new_qty, **kwargs):
return new_qty, ''
def _prepare_order_line_values(
self, product_id, quantity, linked_line_id=False,
no_variant_attribute_values=None, product_custom_attribute_values=None,
**kwargs
):
self.ensure_one()
product = self.env['product.product'].browse(product_id)
no_variant_attribute_values = 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
product_template = product.product_tmpl_id
# handle all cases where incorrect or incomplete data are received
combination = product_template._get_closest_possible_combination(received_combination)
# get or create (if dynamic) the correct variant
product = product_template._create_product_variant(combination)
if not product:
raise UserError(_("The given combination does not exist therefore it cannot be added to cart."))
values = {
'product_id': product.id,
'product_uom_qty': quantity,
'order_id': self.id,
'linked_line_id': linked_line_id,
}
# add no_variant attributes that were not received
for ptav in combination.filtered(
lambda ptav: ptav.attribute_id.create_variant == 'no_variant' and ptav not in received_no_variant_values
):
no_variant_attribute_values.append({
'value': ptav.id,
})
if no_variant_attribute_values:
values['product_no_variant_attribute_value_ids'] = [
fields.Command.set([int(attribute['value']) for attribute in no_variant_attribute_values])
]
# add is_custom attribute values that were not received
custom_values = product_custom_attribute_values or []
received_custom_values = product.env['product.template.attribute.value'].browse([
int(ptav['custom_product_template_attribute_value_id'])
for ptav in custom_values
])
for ptav in combination.filtered(lambda ptav: ptav.is_custom and ptav not in received_custom_values):
custom_values.append({
'custom_product_template_attribute_value_id': ptav.id,
'custom_value': '',
})
if custom_values:
values['product_custom_attribute_value_ids'] = [
fields.Command.create({
'custom_product_template_attribute_value_id': custom_value['custom_product_template_attribute_value_id'],
'custom_value': custom_value['custom_value'],
}) for custom_value in custom_values
]
return values
def _prepare_order_line_update_values(
self, order_line, quantity, linked_line_id=False, **kwargs
):
self.ensure_one()
values = {}
if quantity != order_line.product_uom_qty:
values['product_uom_qty'] = quantity
if linked_line_id and linked_line_id != order_line.linked_line_id.id:
values['linked_line_id'] = linked_line_id
return values
# hook to be overridden
def _update_cart_line_values(self, order_line, update_values):
self.ensure_one()
order_line.write(update_values)
def _cart_accessories(self):
""" Suggest accessories based on 'Accessory Products' of products in cart """
@@ -274,10 +274,10 @@ class TestWebsitePriceList(TransactionCase):
self.assertEqual(sol.price_total, 0)
so.pricelist_id = website_pricelist
with MockRequest(self.env, website=current_website, sale_order_id=so.id):
so._cart_update(product_id=product.id, line_id=sol.id, set_qty=5)
so._cart_update(product_id=product.id, line_id=sol.id, set_qty=6)
self.assertEqual(sol.price_unit, 10.0, 'Pricelist price should be applied')
self.assertEqual(sol.price_reduce, 10.0, 'Pricelist price should be applied')
self.assertEqual(sol.price_total, 50.0)
self.assertEqual(sol.price_total, 60.0)
def simulate_frontend_context(self, website_id=1):
@@ -191,12 +191,12 @@ class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon):
sol = self.env['sale.order.line'].create({
'product_id': test_product.product_variant_id.id,
'order_id': so.id,
'tax_id': [(6, 0, [tax10.id])],
})
self.assertEqual(round(sol.price_total), 110.0, "110$ with 10% included tax")
self.assertEqual(round(sol.price_total), 55.0, "110$ with 50% discount 10% included tax")
self.assertEqual(round(sol.price_tax), 5.0, "110$ with 50% discount 10% included tax")
so.pricelist_id = pricelist
so.fiscal_position_id = fpos
sol._compute_tax_id()
with MockRequest(self.env, website=current_website, sale_order_id=so.id):
so._cart_update(product_id=test_product.product_variant_id.id, line_id=sol.id, set_qty=1)
self.assertEqual(round(sol.price_total), 50, "100$ with 50% discount + 0% tax (mapped from fp 10% -> 0%)")
so._cart_update(product_id=test_product.product_variant_id.id, line_id=sol.id, set_qty=2)
self.assertEqual(round(sol.price_total), 100, "100$ with 50% discount + 0% tax (mapped from fp 10% -> 0%)")
+31 -17
View File
@@ -9,26 +9,40 @@ class SaleOrder(models.Model):
warning_stock = fields.Char('Warning')
def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kwargs):
values = super(SaleOrder, self)._cart_update(product_id, line_id, add_qty, set_qty, **kwargs)
line_id = values.get('line_id')
def _verify_updated_quantity(self, order_line, product_id, new_qty, **kwargs):
self.ensure_one()
product = self.env['product.product'].browse(product_id)
if product.type == 'product' and not product.allow_out_of_stock_order:
available_qty = product.with_context(warehouse=self.warehouse_id.id).free_qty
product_qty_in_cart = sum(
self.order_line.filtered(
lambda p: p.product_id.id == product_id
).mapped('product_uom_qty')
)
for line in self.order_line:
if line.product_id.type == 'product' and not line.product_id.allow_out_of_stock_order:
cart_qty = sum(self.order_line.filtered(lambda p: p.product_id.id == line.product_id.id).mapped('product_uom_qty'))
if cart_qty > line.product_id.with_context(warehouse=self.warehouse_id.id).free_qty and (line_id == line.id):
qty = line.product_id.with_context(warehouse=self.warehouse_id.id).free_qty - cart_qty
new_val = super(SaleOrder, self)._cart_update(line.product_id.id, line.id, qty, 0, **kwargs)
values.update(new_val)
old_qty = order_line.product_uom_qty if order_line else 0
added_qty = new_qty - old_qty
# Make sure line still exists, it may have been deleted in super()_cartupdate because qty can be <= 0
if line.exists() and new_val['quantity']:
line.warning_stock = _('You ask for %s products but only %s is available') % (cart_qty, new_val['quantity'])
values['warning'] = line.warning_stock
if available_qty < (product_qty_in_cart + added_qty):
allowed_line_qty = available_qty - (product_qty_in_cart - old_qty)
if allowed_line_qty > 0:
warning = _(
'You ask for %s products but only %s is available',
product_qty_in_cart + added_qty,
available_qty,
)
if order_line:
order_line.warning_stock = warning
else:
self.warning_stock = _("Some products became unavailable and your cart has been updated. We're sorry for the inconvenience.")
values['warning'] = self.warning_stock
return values
self.warning_stock = warning
else: # 0 or negative allowed_qty
# if existing line: it will be deleted
# if no existing line: no line will be created
self.warning_stock = _(
"Some products became unavailable and your cart has been updated. We're sorry for the inconvenience.")
return allowed_line_qty, order_line.warning_stock or self.warning_stock
return super()._verify_updated_quantity(order_line, product_id, new_qty, **kwargs)
def _get_stock_warning(self, clear=True):
self.ensure_one()