mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-28 00:52:07 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#B06161"/><stop offset="45.785%" stop-color="#984E4E"/><stop offset="100%" stop-color="#7C3838"/></linearGradient><path id="d" d="M24.592 27.281l3.283 14.7L31 41.796V39h19v2.042h1.908c1.441 0 1.441 1.834.088 1.998L28.36 44l.942 3h21.801c.942 0 .942 2 0 2H27.417l-5.65-24h-1.884v1c0 .667-.313 1-.941 1-.628 0-.942-.333-.942-1v-2c.062-.667.376-1 .942-1h3.767c.486 0 .8.333.941 1l.942 3.281zM49.423 55a2.497 2.497 0 0 1-2.493-2.5c0-1.38 1.116-2.5 2.493-2.5a2.497 2.497 0 0 1 2.494 2.5c0 1.38-1.117 2.5-2.494 2.5zm-19.95 0a2.497 2.497 0 0 1-2.494-2.5c0-1.38 1.116-2.5 2.494-2.5a2.497 2.497 0 0 1 2.493 2.5c0 1.38-1.116 2.5-2.493 2.5zM31 35h19v3H31v-3zm0-5h19v4H31v-4zm0-5h19v4H31v-4zm1 1v2h17v-2H32z"/><path id="e" d="M24.592 25.281l3.283 14.7L31 39.796V37h19v2.042h1.908c1.441 0 1.441 1.834.088 1.998L28.36 42l.942 3h21.801c.942 0 .942 2 0 2H27.417l-5.65-24h-1.884v1c0 .667-.313 1-.941 1-.628 0-.942-.333-.942-1v-2c.062-.667.376-1 .942-1h3.767c.486 0 .8.333.941 1l.942 3.281zM49.423 53a2.497 2.497 0 0 1-2.493-2.5c0-1.38 1.116-2.5 2.493-2.5a2.497 2.497 0 0 1 2.494 2.5c0 1.38-1.117 2.5-2.494 2.5zm-19.95 0a2.497 2.497 0 0 1-2.494-2.5c0-1.38 1.116-2.5 2.494-2.5a2.497 2.497 0 0 1 2.493 2.5c0 1.38-1.116 2.5-2.493 2.5zM31 33h19v3H31v-3zm0-5h19v4H31v-4zm0-5h19v4H31v-4zm1 1v2h17v-2H32z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M35.869 69H4c-2 0-4-1-4-4V38.32l18.528-17.185h4.459l2.385 7.467L31 23h19v16l2.5 1.5-9.773 4.8H51V47l-.637 1.385 1.485 2.626-.388.927L35.869 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#d"/><use fill="#FFF" fill-rule="nonzero" xlink:href="#e"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,26 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
import wTourUtils from 'website.tour_utils';
|
||||
|
||||
tour.register('website_sale_stock_reorder_from_portal', {
|
||||
test: true,
|
||||
url: '/my/orders',
|
||||
},
|
||||
[
|
||||
{
|
||||
content: 'Select first order',
|
||||
trigger: '.o_portal_my_doc_table a:first',
|
||||
},
|
||||
wTourUtils.clickOnElement('Reorder Again', '.o_wsale_reorder_button'),
|
||||
{
|
||||
content: "Check that there is one out of stock product",
|
||||
trigger: "#o_wsale_reorder_body div.text-warning span:contains('This product is out of stock.')",
|
||||
},
|
||||
{
|
||||
content: "Check that there is one product that does not have enough stock",
|
||||
trigger: "#o_wsale_reorder_body div.text-warning:contains('You ask for 2.0 Units but only 1.0 are available.')",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
odoo.define('website_sale_stock.VariantMixin', function (require) {
|
||||
'use strict';
|
||||
|
||||
const {Markup} = require('web.utils');
|
||||
const field_utils = require('web.field_utils');
|
||||
var VariantMixin = require('sale.VariantMixin');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var core = require('web.core');
|
||||
var QWeb = core.qweb;
|
||||
|
||||
require('website_sale.website_sale');
|
||||
|
||||
/**
|
||||
* Addition to the variant_mixin._onChangeCombination
|
||||
*
|
||||
* This will prevent the user from selecting a quantity that is not available in the
|
||||
* stock for that product.
|
||||
*
|
||||
* It will also display various info/warning messages regarding the select product's stock.
|
||||
*
|
||||
* This behavior is only applied for the web shop (and not on the SO form)
|
||||
* and only for the main product.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
* @param {$.Element} $parent
|
||||
* @param {Array} combination
|
||||
*/
|
||||
VariantMixin._onChangeCombinationStock = function (ev, $parent, combination) {
|
||||
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();
|
||||
}
|
||||
const isMainProduct = combination.product_id &&
|
||||
($parent.is('.js_main_product') || $parent.is('.main_product')) &&
|
||||
combination.product_id === parseInt(product_id);
|
||||
|
||||
if (!this.isWebsite || !isMainProduct) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $addQtyInput = $parent.find('input[name="add_qty"]');
|
||||
let qty = $addQtyInput.val();
|
||||
let ctaWrapper = $parent[0].querySelector('#o_wsale_cta_wrapper');
|
||||
ctaWrapper.classList.replace('d-none', 'd-flex');
|
||||
ctaWrapper.classList.remove('out_of_stock');
|
||||
|
||||
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) {
|
||||
qty = combination.free_qty || 1;
|
||||
$addQtyInput.val(qty);
|
||||
}
|
||||
if (combination.free_qty < 1) {
|
||||
ctaWrapper.classList.replace('d-flex', 'd-none');
|
||||
ctaWrapper.classList.add('out_of_stock');
|
||||
}
|
||||
}
|
||||
|
||||
// needed xml-side for formatting of remaining qty
|
||||
combination.formatQuantity = (qty) => {
|
||||
if (Number.isInteger(qty)) {
|
||||
return qty;
|
||||
} else {
|
||||
const decimals = Math.max(
|
||||
0,
|
||||
Math.ceil(-Math.log10(combination.uom_rounding))
|
||||
);
|
||||
return field_utils.format.float(qty, {digits: [false, decimals]});
|
||||
}
|
||||
}
|
||||
|
||||
$('.oe_website_sale')
|
||||
.find('.availability_message_' + combination.product_template)
|
||||
.remove();
|
||||
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
|
||||
));
|
||||
$('div.availability_messages').html($message);
|
||||
};
|
||||
|
||||
publicWidget.registry.WebsiteSale.include({
|
||||
/**
|
||||
* Adds the stock checking to the regular _onChangeCombination method
|
||||
* @override
|
||||
*/
|
||||
_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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return VariantMixin;
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/** @odoo-module alias=website_sale_stock.website_sale**/
|
||||
|
||||
import { WebsiteSale } from 'website_sale.website_sale';
|
||||
import { is_email } from 'web.utils';
|
||||
|
||||
WebsiteSale.include({
|
||||
events: Object.assign({}, WebsiteSale.prototype.events, {
|
||||
'click #product_stock_notification_message': '_onClickProductStockNotificationMessage',
|
||||
'click #product_stock_notification_form_submit_button': '_onClickSubmitProductStockNotificationForm',
|
||||
}),
|
||||
|
||||
_onClickProductStockNotificationMessage: function (ev) {
|
||||
const partnerEmail = document.querySelector('#wsale_user_email').value;
|
||||
const emailInputEl = document.querySelector('#stock_notification_input');
|
||||
|
||||
emailInputEl.value = partnerEmail;
|
||||
this._handleClickStockNotificationMessage(ev);
|
||||
},
|
||||
|
||||
_onClickSubmitProductStockNotificationForm: function (ev) {
|
||||
const formEl = ev.currentTarget.closest('#stock_notification_form');
|
||||
const productId = parseInt(formEl.querySelector('input[name="product_id"]').value);
|
||||
this._handleClickSubmitStockNotificationForm(ev, productId);
|
||||
},
|
||||
|
||||
|
||||
_handleClickStockNotificationMessage(ev) {
|
||||
ev.currentTarget.classList.add('d-none');
|
||||
ev.currentTarget.parentElement.querySelector('#stock_notification_form').classList.remove('d-none');
|
||||
},
|
||||
|
||||
_handleClickSubmitStockNotificationForm(ev, productId) {
|
||||
const stockNotificationEl = ev.currentTarget.closest('#stock_notification_div');
|
||||
const formEl = stockNotificationEl.querySelector('#stock_notification_form');
|
||||
const email = stockNotificationEl.querySelector('#stock_notification_input').value.trim();
|
||||
|
||||
if (!is_email(email)) {
|
||||
return this._displayEmailIncorrectMessage(stockNotificationEl);
|
||||
}
|
||||
|
||||
this._rpc({
|
||||
route: "/shop/add/stock_notification",
|
||||
params: {
|
||||
product_id: productId,
|
||||
email,
|
||||
},
|
||||
}).then((data) => {
|
||||
const message = stockNotificationEl.querySelector('#stock_notification_success_message');
|
||||
|
||||
message.classList.remove('d-none');
|
||||
formEl.classList.add('d-none');
|
||||
}).catch((error) => {
|
||||
this._displayEmailIncorrectMessage(stockNotificationEl);
|
||||
});
|
||||
},
|
||||
|
||||
_displayEmailIncorrectMessage(stockNotificationEl) {
|
||||
const incorrectIconEl = stockNotificationEl.querySelector('#stock_notification_input_incorrect');
|
||||
incorrectIconEl.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
export default WebsiteSale;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ReorderDialog } from "@website_sale/js/website_sale_reorder";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
|
||||
patch(ReorderDialog.prototype, "website_sale_stock_reorder", {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async onWillStartHandler() {
|
||||
const res = await this._super(...arguments);
|
||||
for (const product of this.content.products) {
|
||||
this.stockCheckCombinationInfo(product);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async loadProductCombinationInfo(product) {
|
||||
await this._super(...arguments);
|
||||
},
|
||||
|
||||
stockCheckCombinationInfo(product) {
|
||||
// Products that should have a max quantity available should be limited by default.
|
||||
if (product.combinationInfo.allow_out_of_stock_order || product.type !== "product") {
|
||||
return;
|
||||
}
|
||||
product.max_quantity_available = product.combinationInfo.free_qty;
|
||||
if (!product.max_quantity_available) {
|
||||
product.add_to_cart_allowed = false;
|
||||
}
|
||||
if (product.max_quantity_available < product.qty) {
|
||||
product.qty_warning = sprintf(
|
||||
this.env._t("You ask for %s Units but only %s are available."),
|
||||
product.qty.toFixed(1),
|
||||
product.max_quantity_available.toFixed(1)
|
||||
);
|
||||
product.qty = product.max_quantity_available;
|
||||
product.stock_warning = true;
|
||||
} else if (product.combinationInfo.cart_qty) {
|
||||
product.qty_warning = sprintf(
|
||||
this.env._t("You already have %s Units in your cart."),
|
||||
product.combinationInfo.cart_qty.toFixed(1)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getWarningForProduct(product) {
|
||||
if (product.hasOwnProperty("max_quantity_available") && !product.max_quantity_available) {
|
||||
return this.env._t("This product is out of stock.");
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
changeProductQty(product, newQty) {
|
||||
if (product.max_quantity_available && newQty > product.max_quantity_available) {
|
||||
product.qty_warning = sprintf(
|
||||
this.env._t("You ask for %s Units but only %s are available."),
|
||||
newQty.toFixed(1),
|
||||
product.max_quantity_available.toFixed(1)
|
||||
);
|
||||
product.stock_warning = true;
|
||||
newQty = product.max_quantity_available;
|
||||
} else if (product.stock_warning) {
|
||||
product.qty_warning = false;
|
||||
product.stock_warning = false;
|
||||
}
|
||||
this._super(product, newQty);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
<t t-name="website_sale_stock.product_availability">
|
||||
<t t-if="product_type == 'product' and !prevent_zero_price_sale">
|
||||
<div t-if="free_qty lte 0 and !cart_qty" t-attf-class="availability_message_#{product_template} mb-1">
|
||||
<div id="out_of_stock_message">
|
||||
<t t-if='has_out_of_stock_message' t-out='out_of_stock_message'/>
|
||||
<t t-elif="!allow_out_of_stock_order">
|
||||
<div class="text-danger fw-bold">
|
||||
<i class="fa fa-times me-1"/>
|
||||
Out of Stock
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div id="stock_notification_div" t-if="!allow_out_of_stock_order">
|
||||
<div class="btn btn-link px-0" t-if="!has_stock_notification"
|
||||
id="product_stock_notification_message">
|
||||
<i class="fa fa-envelope-o me-1"/>
|
||||
Get notified when back in stock
|
||||
</div>
|
||||
<div id="stock_notification_form" class="d-none">
|
||||
<div class="input-group">
|
||||
<input class="form-control"
|
||||
id="stock_notification_input" name="email"
|
||||
type="text" placeholder="youremail@gmail.com" t-value="stock_notification_email if stock_notification_email else None"/>
|
||||
<input name="product_id" type="hidden" t-att-value="product_id"/>
|
||||
|
||||
<div id="product_stock_notification_form_submit_button" class="btn btn-secondary">
|
||||
<i class="fa fa-paper-plane"/>
|
||||
</div>
|
||||
<div id="stock_notification_input_incorrect" class="btn d-none">
|
||||
<i class="fa fa-times text-danger"/>
|
||||
Invalid email
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="stock_notification_success_message"
|
||||
t-att-class="has_stock_notification ? '' : 'd-none'">
|
||||
<div class="text-muted">
|
||||
<i class="fa fa-bell"/>
|
||||
We'll notify you once the product is back in stock.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="threshold_message" t-elif="show_availability and free_qty lte available_threshold" t-attf-class="availability_message_#{product_template} text-warning fw-bold">
|
||||
Only <t t-esc="formatQuantity(free_qty)"/> <t t-esc="uom_name" /> left in stock.
|
||||
</div>
|
||||
|
||||
<div id="already_in_cart_message" 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>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('website_sale_stock_multilang', {
|
||||
test: true,
|
||||
url: '/fr/shop?search=unavailable',
|
||||
},
|
||||
[{
|
||||
content: "Open unavailable product page",
|
||||
trigger: 'a[content="unavailable_product"]',
|
||||
}, {
|
||||
content: "Check out of stock message",
|
||||
trigger: '#out_of_stock_message:contains("Hors-stock")',
|
||||
run: () => {}, // This is a check.
|
||||
}, {
|
||||
content: "Check price",
|
||||
trigger: 'span:contains("123,45")',
|
||||
run: () => {}, // This is a check.
|
||||
}, {
|
||||
content: "Open language selector",
|
||||
trigger: '.js_language_selector button',
|
||||
}, {
|
||||
content: "Switch to English",
|
||||
trigger: '.js_change_lang[data-url_code="en"]',
|
||||
}, {
|
||||
content: "Check out of stock message",
|
||||
trigger: '#out_of_stock_message:contains("Out of stock")',
|
||||
run: () => {}, // This is a check.
|
||||
}, {
|
||||
content: "Check price",
|
||||
trigger: 'span:contains("123.45")',
|
||||
run: () => {}, // This is a check.
|
||||
}, {
|
||||
content: "Open language selector",
|
||||
trigger: '.js_language_selector button',
|
||||
}, {
|
||||
content: "Switch to French",
|
||||
trigger: '.js_change_lang[data-url_code="fr"]',
|
||||
}, {
|
||||
content: "Check out of stock message",
|
||||
trigger: '#out_of_stock_message:contains("Hors-stock")',
|
||||
run: () => {}, // This is a check.
|
||||
}, {
|
||||
content: "Check price",
|
||||
trigger: 'span:contains("123,45")',
|
||||
run: () => {}, // This is a check.
|
||||
}]);
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('back_in_stock_notification_product', {
|
||||
test: true,
|
||||
url: '/shop?search=Macbook%20Pro',
|
||||
},
|
||||
[
|
||||
{
|
||||
content: "Open product page",
|
||||
trigger: 'a:contains("Macbook Pro")',
|
||||
},
|
||||
{
|
||||
content: "Click on 'Be notified when back in stock'",
|
||||
trigger: '#product_stock_notification_message',
|
||||
},
|
||||
{
|
||||
content: "Fill email form",
|
||||
trigger: 'div[id="stock_notification_form"] input[name="email"]',
|
||||
run: 'text test@test.test',
|
||||
},
|
||||
{
|
||||
content: "Click on the button",
|
||||
trigger: '#product_stock_notification_form_submit_button',
|
||||
},
|
||||
{
|
||||
content: "Success Message",
|
||||
trigger: '#stock_notification_success_message',
|
||||
},
|
||||
],
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue