Initial commit: Sale packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 14e3d26998
6469 changed files with 2479670 additions and 0 deletions

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import website
from . import product_product
from . import product_template
from . import res_config_settings
from . import sale_order
from . import sale_order_line
from . import stock_picking

View file

@ -0,0 +1,62 @@
# -*- 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
class ProductProduct(models.Model):
_inherit = 'product.product'
stock_notification_partner_ids = fields.Many2many('res.partner', relation='stock_notification_product_partner_rel', string='Back in stock Notifications')
def _has_stock_notification(self, partner):
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 _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
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()
def _send_availability_email(self):
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)
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",
body_html,
add_context=dict(message=msg, model_description=_("Product")),
)
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,
}
del context
mail = self_ctxt.env['mail.mail'].sudo().create(mail_values)
mail.send(raise_exception=False)
product.stock_notification_partner_ids -= partner

View file

@ -0,0 +1,65 @@
# -*- 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.http import request
class ProductTemplate(models.Model):
_inherit = 'product.template'
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)
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)
if not self.env.context.get('website_sale_stock_get_quantity'):
return combination_info
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())
stock_notification_email = request and request.session.get('stock_notification_email', '')
combination_info.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,
'has_stock_notification': has_stock_notification,
'stock_notification_email': stock_notification_email,
})
else:
product_template = self.sudo()
combination_info.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
def _is_sold_out(self):
return self.product_variant_id._is_sold_out()
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()

View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
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)
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

View file

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
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()
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:
product_qty_in_cart, available_qty = self._get_cart_and_free_qty(
line=order_line, product=product, **kwargs
)
old_qty = order_line.product_uom_qty if order_line else 0
added_qty = new_qty - old_qty
total_cart_qty = product_qty_in_cart + added_qty
if available_qty < total_cart_qty:
allowed_line_qty = available_qty - (product_qty_in_cart - old_qty)
if allowed_line_qty > 0:
if order_line:
order_line._set_shop_warning_stock(total_cart_qty, available_qty)
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)
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.
Note: self.ensure_one()
:param SaleOrderLine line: The optional line
:param ProductProduct product: The optional product
"""
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
def _get_common_product_lines(self, line=None, product=None, **kwargs):
""" Get the lines with the same product or line's product
:param SaleOrderLine line: The optional line
:param ProductProduct product: The optional product
"""
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.shop_warning
def _get_cache_key_for_line(self, line):
return line.product_id
def _get_context_for_line(self, line):
return {
'website_sale_stock_get_quantity': True,
}
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 = {}
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
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)

View file

@ -0,0 +1,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, models
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
def _set_shop_warning_stock(self, desired_qty, new_qty):
self.ensure_one()
self.shop_warning = _(
'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
def _get_max_available_qty(self):
return self.product_id.free_qty - self.product_id._get_cart_qty()

View file

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class StockPicking(models.Model):
_inherit = 'stock.picking'
website_id = fields.Many2one('website', related='sale_id.website_id', string='Website',
help='Website where this order has been placed, for eCommerce orders.',
store=True, readonly=True)

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class Website(models.Model):
_inherit = 'website'
warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse')
def _prepare_sale_order_values(self, partner_sudo):
values = super()._prepare_sale_order_values(partner_sudo)
warehouse_id = self._get_warehouse_available()
if warehouse_id:
values['warehouse_id'] = warehouse_id
return values
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