mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-28 01:32:01 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue