[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:
@@ -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
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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%)")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user