JairoDanielMT's picture
Add receipt image registration with category expiry rules
9218640
const SHEET_NAME = 'Productos';
const MOVEMENTS_SHEET_NAME = 'Movimientos';
const HEADERS = [
'ID',
'Producto',
'Precio',
'Cantidad',
'Unidad',
'FechaCaducidad',
'FechaIngreso',
'FechaProduccion',
'Categoria',
'CaducidadEstimada',
'Notas',
'Fuente',
'StockActual',
'ConsumidoTotal',
'CreadoEn',
'ActualizadoEn'
];
const MOVEMENT_HEADERS = [
'ID',
'ProductID',
'Producto',
'Tipo',
'Cantidad',
'Unidad',
'Notas',
'Fuente',
'CreadoEn'
];
function doGet(e) {
return handleRequest_({
method: 'GET',
params: e.parameter || {}
});
}
function doPost(e) {
const body = e.postData && e.postData.contents ? JSON.parse(e.postData.contents) : {};
return handleRequest_({
method: 'POST',
params: body
});
}
function handleRequest_(request) {
try {
const params = request.params || {};
validateToken_(params.token);
const action = params.action;
if (action === 'addRecord') return jsonResponse_(addRecord_(params.record || {}));
if (action === 'listRecords') return jsonResponse_(listRecords_(params.query || ''));
if (action === 'consumeProduct') return jsonResponse_(consumeProduct_(params.consumption || {}));
if (action === 'listMovements') return jsonResponse_(listMovements_());
if (action === 'replaceSnapshot') {
return jsonResponse_(replaceSnapshot_(params.records || [], params.movements || []));
}
return jsonResponse_({ ok: false, error: 'Accion no soportada.' });
} catch (error) {
return jsonResponse_({ ok: false, error: error.message });
}
}
function getSheet_() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let sheet = spreadsheet.getSheetByName(SHEET_NAME);
if (!sheet) sheet = spreadsheet.insertSheet(SHEET_NAME);
if (sheet.getLastRow() === 0) {
sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]);
sheet.setFrozenRows(1);
}
return sheet;
}
function getMovementSheet_() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let sheet = spreadsheet.getSheetByName(MOVEMENTS_SHEET_NAME);
if (!sheet) sheet = spreadsheet.insertSheet(MOVEMENTS_SHEET_NAME);
if (sheet.getLastRow() === 0) {
sheet.getRange(1, 1, 1, MOVEMENT_HEADERS.length).setValues([MOVEMENT_HEADERS]);
sheet.setFrozenRows(1);
}
return sheet;
}
function validateToken_(token) {
const scriptToken = PropertiesService.getScriptProperties().getProperty('API_TOKEN');
if (!scriptToken) throw new Error('Falta API_TOKEN en Script Properties.');
if (token !== scriptToken) throw new Error('Token invalido.');
}
function addRecord_(record) {
const sheet = getSheet_();
const movementSheet = getMovementSheet_();
const createdAt = new Date().toISOString();
const id = record.id || Utilities.getUuid();
sheet.appendRow([
id,
record.producto || '',
Number(record.precio || 0),
Number(record.cantidad || 0),
record.unidad || 'unidad',
record.fechaCaducidad || '',
record.fechaIngreso || '',
record.fechaProduccion || '',
record.categoria || '',
String(Boolean(record.caducidadEstimada || false)),
record.notas || '',
record.fuente || 'web',
Number(record.stockActual != null ? record.stockActual : record.cantidad || 0),
Number(record.consumidoTotal || 0),
record.createdAt || createdAt,
record.updatedAt || createdAt
]);
movementSheet.appendRow([
Utilities.getUuid(),
id,
record.producto || '',
'ingreso',
Number(record.cantidad || 0),
record.unidad || 'unidad',
record.notas || '',
record.fuente || 'web',
createdAt
]);
return { ok: true, id: id, createdAt: createdAt };
}
function listRecords_(query) {
const sheet = getSheet_();
const values = sheet.getDataRange().getValues();
if (values.length <= 1) return { ok: true, records: [] };
const headers = values[0];
const rows = values.slice(1).map(function(row) {
return toRecord_(headers, row);
});
const normalizedQuery = String(query || '').toLowerCase().trim();
const records = normalizedQuery
? rows.filter(function(record) {
return JSON.stringify(record).toLowerCase().indexOf(normalizedQuery) >= 0;
})
: rows;
return { ok: true, records: records };
}
function toRecord_(headers, row) {
const result = {};
headers.forEach(function(header, index) {
result[header] = row[index];
});
return {
id: result.ID || '',
producto: result.Producto || '',
precio: result.Precio || 0,
cantidad: result.Cantidad || 0,
unidad: result.Unidad || 'unidad',
fechaCaducidad: formatDateValue_(result.FechaCaducidad),
fechaIngreso: formatDateValue_(result.FechaIngreso),
fechaProduccion: formatDateValue_(result.FechaProduccion),
categoria: result.Categoria || '',
caducidadEstimada: String(result.CaducidadEstimada).toLowerCase() === 'true',
notas: result.Notas || '',
fuente: result.Fuente || '',
stockActual: result.StockActual || 0,
consumidoTotal: result.ConsumidoTotal || 0,
createdAt: formatDateValue_(result.CreadoEn),
updatedAt: formatDateValue_(result.ActualizadoEn)
};
}
function consumeProduct_(consumption) {
const sheet = getSheet_();
const movementSheet = getMovementSheet_();
const values = sheet.getDataRange().getValues();
if (values.length <= 1) throw new Error('No hay productos registrados.');
const headers = values[0];
const rows = values.slice(1);
const targetProductId = consumption.productId || '';
let rowIndex = -1;
for (var i = 0; i < rows.length; i++) {
var currentRecord = toRecord_(headers, rows[i]);
if (targetProductId) {
if (currentRecord.id === targetProductId) {
rowIndex = i + 2;
break;
}
} else if (
String(currentRecord.producto).toLowerCase() === String(consumption.producto || '').toLowerCase() &&
Number(currentRecord.stockActual || 0) > 0
) {
rowIndex = i + 2;
break;
}
}
if (rowIndex === -1) throw new Error('No se encontro el producto para consumir.');
const recordValues = sheet.getRange(rowIndex, 1, 1, headers.length).getValues()[0];
const record = toRecord_(headers, recordValues);
const quantity = Number(consumption.cantidad || 0);
if (record.unidad !== consumption.unidad) throw new Error('La unidad no coincide.');
if (Number(record.stockActual) < quantity) throw new Error('Stock insuficiente en Google Sheets.');
const newStock = Number(record.stockActual) - quantity;
const newConsumed = Number(record.consumidoTotal || 0) + quantity;
const updatedAt = new Date().toISOString();
const columns = headerIndexMap_(headers);
sheet.getRange(rowIndex, columns.StockActual).setValue(newStock);
sheet.getRange(rowIndex, columns.ConsumidoTotal).setValue(newConsumed);
sheet.getRange(rowIndex, columns.ActualizadoEn).setValue(updatedAt);
movementSheet.appendRow([
Utilities.getUuid(),
record.id,
record.producto,
'consumo',
quantity,
record.unidad,
consumption.notas || '',
consumption.fuente || 'telegram-consumo',
updatedAt
]);
return {
ok: true,
id: record.id,
stockActual: newStock,
consumidoTotal: newConsumed,
updatedAt: updatedAt
};
}
function listMovements_() {
const sheet = getMovementSheet_();
const values = sheet.getDataRange().getValues();
if (values.length <= 1) return { ok: true, movements: [] };
const headers = values[0];
const movements = values.slice(1).map(function(row) {
const result = {};
headers.forEach(function(header, index) {
result[header] = row[index];
});
return {
id: result.ID || '',
productId: result.ProductID || '',
producto: result.Producto || '',
tipo: result.Tipo || '',
cantidad: result.Cantidad || 0,
unidad: result.Unidad || 'unidad',
notas: result.Notas || '',
fuente: result.Fuente || '',
createdAt: formatDateValue_(result.CreadoEn)
};
});
return { ok: true, movements: movements };
}
function replaceSnapshot_(records, movements) {
const productSheet = getSheet_();
const movementSheet = getMovementSheet_();
productSheet.clearContents();
movementSheet.clearContents();
productSheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]);
movementSheet.getRange(1, 1, 1, MOVEMENT_HEADERS.length).setValues([MOVEMENT_HEADERS]);
if (records.length) {
const productRows = records.map(function(record) {
return [
record.id || Utilities.getUuid(),
record.producto || '',
Number(record.precio || 0),
Number(record.cantidad || 0),
record.unidad || 'unidad',
record.fechaCaducidad || '',
record.fechaIngreso || '',
record.fechaProduccion || '',
record.categoria || '',
String(Boolean(record.caducidadEstimada || false)),
record.notas || '',
record.fuente || 'web',
Number(record.stockActual || 0),
Number(record.consumidoTotal || 0),
record.createdAt || '',
record.updatedAt || record.createdAt || ''
];
});
productSheet.getRange(2, 1, productRows.length, HEADERS.length).setValues(productRows);
}
if (movements.length) {
const movementRows = movements.map(function(movement) {
return [
movement.id || Utilities.getUuid(),
movement.productId || '',
movement.producto || '',
movement.tipo || '',
Number(movement.cantidad || 0),
movement.unidad || 'unidad',
movement.notas || '',
movement.fuente || '',
movement.createdAt || ''
];
});
movementSheet.getRange(2, 1, movementRows.length, MOVEMENT_HEADERS.length).setValues(movementRows);
}
productSheet.setFrozenRows(1);
movementSheet.setFrozenRows(1);
return { ok: true, records: records.length, movements: movements.length };
}
function headerIndexMap_(headers) {
const result = {};
headers.forEach(function(header, index) {
result[header] = index + 1;
});
return result;
}
function formatDateValue_(value) {
if (Object.prototype.toString.call(value) === '[object Date]' && !isNaN(value)) {
return Utilities.formatDate(value, Session.getScriptTimeZone(), 'yyyy-MM-dd');
}
return value || '';
}
function jsonResponse_(payload) {
return ContentService
.createTextOutput(JSON.stringify(payload))
.setMimeType(ContentService.MimeType.JSON);
}