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