Skip to content

Commit

Permalink
[IMP] website_sale*: add to whislist and notify stock
Browse files Browse the repository at this point in the history
This module allows users to be notified by email when a product out of
stock comes back in stock. For that they can add it to their wishlist
and select the appropriate option.

We show out-of-stock warning in all case whatever the show availibility option.
You cannot custom you 'in stock' message ot free message.

Spec of Pde and Fp

Part of #68221
task-2458165

closes #68221

Related: odoo/upgrade#2405
Signed-off-by: Jérémy Kersten (jke) <[email protected]>
Co-authored-by: Kersten Jeremy <[email protected]>
Co-authored-by: Younn Olivier <[email protected]>
  • Loading branch information
JKE-be and younn-o committed Aug 9, 2021
1 parent 91ba4a3 commit 88cb6db
Show file tree
Hide file tree
Showing 35 changed files with 537 additions and 116 deletions.
2 changes: 1 addition & 1 deletion addons/website_sale/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def process(self, products, ppg=20, ppr=4):
self.table[(pos // ppr) + y2][(pos % ppr) + x2] = False
self.table[pos // ppr][pos % ppr] = {
'product': p, 'x': x, 'y': y,
'ribbon': p.website_ribbon_id,
'ribbon': p._get_website_ribbon(),
}
if index <= ppg:
maxy = max(maxy, y + (pos // ppr))
Expand Down
5 changes: 5 additions & 0 deletions addons/website_sale/data/data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
<field name="html_class">bg-danger o_ribbon_left</field>
</record>

<record id="website_sale.out_of_stock_ribbon" model="product.ribbon">
<field name="html">Out of stock</field>
<field name="html_class">bg-warning o_ribbon_left</field>
</record>

<record id="website_sale.new_ribbon" model="product.ribbon">
<field name="html">New!</field>
<field name="html_class">bg-primary o_ribbon_left</field>
Expand Down
10 changes: 10 additions & 0 deletions addons/website_sale/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@ def _compute_website_url(self):
if product.id:
product.website_url = "/shop/%s" % slug(product)

def _is_sold_out(self):
return self.product_variant_id._is_sold_out()

def _get_website_ribbon(self):
return self.website_ribbon_id

# ---------------------------------------------------------
# Rating Mixin API
# ---------------------------------------------------------
Expand Down Expand Up @@ -485,3 +491,7 @@ def _get_images(self):
# [1:] to remove the main image from the template, we only display
# the template extra images here
return variant_images + self.product_tmpl_id._get_images()[1:]

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
5 changes: 0 additions & 5 deletions addons/website_sale/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from ast import literal_eval

from odoo import api, models, fields


Expand All @@ -24,10 +22,7 @@ class ResConfigSettings(models.TransientModel):
module_website_sale_digital = fields.Boolean("Digital Content")
module_website_sale_wishlist = fields.Boolean("Wishlists")
module_website_sale_comparison = fields.Boolean("Product Comparison Tool")
module_website_sale_stock = fields.Boolean("Inventory", help='Installs the "Website Delivery Information" application')
module_website_sale_gift_card = fields.Boolean("Gift Card")


module_account = fields.Boolean("Invoicing")

cart_recovery_mail_template = fields.Many2one('mail.template', string='Cart Recovery Email', domain="[('model', '=', 'sale.order')]",
Expand Down
11 changes: 0 additions & 11 deletions addons/website_sale/views/res_config_settings_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,6 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="product_availability_setting">
<div class="o_setting_left_pane">
<field name="module_website_sale_stock"/>
</div>
<div class="o_setting_right_pane" name="stock_inventory_availability">
<label for="module_website_sale_stock"/>
<div class="text-muted">
Manage availability of products
</div>
</div>
</div>
</div>

<h2>Pricing</h2>
Expand Down
1 change: 1 addition & 0 deletions addons/website_sale_comparison_wishlist/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@
],
},
'auto_install': True,
'license': 'LGPL-3',
}
4 changes: 2 additions & 2 deletions addons/website_sale_stock/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from odoo.addons.website_sale.controllers import main as website_sale_controller

from odoo import http,_
from odoo import http, _
from odoo.http import request
from odoo.exceptions import ValidationError

Expand All @@ -18,7 +18,7 @@ def shop_payment_transaction(self, *args, **kwargs):
order = request.website.sale_get_order()
values = []
for line in order.order_line:
if line.product_id.type == 'product' and line.product_id.inventory_availability in ['always', 'threshold']:
if line.product_id.type == 'product' and not line.product_id.allow_out_of_stock_order:
cart_qty = sum(order.order_line.filtered(lambda p: p.product_id.id == line.product_id.id).mapped('product_uom_qty'))
avl_qty = line.product_id.with_context(warehouse=order.warehouse_id.id).free_qty
if cart_qty > avl_qty:
Expand Down
36 changes: 17 additions & 19 deletions addons/website_sale_stock/models/product_template.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
# -*- 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
from odoo.tools.translate import html_translate


class ProductTemplate(models.Model):
_inherit = 'product.template'

inventory_availability = fields.Selection([
('never', 'Sell regardless of inventory'),
('always', 'Show inventory on website and prevent sales if not enough stock'),
('threshold', 'Show inventory below a threshold and prevent sales if not enough stock'),
('custom', 'Show product-specific notifications'),
], string='Inventory Availability', help='Adds an inventory availability status on the web product page.', default='never')
available_threshold = fields.Float(string='Availability Threshold', default=5.0)
custom_message = fields.Text(string='Custom Message', default='', translate=True)
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(
Expand All @@ -27,28 +24,29 @@ def _get_combination_info(self, combination=False, product_id=False, add_qty=1,
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.warehouse_id.id).free_qty
product_with_context = product.with_context(warehouse=website.warehouse_id.id)

free_qty = product_with_context.free_qty
combination_info.update({
'free_qty': free_qty,
'free_qty_formatted': self.env['ir.qweb.field.float'].value_to_html(free_qty, {'precision': 0}),
'product_type': product.type,
'inventory_availability': product.inventory_availability,
'available_threshold': product.available_threshold,
'custom_message': product.custom_message,
'product_template': product.product_tmpl_id.id,
'product_template': self.id,
'available_threshold': self.available_threshold,
'cart_qty': product.cart_qty,
'uom_name': product.uom_id.name,
'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,
})
else:
product_template = self.sudo()
combination_info.update({
'free_qty': 0,
'product_type': product_template.type,
'inventory_availability': product_template.inventory_availability,
'allow_out_of_stock_order': product_template.allow_out_of_stock_order,
'available_threshold': product_template.available_threshold,
'custom_message': product_template.custom_message,
'product_template': product_template.id,
'cart_qty': 0
'cart_qty': 0,
})

return combination_info
26 changes: 15 additions & 11 deletions addons/website_sale_stock/models/res_config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,31 @@
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'

inventory_availability = fields.Selection([
('never', 'Sell regardless of inventory'),
('always', 'Show inventory on website and prevent sales if not enough stock'),
('threshold', 'Show inventory when below the threshold and prevent sales if not enough stock'),
('custom', 'Show product-specific notifications'),
], string='Inventory Availability', default='never')
available_threshold = fields.Float(string='Availability Threshold')
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', 'inventory_availability', self.inventory_availability)
IrDefault.set('product.template', 'available_threshold', self.available_threshold if self.inventory_availability == 'threshold' else None)

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()
res.update(inventory_availability=IrDefault.get('product.template', 'inventory_availability') or 'never',
available_threshold=IrDefault.get('product.template', 'available_threshold') or 5.0)

res.update(
allow_out_of_stock_order=IrDefault.get('product.template', 'allow_out_of_stock_order') or True,
available_threshold=IrDefault.get('product.template', 'available_threshold') or 5.0,
show_availability=IrDefault.get('product.template', 'show_availability') or False
)

return res

@api.onchange('website_company_id')
Expand Down
2 changes: 1 addition & 1 deletion addons/website_sale_stock/models/sale_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kw
line_id = values.get('line_id')

for line in self.order_line:
if line.product_id.type == 'product' and line.product_id.inventory_availability in ['always', 'threshold']:
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'))
# The quantity should be computed based on the warehouse of the website, not the
# warehouse of the SO.
Expand Down
43 changes: 29 additions & 14 deletions addons/website_sale_stock/static/src/js/variant_mixin.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
odoo.define('website_sale_stock.VariantMixin', function (require) {
'use strict';

const {Markup} = require('web.utils');
var VariantMixin = require('sale.VariantMixin');
var publicWidget = require('web.public.widget');
var ajax = require('web.ajax');
var core = require('web.core');
var QWeb = core.qweb;
var xml_load = ajax.loadXML(
'/website_sale_stock/static/src/xml/website_sale_stock_product_availability.xml',
QWeb
);

const loadXml = async () => {
return ajax.loadXML('/website_sale_stock/static/src/xml/website_sale_stock_product_availability.xml', QWeb);
};

require('website_sale.website_sale');

/**
Expand All @@ -28,32 +30,33 @@ require('website_sale.website_sale');
* @param {Array} combination
*/
VariantMixin._onChangeCombinationStock = function (ev, $parent, combination) {
var product_id = 0;
let product_id = 0;
// needed for list view of variants
if ($parent.find('input.product_id:checked').length) {
product_id = $parent.find('input.product_id:checked').val();
} else {
product_id = $parent.find('.product_id').val();
}
var isMainProduct = combination.product_id &&
const isMainProduct = combination.product_id &&
($parent.is('.js_main_product') || $parent.is('.main_product')) &&
combination.product_id === parseInt(product_id);

if (!this.isWebsite || !isMainProduct){
if (!this.isWebsite || !isMainProduct) {
return;
}

var qty = $parent.find('input[name="add_qty"]').val();
const $addQtyInput = $parent.find('input[name="add_qty"]');
let qty = $addQtyInput.val();

$parent.find('#add_to_cart').removeClass('out_of_stock');
$parent.find('.o_we_buy_now').removeClass('out_of_stock');
if (combination.product_type === 'product' && _.contains(['always', 'threshold'], combination.inventory_availability)) {
if (combination.product_type === 'product' && !combination.allow_out_of_stock_order) {
combination.free_qty -= parseInt(combination.cart_qty);
$addQtyInput.data('max', combination.free_qty || 1);
if (combination.free_qty < 0) {
combination.free_qty = 0;
}
if (qty > combination.free_qty) {
var $addQtyInput = $parent.find('input[name="add_qty"]');
qty = combination.free_qty || 1;
$addQtyInput.val(qty);
}
Expand All @@ -63,12 +66,13 @@ VariantMixin._onChangeCombinationStock = function (ev, $parent, combination) {
}
}

xml_load.then(function () {
loadXml().then(function (result) {
$('.oe_website_sale')
.find('.availability_message_' + combination.product_template)
.remove();

var $message = $(QWeb.render(
combination.has_out_of_stock_message = $(combination.out_of_stock_message).text() !== '';
combination.out_of_stock_message = Markup(combination.out_of_stock_message);
const $message = $(QWeb.render(
'website_sale_stock.product_availability',
combination
));
Expand All @@ -81,9 +85,20 @@ publicWidget.registry.WebsiteSale.include({
* Adds the stock checking to the regular _onChangeCombination method
* @override
*/
_onChangeCombination: function (){
_onChangeCombination: function () {
this._super.apply(this, arguments);
VariantMixin._onChangeCombinationStock.apply(this, arguments);
},
/**
* Recomputes the combination after adding a product to the cart
* @override
*/
_onClickAdd(ev) {
return this._super.apply(this, arguments).then(() => {
if ($('div.availability_messages').length) {
this._getCombinationInfo(ev);
}
});
}
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>

<templates>

<t t-name="website_sale_stock.product_availability">
<t t-if="product_type == 'product' and _.contains(['always', 'threshold'], inventory_availability)">
<t t-if="free_qty gt 0">
<div t-if="inventory_availability == 'always'" t-attf-class="availability_message_#{product_template} text-success mt16">
<t t-esc="free_qty_formatted" /> <t t-esc="uom_name" /> available
</div>
<t t-if="inventory_availability == 'threshold'">
<div t-if="free_qty lte available_threshold" t-attf-class="availability_message_#{product_template} text-warning mt16">
<i class="fa fa-exclamation-triangle" title="Warning" role="img" aria-label="Warning"/>
<t t-esc="free_qty_formatted" /> <t t-esc="uom_name" /> available
</div>
<div t-if="free_qty gt available_threshold" t-attf-class="availability_message_#{product_template} text-success mt16">In stock</div>
<t t-if="product_type == 'product'">
<!-- show out_of_stock_message whatever the show_availability - pde's spec-->
<div id="out_of_stock_message" t-if="free_qty lte 0 and !cart_qty" t-attf-class="availability_message_#{product_template}">
<t t-if='has_out_of_stock_message' t-out='out_of_stock_message'/>
<t t-elif="!allow_out_of_stock_order">Out of Stock</t>
</div>
<div id="threshold_message" t-elif="show_availibility and free_qty lte available_threshold" t-attf-class="availability_message_#{product_template}">
Only <t t-esc='free_qty'/> <t t-esc="uom_name" /> left in stock.
</div>

<div t-if="!allow_out_of_stock_order and show_availability and cart_qty" t-attf-class="availability_message_#{product_template} text-warning mt8">
<t t-if='!free_qty'>
You already added all the available product in your cart.
</t>
<t t-else=''>
You already added <t t-esc="cart_qty" /> <t t-esc="uom_name" /> in your cart.
</t>
</t>
<div t-if="cart_qty" t-attf-class="availability_message_#{product_template} text-warning mt8">
You already added <t t-if="!free_qty">all</t> <t t-esc="cart_qty" /> <t t-esc="uom_name" /> in your cart.
</div>
<div t-if="!cart_qty and free_qty lte 0" t-attf-class="availability_message_#{product_template} text-danger mt16"><i class="fa fa-exclamation-triangle" role="img" aria-label="Warning" title="Warning"/> Temporarily out of stock</div>
</t>
<div t-if="product_type == 'product' and inventory_availability == 'custom'" t-attf-class="availability_message_#{product_template} text-success mt16">
<t t-esc="custom_message" />
</div>
</t>

</templates>
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ def test_get_combination_info(self):
# Create two stockable products
product_1 = self.env['product.product'].create({
'name': 'Product A',
'inventory_availability': 'always',
'allow_out_of_stock_order': False,
'type': 'product',
'default_code': 'E-COM1',
})

product_2 = self.env['product.product'].create({
'name': 'Product B',
'inventory_availability': 'always',
'allow_out_of_stock_order': False,
'type': 'product',
'default_code': 'E-COM2',
})
Expand Down
Loading

0 comments on commit 88cb6db

Please sign in to comment.