Add allow_zero_price_sales toggle: config UI, POS enforcement and backend validation
This commit is contained in:
@@ -625,6 +625,7 @@ const Config = (() => {
|
||||
await saveVehicleCompatSource();
|
||||
await saveAllowedBrands();
|
||||
await saveModules();
|
||||
await saveSalesSettings();
|
||||
await saveReceiptConfig();
|
||||
toast('Configuración guardada', 'ok');
|
||||
} catch (e) {
|
||||
@@ -1015,6 +1016,37 @@ const Config = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSalesSettings() {
|
||||
try {
|
||||
var res = await fetch(API + '/sales-settings', { headers: headers() });
|
||||
if (!res.ok) return;
|
||||
var data = await res.json();
|
||||
var cb = document.getElementById('cfg-allow-zero-price');
|
||||
if (cb) cb.checked = data.allow_zero_price_sales !== false;
|
||||
} catch (e) {
|
||||
console.error('Config.loadSalesSettings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSalesSettings() {
|
||||
var cb = document.getElementById('cfg-allow-zero-price');
|
||||
if (!cb) return;
|
||||
try {
|
||||
var res = await fetch(API + '/sales-settings', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ allow_zero_price_sales: cb.checked })
|
||||
});
|
||||
if (!res.ok) {
|
||||
var err = await res.json().catch(function() { return { error: res.statusText }; });
|
||||
throw new Error(err.error || 'Save failed');
|
||||
}
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tab navigation
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -1277,6 +1309,7 @@ const Config = (() => {
|
||||
loadVehicleCompatSource();
|
||||
loadAllowedBrands();
|
||||
loadModules();
|
||||
loadSalesSettings();
|
||||
loadReceiptConfig();
|
||||
if (isAdmin) {
|
||||
loadRolePermissions();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
||||
// so templates can use cache-busting query params freely.
|
||||
|
||||
const VERSION = 43;
|
||||
const VERSION = 44;
|
||||
const CACHE_NAME = 'nexus-pos-v' + VERSION;
|
||||
|
||||
const APP_SHELL = [
|
||||
|
||||
Reference in New Issue
Block a user