mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-28 06:12:01 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -2,7 +2,10 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import website
|
||||
from . import product_combo
|
||||
from . import product_feed
|
||||
from . import product_product
|
||||
from . import product_ribbon
|
||||
from . import product_template
|
||||
from . import res_config_settings
|
||||
from . import sale_order
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ProductCombo(models.Model):
|
||||
_inherit = 'product.combo'
|
||||
|
||||
def _get_max_quantity(self, website, sale_order, **kwargs):
|
||||
""" The max quantity of a combo is the max quantity of its combo item with the highest max
|
||||
quantity. If one of the combo items has no max quantity, then the combo also has no max
|
||||
quantity.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param website website: The website for which to compute the max quantity.
|
||||
:return: The max quantity of the combo.
|
||||
:rtype: float | None
|
||||
"""
|
||||
self.ensure_one()
|
||||
max_quantities = [
|
||||
item.product_id._get_max_quantity(website, sale_order, **kwargs)
|
||||
for item in self.combo_item_ids
|
||||
]
|
||||
return max(max_quantities) if (None not in max_quantities) else None
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ProductFeed(models.Model):
|
||||
_inherit = 'product.feed'
|
||||
|
||||
def _prepare_gmc_stock_info(self, product):
|
||||
"""Override of `website_sale` to check the stock level if the current product cannot be out
|
||||
of stock."""
|
||||
stock_info = super()._prepare_gmc_stock_info(product)
|
||||
if product._is_sold_out():
|
||||
stock_info['availability'] = 'out_of_stock'
|
||||
return stock_info
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields, _
|
||||
from odoo.http import request
|
||||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
|
|
@ -14,49 +12,82 @@ class ProductProduct(models.Model):
|
|||
self.ensure_one()
|
||||
return partner in self.stock_notification_partner_ids
|
||||
|
||||
def _get_cart_qty(self, website=None):
|
||||
if not self.allow_out_of_stock_order:
|
||||
website = website or self.env['website'].get_current_website()
|
||||
# When the cron is run manually, request has no attribute website, and that would cause a crash
|
||||
# so we check for it
|
||||
cart = website and request and hasattr(request, 'website') and website.sale_get_order() or None
|
||||
if cart:
|
||||
return sum(
|
||||
cart._get_common_product_lines(product=self).mapped('product_uom_qty')
|
||||
)
|
||||
return 0
|
||||
def _get_max_quantity(self, website, sale_order, **kwargs):
|
||||
""" The max quantity of a product is the difference between the quantity that's free to use
|
||||
and the quantity that's already been added to the cart.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param website website: The website for which to compute the max quantity.
|
||||
:return: The max quantity of the product.
|
||||
:rtype: float | None
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.is_storable and not self.allow_out_of_stock_order:
|
||||
free_qty = website._get_product_available_qty(self.sudo(), **kwargs)
|
||||
cart_qty = sale_order._get_cart_qty(self.id)
|
||||
return free_qty - cart_qty
|
||||
return None
|
||||
|
||||
def _is_sold_out(self):
|
||||
combination_info = self.with_context(website_sale_stock_get_quantity=True).product_tmpl_id._get_combination_info(product_id=self.id)
|
||||
return combination_info['product_type'] == 'product' and combination_info['free_qty'] <= 0
|
||||
"""Return whether the product is sold out (no available quantity).
|
||||
|
||||
If a product inventory is not tracked, or if it's allowed to be sold regardless
|
||||
of availabilities, the product is never considered sold out.
|
||||
|
||||
:return: whether the product can still be sold
|
||||
:rtype: bool
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.is_storable or self.allow_out_of_stock_order:
|
||||
return False
|
||||
free_qty = self.env['website'].get_current_website()._get_product_available_qty(self.sudo())
|
||||
return free_qty <= 0
|
||||
|
||||
def _website_show_quick_add(self):
|
||||
return (self.allow_out_of_stock_order or not self._is_sold_out()) and super()._website_show_quick_add()
|
||||
return not self._is_sold_out() and super()._website_show_quick_add()
|
||||
|
||||
def _send_availability_email(self):
|
||||
website = self.env['website'].get_current_website()
|
||||
for product in self.search([('stock_notification_partner_ids', '!=', False)]):
|
||||
if product._is_sold_out():
|
||||
continue
|
||||
for partner in product.stock_notification_partner_ids:
|
||||
self_ctxt = self.with_context(lang=partner.lang)
|
||||
self_ctxt = self.with_context(lang=partner.lang).with_user(website.salesperson_id)
|
||||
product_ctxt = product.with_context(lang=partner.lang)
|
||||
body_html = self_ctxt.env['ir.qweb']._render(
|
||||
'website_sale_stock.availability_email_body', {'product': product_ctxt})
|
||||
msg = self_ctxt.env['mail.message'].sudo().new(dict(body=body_html, record_name=product_ctxt.name))
|
||||
full_mail = self_ctxt.env['mail.render.mixin']._render_encapsulate(
|
||||
"mail.mail_notification_light",
|
||||
'website_sale_stock.availability_email_body',
|
||||
{'product': product_ctxt},
|
||||
)
|
||||
full_mail = product_ctxt.env['mail.render.mixin']._render_encapsulate(
|
||||
'mail.mail_notification_light',
|
||||
body_html,
|
||||
add_context=dict(message=msg, model_description=_("Product")),
|
||||
add_context={'model_description': _("Product")},
|
||||
context_record=product_ctxt,
|
||||
)
|
||||
context = {'lang': partner.lang} # Use partner lang to translate mail subject below
|
||||
mail_values = {
|
||||
"subject": _("The product '%(product_name)s' is now available", product_name=product_ctxt.name),
|
||||
"email_from": (product.company_id.partner_id or self.env.user).email_formatted,
|
||||
"email_to": partner.email_formatted,
|
||||
"body_html": full_mail,
|
||||
'subject': _(
|
||||
"The product '%(product_name)s' is now available",
|
||||
product_name=product_ctxt.name
|
||||
),
|
||||
'email_from': (website.company_id.partner_id or self_ctxt.env.user).email_formatted,
|
||||
'email_to': partner.email_formatted,
|
||||
'body_html': full_mail,
|
||||
}
|
||||
del context
|
||||
|
||||
mail = self_ctxt.env['mail.mail'].sudo().create(mail_values)
|
||||
mail.send(raise_exception=False)
|
||||
product.stock_notification_partner_ids -= partner
|
||||
|
||||
def _to_markup_data(self, website):
|
||||
""" Override of `website_sale` to include the product availability in the offer. """
|
||||
markup_data = super()._to_markup_data(website)
|
||||
if self.is_product_variant and self.is_storable:
|
||||
if not self._is_sold_out():
|
||||
availability = 'https://schema.org/InStock'
|
||||
else:
|
||||
availability = 'https://schema.org/OutOfStock'
|
||||
markup_data['offers']['availability'] = availability
|
||||
return markup_data
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductRibbon(models.Model):
|
||||
_inherit = 'product.ribbon'
|
||||
|
||||
assign = fields.Selection(
|
||||
selection_add=[('out_of_stock', "when out of stock")],
|
||||
ondelete={'out_of_stock': 'cascade'},
|
||||
help=(
|
||||
"Defines how this ribbon is assigned to products:\n"
|
||||
"- Manually: You assign the ribbon manually to products.\n"
|
||||
"- Sale: Applied when the product is visibly on sale.\n"
|
||||
"- New: Applied based on the New period you will define.\n"
|
||||
"- Out Of Stock: Applied when the product is out of stock."
|
||||
),
|
||||
)
|
||||
|
||||
def _is_applicable_for(self, product, price_data):
|
||||
"""Override of `website_sale` to handle `out_of_stock` ribbons."""
|
||||
return super()._is_applicable_for(product, price_data) or (
|
||||
product
|
||||
and self.assign == 'out_of_stock'
|
||||
and not product.product_tmpl_id.allow_out_of_stock_order
|
||||
and product._is_sold_out()
|
||||
)
|
||||
|
|
@ -1,65 +1,135 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import fields, models
|
||||
from odoo.tools.translate import html_translate
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.http import request
|
||||
from odoo.tools import float_round
|
||||
from odoo.tools.translate import html_translate
|
||||
|
||||
from odoo.addons.website.models import ir_http
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
allow_out_of_stock_order = fields.Boolean(string='Continue selling when out-of-stock', default=True)
|
||||
allow_out_of_stock_order = fields.Boolean(string="Sell when Out-of-Stock", default=True)
|
||||
|
||||
available_threshold = fields.Float(string='Show Threshold', default=5.0)
|
||||
show_availability = fields.Boolean(string='Show availability Qty', default=False)
|
||||
available_threshold = fields.Float(string="Show Threshold", default=5.0)
|
||||
show_availability = fields.Boolean(string="Show availability Qty", default=False)
|
||||
out_of_stock_message = fields.Html(string="Out-of-Stock Message", translate=html_translate)
|
||||
|
||||
def _get_combination_info(self, combination=False, product_id=False, add_qty=1, pricelist=False, parent_combination=False, only_template=False):
|
||||
combination_info = super(ProductTemplate, self)._get_combination_info(
|
||||
combination=combination, product_id=product_id, add_qty=add_qty, pricelist=pricelist,
|
||||
parent_combination=parent_combination, only_template=only_template)
|
||||
def _is_sold_out(self):
|
||||
"""Return whether the product is sold out (no available quantity).
|
||||
|
||||
If a product inventory is not tracked, or if it's allowed to be sold regardless
|
||||
of availabilities, the product is never considered sold out.
|
||||
|
||||
Note: only checks the availability of the first variant of the template.
|
||||
|
||||
:return: whether the product can still be sold
|
||||
:rtype: bool
|
||||
"""
|
||||
if not self.is_storable or self.allow_out_of_stock_order:
|
||||
return False
|
||||
return self.product_variant_id._is_sold_out()
|
||||
|
||||
def _website_show_quick_add(self):
|
||||
return (
|
||||
super()._website_show_quick_add()
|
||||
and not self._is_sold_out()
|
||||
)
|
||||
|
||||
def _get_additionnal_combination_info(self, product_or_template, quantity, uom, date, website):
|
||||
res = super()._get_additionnal_combination_info(product_or_template, quantity, uom, date, website)
|
||||
|
||||
if not self.env.context.get('website_sale_stock_get_quantity'):
|
||||
return combination_info
|
||||
return res
|
||||
|
||||
if combination_info['product_id']:
|
||||
product = self.env['product.product'].sudo().browse(combination_info['product_id'])
|
||||
website = self.env['website'].get_current_website()
|
||||
free_qty = product.with_context(warehouse=website._get_warehouse_available()).free_qty
|
||||
has_stock_notification = product._has_stock_notification(self.env.user.partner_id) \
|
||||
or request \
|
||||
and product.id in request.session.get('product_with_stock_notification_enabled',
|
||||
set())
|
||||
if product_or_template.type == 'combo':
|
||||
# The max quantity of a combo product is the max quantity of its combo with the lowest
|
||||
# max quantity. If none of the combos has a max quantity, then the combo product also
|
||||
# has no max quantity.
|
||||
max_quantities = [
|
||||
max_quantity for combo in product_or_template.sudo().combo_ids
|
||||
if (max_quantity := combo._get_max_quantity(website, request.cart)) is not None
|
||||
]
|
||||
if max_quantities:
|
||||
# No uom conversion: combo are not supposed to be sold with other uoms.
|
||||
res['max_combo_quantity'] = min(max_quantities)
|
||||
|
||||
if not product_or_template.is_storable:
|
||||
return res
|
||||
|
||||
res.update({
|
||||
'is_storable': True,
|
||||
'allow_out_of_stock_order': product_or_template.allow_out_of_stock_order,
|
||||
'available_threshold': product_or_template.available_threshold,
|
||||
})
|
||||
if product_or_template.is_product_variant:
|
||||
product_sudo = product_or_template.sudo()
|
||||
computed_qty = product_sudo.uom_id._compute_quantity(
|
||||
website._get_product_available_qty(product_sudo),
|
||||
to_unit=uom,
|
||||
round=False,
|
||||
)
|
||||
free_qty = float_round(computed_qty, precision_digits=0, rounding_method='DOWN')
|
||||
has_stock_notification = (
|
||||
product_sudo._has_stock_notification(self.env.user.partner_id)
|
||||
or (
|
||||
request
|
||||
and product_sudo.id in request.session.get(
|
||||
'product_with_stock_notification_enabled', set()
|
||||
)
|
||||
)
|
||||
)
|
||||
stock_notification_email = request and request.session.get('stock_notification_email', '')
|
||||
combination_info.update({
|
||||
cart_quantity = 0.0
|
||||
if not product_sudo.allow_out_of_stock_order:
|
||||
cart_quantity = product_sudo.uom_id._compute_quantity(
|
||||
request.cart._get_cart_qty(product_sudo.id),
|
||||
to_unit=uom,
|
||||
)
|
||||
res.update({
|
||||
'free_qty': free_qty,
|
||||
'product_type': product.type,
|
||||
'product_template': self.id,
|
||||
'available_threshold': self.available_threshold,
|
||||
'cart_qty': product._get_cart_qty(website),
|
||||
'uom_name': product.uom_id.name,
|
||||
'uom_rounding': product.uom_id.rounding,
|
||||
'allow_out_of_stock_order': self.allow_out_of_stock_order,
|
||||
'show_availability': self.show_availability,
|
||||
'out_of_stock_message': self.out_of_stock_message,
|
||||
'cart_qty': cart_quantity,
|
||||
'uom_name': uom.name,
|
||||
'uom_rounding': uom.rounding,
|
||||
'show_availability': product_sudo.show_availability,
|
||||
'out_of_stock_message': product_sudo.out_of_stock_message,
|
||||
'has_stock_notification': has_stock_notification,
|
||||
'stock_notification_email': stock_notification_email,
|
||||
})
|
||||
else:
|
||||
product_template = self.sudo()
|
||||
combination_info.update({
|
||||
res.update({
|
||||
'free_qty': 0,
|
||||
'product_type': product_template.type,
|
||||
'allow_out_of_stock_order': product_template.allow_out_of_stock_order,
|
||||
'available_threshold': product_template.available_threshold,
|
||||
'product_template': product_template.id,
|
||||
'cart_qty': 0,
|
||||
})
|
||||
|
||||
return combination_info
|
||||
return res
|
||||
|
||||
def _is_sold_out(self):
|
||||
return self.product_variant_id._is_sold_out()
|
||||
@api.model
|
||||
def _get_additional_configurator_data(
|
||||
self, product_or_template, date, currency, pricelist, *, uom=None, **kwargs
|
||||
):
|
||||
"""Override of `website_sale` to append stock data.
|
||||
|
||||
def _website_show_quick_add(self):
|
||||
return (self.allow_out_of_stock_order or not self._is_sold_out()) and super()._website_show_quick_add()
|
||||
:param product.product|product.template product_or_template: The product for which to get
|
||||
additional data.
|
||||
:param datetime date: The date to use to compute prices.
|
||||
:param res.currency currency: The currency to use to compute prices.
|
||||
:param product.pricelist pricelist: The pricelist to use to compute prices.
|
||||
:param uom.uom uom: The uom to use to compute prices.
|
||||
:param dict kwargs: Locally unused data passed to overrides.
|
||||
:rtype: dict
|
||||
:return: A dict containing additional data about the specified product.
|
||||
"""
|
||||
data = super()._get_additional_configurator_data(
|
||||
product_or_template, date, currency, pricelist, **kwargs
|
||||
)
|
||||
|
||||
if (website := ir_http.get_request_website()) and product_or_template.is_product_variant:
|
||||
max_quantity = product_or_template._get_max_quantity(website, request.cart, **kwargs)
|
||||
if max_quantity is not None:
|
||||
if uom:
|
||||
max_quantity = product_or_template.uom_id._compute_quantity(max_quantity, to_unit=uom)
|
||||
data['free_qty'] = max_quantity
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -1,43 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
allow_out_of_stock_order = fields.Boolean(
|
||||
string='Continue selling when out-of-stock',
|
||||
default=True)
|
||||
available_threshold = fields.Float(
|
||||
string='Show Threshold',
|
||||
default=5.0)
|
||||
show_availability = fields.Boolean(
|
||||
string='Show availability Qty',
|
||||
default=False)
|
||||
default_allow_out_of_stock_order = fields.Boolean(
|
||||
string="Continue selling when out-of-stock",
|
||||
default=True,
|
||||
default_model='product.template',
|
||||
)
|
||||
default_available_threshold = fields.Float(
|
||||
string="Show Threshold",
|
||||
default=5.0,
|
||||
default_model='product.template',
|
||||
)
|
||||
default_show_availability = fields.Boolean(
|
||||
string="Show availability Qty",
|
||||
default=False,
|
||||
default_model='product.template',
|
||||
)
|
||||
website_warehouse_id = fields.Many2one(
|
||||
'stock.warehouse',
|
||||
related='website_id.warehouse_id',
|
||||
domain="[('company_id', '=', website_company_id)]",
|
||||
readonly=False)
|
||||
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
IrDefault = self.env['ir.default'].sudo()
|
||||
|
||||
IrDefault.set('product.template', 'allow_out_of_stock_order', self.allow_out_of_stock_order)
|
||||
IrDefault.set('product.template', 'available_threshold', self.available_threshold)
|
||||
IrDefault.set('product.template', 'show_availability', self.show_availability)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
IrDefault = self.env['ir.default'].sudo()
|
||||
allow_out_of_stock_order = IrDefault.get('product.template', 'allow_out_of_stock_order')
|
||||
|
||||
res.update(
|
||||
allow_out_of_stock_order=allow_out_of_stock_order if allow_out_of_stock_order is not None else True,
|
||||
available_threshold=IrDefault.get('product.template', 'available_threshold') or 5.0,
|
||||
show_availability=IrDefault.get('product.template', 'show_availability') or False)
|
||||
return res
|
||||
readonly=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,34 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
from odoo import models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_round
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
def _get_warehouse_available(self):
|
||||
self.ensure_one()
|
||||
warehouse = self.website_id._get_warehouse_available()
|
||||
if not warehouse and self.user_id and self.company_id:
|
||||
warehouse = self.user_id.with_company(self.company_id.id)._get_default_warehouse_id()
|
||||
if not warehouse:
|
||||
warehouse = self.env.user._get_default_warehouse_id()
|
||||
return warehouse
|
||||
|
||||
def _compute_warehouse_id(self):
|
||||
website_orders = self.filtered('website_id')
|
||||
super(SaleOrder, self - website_orders)._compute_warehouse_id()
|
||||
for order in website_orders:
|
||||
order.warehouse_id = order._get_warehouse_available()
|
||||
if order.website_id.warehouse_id:
|
||||
order.warehouse_id = order.website_id.warehouse_id
|
||||
else:
|
||||
super(SaleOrder, order)._compute_warehouse_id()
|
||||
if not order.warehouse_id:
|
||||
order.warehouse_id = self.env.user._get_default_warehouse_id()
|
||||
|
||||
def _verify_updated_quantity(self, order_line, product_id, new_qty, **kwargs):
|
||||
def _verify_updated_quantity(self, order_line, product_id, new_qty, uom_id, **kwargs):
|
||||
self.ensure_one()
|
||||
product = self.env['product.product'].browse(product_id)
|
||||
if product.type == 'product' and not product.allow_out_of_stock_order:
|
||||
product_qty_in_cart, available_qty = self._get_cart_and_free_qty(
|
||||
line=order_line, product=product, **kwargs
|
||||
)
|
||||
if product.is_storable and not product.allow_out_of_stock_order:
|
||||
uom = self.env['uom.uom'].browse(uom_id)
|
||||
product_uom = product.uom_id
|
||||
|
||||
product_qty_in_cart, available_qty = self._get_cart_and_free_qty(product)
|
||||
|
||||
# Convert cart and available quantities to the requested uom
|
||||
product_qty_in_cart = product_uom._compute_quantity(product_qty_in_cart, uom)
|
||||
available_qty = product_uom._compute_quantity(available_qty, uom, round=False)
|
||||
available_qty = float_round(available_qty, precision_digits=0, rounding_method='DOWN')
|
||||
|
||||
old_qty = order_line.product_uom_qty if order_line else 0
|
||||
added_qty = new_qty - old_qty
|
||||
|
|
@ -36,81 +39,106 @@ class SaleOrder(models.Model):
|
|||
if available_qty < total_cart_qty:
|
||||
allowed_line_qty = available_qty - (product_qty_in_cart - old_qty)
|
||||
if allowed_line_qty > 0:
|
||||
def format_qty(qty):
|
||||
return int(qty) if float(qty).is_integer() else qty
|
||||
if order_line:
|
||||
order_line._set_shop_warning_stock(total_cart_qty, available_qty)
|
||||
warning = order_line._set_shop_warning_stock(
|
||||
format_qty(total_cart_qty),
|
||||
format_qty(available_qty),
|
||||
save=False,
|
||||
)
|
||||
else:
|
||||
self._set_shop_warning_stock(total_cart_qty, available_qty)
|
||||
else: # 0 or negative allowed_qty
|
||||
# if existing line: it will be deleted
|
||||
# if no existing line: no line will be created
|
||||
self.shop_warning = _(
|
||||
"Some products became unavailable and your cart has been updated. We're sorry for the inconvenience.")
|
||||
return allowed_line_qty, order_line.shop_warning or self.shop_warning
|
||||
return super()._verify_updated_quantity(order_line, product_id, new_qty, **kwargs)
|
||||
warning = self.env._(
|
||||
"You ask for %(desired_qty)s products but only %(available_qty)s is"
|
||||
" available.",
|
||||
desired_qty=format_qty(total_cart_qty),
|
||||
available_qty=format_qty(available_qty),
|
||||
)
|
||||
elif order_line:
|
||||
# Line will be deleted
|
||||
warning = self.env._(
|
||||
"Some products became unavailable and your cart has been updated. We're"
|
||||
" sorry for the inconvenience."
|
||||
)
|
||||
else:
|
||||
warning = self.env._(
|
||||
"%(product_name)s has not been added to your cart since it is not available.",
|
||||
product_name=product.name,
|
||||
)
|
||||
return allowed_line_qty, warning
|
||||
return super()._verify_updated_quantity(order_line, product_id, new_qty, uom_id, **kwargs)
|
||||
|
||||
def _get_cart_and_free_qty(self, line=None, product=None, **kwargs):
|
||||
""" Get cart quantity and free quantity for given product or line's product.
|
||||
def _get_cart_and_free_qty(self, product):
|
||||
"""Get cart quantity and free quantity for given product.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param SaleOrderLine line: The optional line
|
||||
:param ProductProduct product: The optional product
|
||||
:param product: `product.product` record.
|
||||
:returns: cart quantity and available quantity in the product uom
|
||||
:rtype: tuple
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not line and not product:
|
||||
return 0, 0
|
||||
cart_qty = sum(
|
||||
self._get_common_product_lines(line, product, **kwargs).mapped('product_uom_qty')
|
||||
)
|
||||
free_qty = (product or line.product_id).with_context(warehouse=self.warehouse_id.id).free_qty
|
||||
return cart_qty, free_qty
|
||||
product.ensure_one()
|
||||
|
||||
def _get_common_product_lines(self, line=None, product=None, **kwargs):
|
||||
""" Get the lines with the same product or line's product
|
||||
return self._get_cart_qty(product.id), self._get_free_qty(product)
|
||||
|
||||
:param SaleOrderLine line: The optional line
|
||||
:param ProductProduct product: The optional product
|
||||
def _get_free_qty(self, product):
|
||||
return product.with_context(warehouse_id=self._get_shop_warehouse_id()).free_qty
|
||||
|
||||
def _get_shop_warehouse_id(self):
|
||||
"""Return the warehouse to use for shop availability checks.
|
||||
|
||||
If no warehouse is specified on the website, all warehouses are considered,
|
||||
regardless of the warehouse automatically assigned to the order.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:returns: `stock.warehouse` id
|
||||
:rtype: int or False
|
||||
"""
|
||||
if not line and not product:
|
||||
return self.env['sale.order.line']
|
||||
product = product or line.product_id
|
||||
return self.order_line.filtered(lambda l: l.product_id == product)
|
||||
|
||||
def _set_shop_warning_stock(self, desired_qty, new_qty):
|
||||
self.ensure_one()
|
||||
self.shop_warning = _(
|
||||
'You ask for %(desired_qty)s products but only %(new_qty)s is available',
|
||||
desired_qty=desired_qty, new_qty=new_qty
|
||||
return self.website_id.warehouse_id.id
|
||||
|
||||
def _get_cart_qty(self, product_id):
|
||||
"""Return the quantity of the given product in the current cart, if any.
|
||||
|
||||
:param int product_id: `product.product` id
|
||||
:return: product quantity in the product uom
|
||||
:rtype: float
|
||||
"""
|
||||
if not self:
|
||||
return 0.0
|
||||
order_lines = self._get_common_product_lines(product_id)
|
||||
return sum(
|
||||
order_lines.mapped(
|
||||
lambda sol: sol.product_uom_id._compute_quantity(
|
||||
sol.product_uom_qty, sol.product_id.uom_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
return self.shop_warning
|
||||
|
||||
def _get_cache_key_for_line(self, line):
|
||||
return line.product_id
|
||||
def _get_common_product_lines(self, product_id=None):
|
||||
"""Get all the lines of the current order with the given product."""
|
||||
return self.order_line.filtered(lambda sol: sol.product_id.id == product_id)
|
||||
|
||||
def _get_context_for_line(self, line):
|
||||
return {
|
||||
'website_sale_stock_get_quantity': True,
|
||||
}
|
||||
def _check_cart_is_ready_to_be_paid(self):
|
||||
values = [
|
||||
line.shop_warning
|
||||
for line in self.order_line
|
||||
if not line._check_availability()
|
||||
]
|
||||
if values:
|
||||
raise ValidationError(' '.join(values))
|
||||
return super()._check_cart_is_ready_to_be_paid()
|
||||
|
||||
def _filter_can_send_abandoned_cart_mail(self):
|
||||
""" Filter sale orders on their product availability. """
|
||||
self = super()._filter_can_send_abandoned_cart_mail()
|
||||
combination_info_cache = {}
|
||||
"""Filter sale orders on their product availability."""
|
||||
return super()._filter_can_send_abandoned_cart_mail().filtered(
|
||||
lambda so: so._all_product_available()
|
||||
)
|
||||
|
||||
def _are_all_product_available_for_purchase(sale_order):
|
||||
for line in sale_order.order_line:
|
||||
product = line.product_id
|
||||
if product.type != 'product':
|
||||
continue
|
||||
cache_key = self._get_cache_key_for_line(line)
|
||||
combination_info = combination_info_cache.get(cache_key)
|
||||
if not combination_info:
|
||||
combination_info = product.with_context(**self._get_context_for_line(line), website_id=sale_order.website_id.id)._get_combination_info_variant(add_qty=line.product_uom_qty)
|
||||
combination_info_cache[cache_key] = combination_info
|
||||
if not product.allow_out_of_stock_order and combination_info['free_qty'] == 0:
|
||||
return False
|
||||
def _all_product_available(self):
|
||||
self.ensure_one()
|
||||
if not (lines := self.order_line):
|
||||
return True
|
||||
|
||||
# If none of the products in the checkout are available for purchase (empty inventory, for example),
|
||||
# then the email won't be sent.
|
||||
return self.filtered(_are_all_product_available_for_purchase)
|
||||
return not any(product._is_sold_out() for product in lines.product_id)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,46 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, models
|
||||
from odoo import models
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
def _set_shop_warning_stock(self, desired_qty, new_qty):
|
||||
def _set_shop_warning_stock(self, desired_qty, new_qty, save=True):
|
||||
self.ensure_one()
|
||||
self.shop_warning = _(
|
||||
'You ask for %(desired_qty)s %(product_name)s but only %(new_qty)s is available',
|
||||
warning = self.env._(
|
||||
"You ask for %(desired_qty)s %(product_name)s but only %(new_qty)s is available",
|
||||
desired_qty=desired_qty, product_name=self.product_id.name, new_qty=new_qty
|
||||
)
|
||||
return self.shop_warning
|
||||
if save:
|
||||
self.shop_warning = warning
|
||||
return warning
|
||||
|
||||
def _get_max_line_qty(self):
|
||||
max_quantity = self._get_max_available_qty()
|
||||
return self.product_uom_qty + max_quantity if (max_quantity is not None) else None
|
||||
|
||||
def _get_max_available_qty(self):
|
||||
return self.product_id.free_qty - self.product_id._get_cart_qty()
|
||||
""" The max quantity of a combo product is the max quantity of its selected combo item with
|
||||
the lowest max quantity. If none of the combo items has a max quantity, then the combo
|
||||
product also has no max quantity.
|
||||
"""
|
||||
self.ensure_one()
|
||||
cart_and_free_quantities = [
|
||||
line.order_id._get_cart_and_free_qty(line.product_id)
|
||||
for line in self._get_lines_with_price()
|
||||
if line.product_id.is_storable and not line.product_id.allow_out_of_stock_order
|
||||
]
|
||||
max_quantities = [
|
||||
free_qty - cart_qty for cart_qty, free_qty in cart_and_free_quantities
|
||||
]
|
||||
return min(max_quantities, default=None)
|
||||
|
||||
def _check_availability(self):
|
||||
self.ensure_one()
|
||||
if self.product_id.is_storable and not self.product_id.allow_out_of_stock_order:
|
||||
cart_qty, avl_qty = self.order_id._get_cart_and_free_qty(self.product_id)
|
||||
if cart_qty > avl_qty:
|
||||
self._set_shop_warning_stock(cart_qty, max(avl_qty, 0))
|
||||
return False
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Website(models.Model):
|
||||
|
|
@ -7,23 +8,16 @@ class Website(models.Model):
|
|||
|
||||
warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse')
|
||||
|
||||
def _prepare_sale_order_values(self, partner_sudo):
|
||||
values = super()._prepare_sale_order_values(partner_sudo)
|
||||
def _get_product_available_qty(self, product, **kwargs):
|
||||
"""Give the available quantity of a given product.
|
||||
|
||||
warehouse_id = self._get_warehouse_available()
|
||||
if warehouse_id:
|
||||
values['warehouse_id'] = warehouse_id
|
||||
return values
|
||||
NB: this method is only meant to be used on the shop before the checkout.
|
||||
For checkout steps, please use `cart._get_free_qty` instead to consider
|
||||
the chosen warehouse for delivery (website_sale_collect).
|
||||
|
||||
def _get_warehouse_available(self):
|
||||
return (
|
||||
self.warehouse_id.id or
|
||||
self.env['ir.default'].get('sale.order', 'warehouse_id', company_id=self.company_id.id) or
|
||||
self.env['ir.default'].get('sale.order', 'warehouse_id') or
|
||||
self.env['stock.warehouse'].sudo().search([('company_id', '=', self.company_id.id)], limit=1).id
|
||||
)
|
||||
|
||||
# FIXME VFE check if still needed
|
||||
def sale_get_order(self, *args, **kwargs):
|
||||
so = super().sale_get_order(*args, **kwargs)
|
||||
return so.with_context(warehouse=so.warehouse_id.id) if so else so
|
||||
:param product: product.product record
|
||||
:param dict kwargs: unused parameters, available for overrides
|
||||
:return: available quantity
|
||||
:rtype: float
|
||||
"""
|
||||
return product.with_context(warehouse_id=self.warehouse_id.id).free_qty
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue