-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathSheetUtils.js
More file actions
273 lines (240 loc) · 10.2 KB
/
SheetUtils.js
File metadata and controls
273 lines (240 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
// Índice de la columna K donde se almacena el ID del registro (base 0)
const RECORD_ID_COLUMN_INDEX = 10;
/**
* Obtiene la cotización actual del dólar (USD a ARS)
* @return {number} Cotización del dólar blue
*/
function getUsdToArsRate() {
try {
const response = UrlFetchApp.fetch('https://dolarapi.com/v1/dolares/blue', { muteHttpExceptions: true });
if (response.getResponseCode() !== 200) {
throw new Error(`Error al obtener cotización del dólar: HTTP ${response.getResponseCode()} - ${response.getContentText()}`);
}
const data = JSON.parse(response.getContentText());
if (!data.venta || isNaN(data.venta) || data.venta <= 0) {
throw new Error(`Cotización del dólar inválida: ${JSON.stringify(data)}`);
}
return data.venta;
} catch (error) {
logError('getUsdToArsRate', error);
throw error;
}
}
/**
* Registra datos estructurados en la hoja de gastos
* @param {Object} data - Datos estructurados del gasto
* @param {number} timestamp - Marca de tiempo Unix
* @param {string} [recordId] - ID único del registro (se genera si no se proporciona)
* @return {string} ID único del registro guardado
*/
function logToExpenseSheet(data, timestamp, recordId) {
try {
const sheet = getExpensesSheet();
// Generar ID único si no se proporcionó
if (!recordId) {
recordId = Utilities.getUuid();
}
// Usar la fecha proporcionada por el usuario si está disponible, de lo contrario usar timestamp
let baseDate;
if (data.fecha) {
// Convertir fecha dd/MM/yyyy a objeto Date
const [day, month, year] = data.fecha.split('/').map(Number);
baseDate = new Date(year, month - 1, day);
} else {
baseDate = new Date(timestamp * 1000);
}
// Determinar el tipo de registro en la columna I
const recordType = data.tipo === 'gasto' ? 'Gastos' :
data.tipo === 'ingreso' ? 'Ingresos' :
'Transferencias';
// Determinar la moneda y obtener cotización si es necesario
const currency = data.moneda === 'USD' ? 'USD' : 'ARS';
const usdRate = currency === 'USD' ? getUsdToArsRate() : null;
// Manejar según el tipo de registro
if (data.tipo === 'transferencia') {
// Transferencia: crear dos registros
createTransferRecords(sheet, data, baseDate, recordType, currency, usdRate, recordId);
} else if (data.tipo === 'gasto' && data.cuotas && data.cuotas > 1) {
// Gasto con cuotas: crear un registro por cuota
createInstallmentRecords(sheet, data, baseDate, recordType, currency, usdRate, recordId);
} else {
// Registro simple: gasto sin cuotas o ingreso
createSimpleRecord(sheet, data, baseDate, recordType, currency, usdRate, recordId);
}
// Ordenar la hoja por la fecha (columna 1) en orden descendente
const range = sheet.getDataRange();
range.sort({ column: 1, ascending: false });
return recordId;
} catch (error) {
logError('logToExpenseSheet', error);
throw error;
}
}
/**
* Crea registros para transferencias (registro negativo origen + registro positivo destino)
* @param {Sheet} sheet - Hoja de cálculo
* @param {Object} data - Datos del registro
* @param {Date} baseDate - Fecha base del registro
* @param {string} recordType - Tipo de registro
* @param {string} currency - Moneda del registro ("ARS" o "USD")
* @param {number|null} usdRate - Cotización del dólar (null si es ARS)
* @param {string} recordId - ID único del registro
*/
function createTransferRecords(sheet, data, baseDate, recordType, currency, usdRate, recordId) {
const formattedDate = Utilities.formatDate(baseDate, Session.getScriptTimeZone(), "dd/MM/yyyy");
const amount = Math.abs(data.monto);
// Si es en USD, convertir a pesos para la columna H
const amountInArs = currency === 'USD' ? amount * usdRate : amount;
// Registro negativo para cuenta origen
const lastRow1 = sheet.getLastRow() + 1;
sheet.getRange(lastRow1, 1, 1, 11).setValues([
[formattedDate, -amount, data.cuenta, "", "",
`Transferencia a ${data.cuenta_destino}`, "", -amountInArs, recordType, currency, recordId]
]);
// Registro positivo para cuenta destino
const lastRow2 = sheet.getLastRow() + 1;
sheet.getRange(lastRow2, 1, 1, 11).setValues([
[formattedDate, amount, data.cuenta_destino, "", "",
`Transferencia de ${data.cuenta}`, "", amountInArs, recordType, currency, recordId]
]);
}
/**
* Crea registros para gastos en cuotas (un registro por cuota)
* @param {Sheet} sheet - Hoja de cálculo
* @param {Object} data - Datos del registro
* @param {Date} baseDate - Fecha base del registro
* @param {string} recordType - Tipo de registro
* @param {string} currency - Moneda del registro ("ARS" o "USD")
* @param {number|null} usdRate - Cotización del dólar (null si es ARS)
* @param {string} recordId - ID único del registro
*/
function createInstallmentRecords(sheet, data, baseDate, recordType, currency, usdRate, recordId) {
const totalAmount = Math.abs(data.monto);
const installments = parseInt(data.cuotas);
const monthlyAmount = totalAmount / installments;
// Si es en USD, convertir a pesos para la columna H
const monthlyAmountInArs = currency === 'USD' ? monthlyAmount * usdRate : monthlyAmount;
// Crear un registro por cada cuota
for (let i = 0; i < installments; i++) {
// Calcular la fecha de cada cuota (mes siguiente para cada cuota)
const installmentDate = new Date(baseDate.getFullYear(), baseDate.getMonth() + i, baseDate.getDate());
const formattedDate = Utilities.formatDate(installmentDate, Session.getScriptTimeZone(), "dd/MM/yyyy");
// Descripción con información de cuota
const description = `${data.descripcion} (Cuota ${i + 1}/${installments})`;
const lastRow = sheet.getLastRow() + 1;
sheet.getRange(lastRow, 1, 1, 11).setValues([
[formattedDate, -monthlyAmount, data.cuenta, data.categoria, data.subcategoria,
description, "", -monthlyAmountInArs, recordType, currency, recordId]
]);
}
}
/**
* Crea un registro simple (gasto sin cuotas o ingreso)
* @param {Sheet} sheet - Hoja de cálculo
* @param {Object} data - Datos del registro
* @param {Date} baseDate - Fecha base del registro
* @param {string} recordType - Tipo de registro
* @param {string} currency - Moneda del registro ("ARS" o "USD")
* @param {number|null} usdRate - Cotización del dólar (null si es ARS)
* @param {string} recordId - ID único del registro
*/
function createSimpleRecord(sheet, data, baseDate, recordType, currency, usdRate, recordId) {
const formattedDate = Utilities.formatDate(baseDate, Session.getScriptTimeZone(), "dd/MM/yyyy");
// Determinar el signo del monto según el tipo
let amount = Math.abs(data.monto);
if (data.tipo === 'gasto') {
amount = -amount; // Gastos son negativos
}
// Los ingresos quedan positivos
// Si es en USD, convertir a pesos para la columna H
const amountInArs = currency === 'USD' ? amount * usdRate : amount;
const lastRow = sheet.getLastRow() + 1;
sheet.getRange(lastRow, 1, 1, 11).setValues([
[formattedDate, amount, data.cuenta, data.categoria, data.subcategoria,
data.descripcion, "", amountInArs, recordType, currency, recordId]
]);
}
/**
* Obtiene la hoja de registros
* @return {Sheet} Hoja de registros
*/
function getExpensesSheet() {
const sheet = SpreadsheetApp.openById(CONFIG.SHEET_ID).getSheetByName(CONFIG.EXPENSES_SHEET_NAME);
if (!sheet) {
throw new Error(`Hoja "${CONFIG.EXPENSES_SHEET_NAME}" no encontrada en la planilla`);
}
return sheet;
}
/**
* Busca filas en la hoja de registros por su ID de registro (columna K)
* @param {string} recordId - ID único del registro
* @param {Sheet} [sheet] - Hoja de cálculo (se obtiene automáticamente si no se proporciona)
* @return {number[]} Array de números de fila donde se encontró el registro
*/
function findRowsByRecordId(recordId, sheet) {
if (!sheet) {
sheet = getExpensesSheet();
}
const lastRow = sheet.getLastRow();
if (lastRow < 1) {
return [];
}
// Leer solo la columna de IDs (columna K, índice base 1 = RECORD_ID_COLUMN_INDEX + 1)
const idColumnRange = sheet.getRange(1, RECORD_ID_COLUMN_INDEX + 1, lastRow, 1);
const idValues = idColumnRange.getValues();
const rows = [];
for (let i = 0; i < idValues.length; i++) {
if (idValues[i][0] === recordId) {
rows.push(i + 1); // +1 porque las filas en Sheets empiezan en 1
}
}
return rows;
}
/**
* Elimina todas las filas asociadas a un ID de registro
* @param {string} recordId - ID único del registro a eliminar
* @return {boolean} True si se eliminaron filas, false si no se encontraron
*/
function deleteRecordsByRecordId(recordId) {
const sheet = getExpensesSheet();
const rows = findRowsByRecordId(recordId, sheet);
if (rows.length === 0) {
return false;
}
// Eliminar filas de abajo hacia arriba para no alterar los índices
rows.sort((a, b) => b - a);
for (const row of rows) {
sheet.deleteRow(row);
}
return true;
}
/**
* Registra errores en la hoja de errores
* @param {string} functionName - Nombre de la función donde ocurrió el error
* @param {Error} error - Objeto de error
* @param {Object} additionalInfo - Información adicional opcional
*/
function logError(functionName, error, additionalInfo = {}) {
try {
const sheet = SpreadsheetApp.openById(CONFIG.SHEET_ID).getSheetByName(CONFIG.ERROR_SHEET_NAME);
// Si la hoja no existe, crearla
if (!sheet) {
const ss = SpreadsheetApp.openById(CONFIG.SHEET_ID);
const newSheet = ss.insertSheet(CONFIG.ERROR_SHEET_NAME);
newSheet.appendRow(['Timestamp', 'Function', 'Error Message', 'Stack Trace', 'Additional Info']);
}
// Registrar el error
const errorSheet = SpreadsheetApp.openById(CONFIG.SHEET_ID).getSheetByName(CONFIG.ERROR_SHEET_NAME);
errorSheet.appendRow([
new Date(),
functionName,
error.message || String(error),
error.stack || 'No stack trace',
JSON.stringify(additionalInfo)
]);
} catch (e) {
// Si falla el registro en la hoja, intentar con Logger
Logger.log(`ERROR en ${functionName}: ${error.message || String(error)}`);
Logger.log(`Error al registrar el error: ${e.message || String(e)}`);
}
}