Add allow_zero_price_sales toggle: config UI, POS enforcement and backend validation
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

This commit is contained in:
2026-07-15 17:50:02 +00:00
parent 2734a484f6
commit 116d4452da
7 changed files with 182 additions and 4 deletions

View File

@@ -32,6 +32,7 @@ const POS = (() => {
let canCreateLayaway = false;
let canCreateRemission = false;
let counterRemissionEnabled = false;
let allowZeroPriceSales = true;
let currentPerms = [];
let receiptConfig = {};
let couriers = [];
@@ -142,6 +143,13 @@ const POS = (() => {
} catch (e) {
counterRemissionEnabled = false;
}
// Sales settings (zero-price sales toggle)
try {
const ssCfg = await api('/pos/api/config/sales-settings');
allowZeroPriceSales = ssCfg.allow_zero_price_sales !== false;
} catch (e) {
allowZeroPriceSales = true;
}
// Counter remission workflow applies only to the counter role.
canCreateRemission = counterRemissionEnabled && employeeRole === 'counter' && perms.includes('pos.remission');
@@ -442,6 +450,18 @@ const POS = (() => {
}
}
function isZeroPriceItem(item) {
const unitPrice = parseFloat(item.unit_price || 0);
const qty = parseFloat(item.quantity || 1);
const discount = parseFloat(item.discount_pct || 0);
return (unitPrice * qty * (1 - discount / 100)) <= 0;
}
function findZeroPriceItem() {
if (allowZeroPriceSales) return null;
return cart.find(isZeroPriceItem) || null;
}
function modifyPrice() {
if (!canEditPrice) { showToast('No tienes permiso para modificar precios'); return; }
if (selectedRow < 0 || selectedRow >= cart.length) {
@@ -451,10 +471,13 @@ const POS = (() => {
const p = prompt('Nuevo precio unitario:', cart[selectedRow].unit_price);
if (p !== null) {
const n = parseFloat(p);
if (n >= 0) {
cart[selectedRow].unit_price = n;
renderCart();
if (n < 0) { showToast('Precio no válido'); return; }
if (!allowZeroPriceSales && n === 0) {
showToast('No está permitido dejar el precio en $0');
return;
}
cart[selectedRow].unit_price = n;
renderCart();
}
}
@@ -1052,6 +1075,12 @@ const POS = (() => {
generate_cfdi: document.getElementById('cfdiCheck').checked,
};
const zeroItem = findZeroPriceItem();
if (zeroItem) {
showToast('No está permitido vender artículos en $0: ' + (zeroItem.name || zeroItem.part_number));
return;
}
const confirmBtn = document.getElementById('btnConfirmPayment');
confirmBtn.disabled = true;
confirmBtn.textContent = 'Procesando...';
@@ -1105,6 +1134,11 @@ const POS = (() => {
async function createRemissionNote() {
if (cart.length === 0) { showToast('Carrito vacio'); return; }
if (!canCreateRemission) { showToast('No tienes permiso para generar notas de remision'); return; }
const zeroItem = findZeroPriceItem();
if (zeroItem) {
showToast('No está permitido vender artículos en $0: ' + (zeroItem.name || zeroItem.part_number));
return;
}
const total = getTotal();
const noteData = {
@@ -1262,6 +1296,11 @@ const POS = (() => {
// ─── Quotation ───────────────────────
async function saveQuotation() {
if (cart.length === 0) { showToast('Carrito vacio'); return; }
const zeroItem = findZeroPriceItem();
if (zeroItem) {
showToast('No está permitido cotizar artículos en $0: ' + (zeroItem.name || zeroItem.part_number));
return;
}
const body = {
items: cart.map(item => ({
@@ -1302,6 +1341,11 @@ const POS = (() => {
async function createLayaway() {
if (!canCreateLayaway) { showToast('No tienes permiso para crear apartados'); return; }
if (cart.length === 0) { alert('Carrito vacio'); return; }
const zeroItem = findZeroPriceItem();
if (zeroItem) {
alert('No está permitido apartar artículos en $0: ' + (zeroItem.name || zeroItem.part_number));
return;
}
if (!currentCustomer) { alert('Seleccione un cliente para apartado'); return; }
const total = getTotal();
@@ -1345,6 +1389,11 @@ const POS = (() => {
function createServiceOrder() {
if (!canCreateWorkshopOrder) { showToast('No tienes permiso para ordenes de taller'); return; }
if (cart.length === 0) { showToast('Carrito vacio'); return; }
const zeroItem = findZeroPriceItem();
if (zeroItem) {
showToast('No está permitido generar orden con artículos en $0: ' + (zeroItem.name || zeroItem.part_number));
return;
}
const custInput = document.getElementById('soCustomer');
if (custInput) custInput.value = currentCustomer ? currentCustomer.name : 'Publico General';
document.getElementById('serviceOrderModal').classList.add('open');