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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -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

View file

@ -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.')",
},
]
);

View file

@ -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;
});

View file

@ -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;

View file

@ -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);
},
});

View file

@ -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>

View file

@ -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.
}]);

View file

@ -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',
},
],
);