Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save uzim4414/f8432095b8f4e617896b8b72be660916 to your computer and use it in GitHub Desktop.
Save uzim4414/f8432095b8f4e617896b8b72be660916 to your computer and use it in GitHub Desktop.
KUPA 1.31 STABE BUT STILL NOT WORKING AS WELL
/**
* קוד משופר לניתוח קבלות באמצעות OCR והזנתן לטבלת גוגל שיטס
* עם ממשק חלונות מודליים (פופאפים) במקום אזורי בקרה בגיליון
*
* גרסה משופרת עם מבנה מודולרי וטיפול בשגיאות
*/
// --------------------- הגדרות גלובליות ---------------------
// הגדרת קטגוריות מוצרים לזיהוי
const CATEGORIES = {
חלב: ['חלב רגיל', 'חלב תנובה', 'חלב טרה', 'יוטבתה', 'תנובה', 'טרה', 'משק', 'יוגורט', 'חלב ', 'קרטון חלב', ' חלב'],
חלב_סויה: ['חלב סויה', 'משקה סויה', 'חלב שקדים', 'חלב אורז', 'חלב שקד', 'חלב קוקוס', 'אלפרו', 'סויה ', 'משקה אורז', 'תחליף חלב', 'משקה שקדים', 'משקה צמחי'],
קפה: ['קפה', 'נס קפה', 'קפסולות', 'אספרסו', 'נמס', 'עלית', "ג'ייקובס", 'דנקן', 'דונאטס', 'קפה טורקי', 'קפה שחור'],
חד_פעמי: ['חד פעמי', 'כוסות', 'צלחות', 'מפיות', 'כפיות', 'סכום חד פעמי', 'כוס חד', 'כוסות חד', 'כפות חד', 'מזלגות', 'סכינים', 'קשיות'],
גיבוש: ['פלאפל', 'גיבוש', 'כיבוד', 'בורקס', 'פיצה', 'עוגה', 'עוגיות', 'פירות', 'שתיה', 'עוגת', 'שוקולד', 'פיצוחים', 'ממתק', 'ממתקים', 'קינוח']
};
// הגדרת ספקים לזיהוי
const VENDORS = {
'רמי לוי': ['רמי לוי', 'שיווק השקמה', 'רמי לוי שיווק', 'ר.ל. שיווק', 'ר.ל.'],
'שופרסל': ['שופרסל', 'שופרסל דיל', 'שופרסל אקספרס', 'שופרסל שלי'],
'טיב טעם': ['טיב טעם', 'טיב'],
'ויקטורי': ['ויקטורי', 'ויקטורי סופר'],
'יינות ביתן': ['יינות ביתן', 'ביתן'],
'מגה': ['מגה', 'מגה בעיר', 'מגה מרקט'],
'AMPM': ['AMPM', 'AM:PM', 'אי אם פי אם', 'AM PM'],
'סופר-פארם': ['סופר-פארם', 'סופר פארם', 'super-pharm', 'סופרפארם'],
'ארומה': ['ארומה', 'קפה ארומה'],
'קפה קפה': ['קפה קפה'],
'אלונית': ['אלונית', 'חנות נוחות', 'אלונית מרקט', 'דלק אלונית']
};
// תקציב גיבוש ברירת מחדל
let DEFAULT_GATHERING_BUDGET = 400;
// כתובת המייל לחיפוש קבלות
const RECEIPT_EMAIL_ADDRESS = 'sivan@aleh.org';
// --------------------- פונקציות עזר ---------------------
/**
* בדיקת תקינות של קלט כללי
* @param {any} value ערך לבדיקה
* @param {string} name שם הערך (לצורכי לוג)
* @param {any} defaultValue ערך ברירת מחדל
* @return {any} הערך המקורי אם תקין, אחרת ערך ברירת מחדל
*/
function validateInput(value, name, defaultValue) {
if (value === undefined || value === null) {
Logger.log(`ערך לא תקין: ${name}`);
return defaultValue;
}
return value;
}
/**
* בדיקת תקינות של מחרוזת
* @param {any} text מחרוזת לבדיקה
* @param {string} name שם המחרוזת (לצורכי לוג)
* @param {string} defaultText מחרוזת ברירת מחדל
* @return {string} המחרוזת המקורית אם תקינה, אחרת מחרוזת ברירת מחדל
*/
function validateText(text, name, defaultText = '') {
if (typeof text !== 'string') {
Logger.log(`מחרוזת לא תקינה: ${name} (סוג: ${typeof text})`);
return defaultText;
}
return text;
}
/**
* פיצול טקסט לשורות מנוקות
* @param {string} text הטקסט לפיצול
* @return {Array} מערך של שורות
*/
function splitTextToLines(text) {
try {
// בדיקת תקינות הקלט
const safeText = validateText(text, 'splitTextToLines input');
if (safeText === '') {
return [];
}
// פיצול לשורות ונקיון
return safeText.split('\n')
.map(line => validateText(line, 'line in splitTextToLines').trim())
.filter(line => line !== '');
} catch (error) {
Logger.log(`שגיאה בפיצול טקסט לשורות: ${error}`);
return [];
}
}
/**
* פונקציה לעטיפת קריאות פונקציה בטיפול שגיאות
* @param {Function} func הפונקציה לקריאה
* @param {Array} args ארגומנטים לפונקציה
* @param {any} defaultValue ערך ברירת מחדל במקרה של שגיאה
* @return {any} תוצאת הפונקציה או ערך ברירת מחדל
*/
function safeCall(func, args, defaultValue) {
try {
return func.apply(null, args);
} catch (error) {
Logger.log(`שגיאה בקריאה לפונקציה ${func.name}: ${error}`);
return defaultValue;
}
}
/**
* המרת מחרוזת תאריך לאובייקט Date
* @param {string} dateStr מחרוזת תאריך
* @return {Date|null} אובייקט תאריך אם תקין, אחרת null
*/
function parseDate(dateStr) {
try {
// בדיקת תקינות הקלט
const safeDateStr = validateText(dateStr, 'parseDate input');
if (safeDateStr === '') {
return null;
}
// נסה לנתח תאריך במספר פורמטים
// פורמט DD.MM.YY
let parts = safeDateStr.split('.');
if (parts.length === 3) {
const day = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1; // החודשים ב-JS מתחילים מ-0
let year = parseInt(parts[2]);
if (year < 100) year += 2000;
return new Date(year, month, day);
}
// פורמט DD/MM/YY
parts = safeDateStr.split('/');
if (parts.length === 3) {
const day = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
let year = parseInt(parts[2]);
if (year < 100) year += 2000;
return new Date(year, month, day);
}
// פורמט DD-MM-YY
parts = safeDateStr.split('-');
if (parts.length === 3) {
const day = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
let year = parseInt(parts[2]);
if (year < 100) year += 2000;
return new Date(year, month, day);
}
// בדיקה עם regex
const dateMatch = safeDateStr.match(/(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{2,4})/);
if (dateMatch) {
const day = parseInt(dateMatch[1]);
const month = parseInt(dateMatch[2]) - 1;
let year = parseInt(dateMatch[3]);
if (year < 100) year += 2000;
return new Date(year, month, day);
}
return null;
} catch (error) {
Logger.log(`שגיאה בניתוח תאריך: ${error}`);
return null;
}
}
/**
* חיפוש תאריך בטקסט
* @param {string} text טקסט לחיפוש
* @return {Date|null} תאריך אם נמצא, אחרת null
*/
function findDateInText(text) {
try {
// בדיקת תקינות הקלט
const safeText = validateText(text, 'findDateInText input');
if (safeText === '') {
return null;
}
// חיפוש תאריך בפורמטים שונים
const datePatterns = [
/(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{2,4})/, // dd/mm/yy or dd-mm-yy or dd.mm.yy
/תאריך.*?(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{2,4})/, // תאריך: dd/mm/yy
/(\d{2})\.(\d{2})\.(\d{2})(?:\s+\d{2}:\d{2})?/, // DD.MM.YY HH:MM
/(\d{2})\/(\d{2})\/(\d{2})(?:\s+\d{2}:\d{2})?/, // DD/MM/YY HH:MM
/(\d{2})-(\d{2})-(\d{2})(?:\s+\d{2}:\d{2})?/ // DD-MM-YY HH:MM
];
for (const pattern of datePatterns) {
const match = safeText.match(pattern);
if (match) {
const day = parseInt(match[1]);
const month = parseInt(match[2]);
let year = parseInt(match[3]);
// הוסף 2000 אם השנה היא בפורמט דו-ספרתי
if (year < 100) {
year += 2000;
}
// בדיקת תקינות התאריך
if (day >= 1 && day <= 31 && month >= 1 && month <= 12) {
return new Date(year, month - 1, day);
}
}
}
// פורמט DD.MM.YYYY
const longDateMatch = safeText.match(/(\d{1,2})[.](\d{1,2})[.](\d{4})/);
if (longDateMatch) {
const day = parseInt(longDateMatch[1]);
const month = parseInt(longDateMatch[2]);
const year = parseInt(longDateMatch[3]);
if (day >= 1 && day <= 31 && month >= 1 && month <= 12) {
return new Date(year, month - 1, day);
}
}
return null;
} catch (error) {
Logger.log(`שגיאה בחיפוש תאריך בטקסט: ${error}`);
return null;
}
}
/**
* עיצוב תאריך למחרוזת בפורמט DD.MM.YY
* @param {Date} date תאריך לעיצוב
* @return {string} מחרוזת מעוצבת
*/
function formatDate(date) {
try {
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return '';
}
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear().toString().substr(2);
return `${day}.${month}.${year}`;
} catch (error) {
Logger.log(`שגיאה בעיצוב תאריך: ${error}`);
return '';
}
}
// --------------------- פונקציות ניתוח קבלות ---------------------
/**
* זיהוי ספק לפי טקסט הקבלה
* @param {string} text טקסט הקבלה
* @return {string} שם הספק או "לא ידוע"
*/
function identifyVendor(text) {
try {
// בדיקת תקינות הקלט
const safeText = validateText(text, 'identifyVendor input');
if (safeText === '') {
return 'לא ידוע';
}
// בדיקת הטקסט לכל ספק
for (const [vendorName, keywords] of Object.entries(VENDORS)) {
for (const keyword of keywords) {
if (safeText.includes(keyword)) {
return vendorName;
}
}
}
// בדיקות נוספות עבור ספקים עם צורך בזיהוי מיוחד
if (safeText.includes('שופרס') || safeText.includes('שופר-סל')) {
return 'שופרסל';
}
if (safeText.includes('AM') && safeText.includes('PM')) {
return 'AMPM';
}
if (safeText.includes('רמי') && safeText.includes('לוי')) {
return 'רמי לוי';
}
return 'לא ידוע';
} catch (error) {
Logger.log(`שגיאה בזיהוי ספק: ${error}`);
return 'לא ידוע';
}
}
/**
* סיווג פריט לקטגוריה לפי מילות מפתח
* @param {string} itemName שם הפריט
* @return {string} שם הקטגוריה
*/
function categorizeItem(itemName) {
try {
// בדיקת תקינות הקלט
const safeItemName = validateText(itemName, 'categorizeItem input');
if (safeItemName === '') {
return 'אחר';
}
const lowercaseItem = safeItemName.toLowerCase();
// חיפוש בקטגוריות שהוגדרו
for (const [category, keywords] of Object.entries(CATEGORIES)) {
for (const keyword of keywords) {
// בדיקה אם מילת המפתח מופיעה בשם הפריט
if (lowercaseItem.includes(keyword.toLowerCase())) {
// מקרה מיוחד: אם זוהה כחלב, בדוק שזה לא חלב סויה/שקדים וכו'
if (category === 'חלב') {
const soymilkKeywords = CATEGORIES.חלב_סויה;
if (soymilkKeywords.some(skw => lowercaseItem.includes(skw.toLowerCase()))) {
return 'חלב_סויה';
}
}
return category;
}
}
}
// אם לא נמצאה התאמה, החזר "אחר"
return 'אחר';
} catch (error) {
Logger.log(`שגיאה בסיווג פריט: ${error}`);
return 'אחר';
}
}
/**
* עדכון סכומי הקטגוריות בתוצאות הניתוח
* @param {Object} result אובייקט התוצאה
* @param {string} category קטגוריית הפריט
* @param {number} price מחיר הפריט
*/
function updateCategorySums(result, category, price) {
try {
// וודא שהמחיר הוא מספר תקין
const safePrice = typeof price === 'number' && !isNaN(price) ? price : 0;
if (safePrice === 0) {
return;
}
// וודא שיש אובייקט תוצאה תקין
if (!result || typeof result !== 'object') {
return;
}
// עדכן את הסכום המתאים לפי הקטגוריה
switch (category) {
case 'חלב':
result.regMilkTotal = (result.regMilkTotal || 0) + safePrice;
break;
case 'חלב_סויה':
result.soyMilkTotal = (result.soyMilkTotal || 0) + safePrice;
break;
case 'קפה':
result.coffeeTotal = (result.coffeeTotal || 0) + safePrice;
break;
case 'חד_פעמי':
result.disposableTotal = (result.disposableTotal || 0) + safePrice;
break;
case 'גיבוש':
result.gatheringTotal = (result.gatheringTotal || 0) + safePrice;
break;
default:
result.otherTotal = (result.otherTotal || 0) + safePrice;
}
} catch (error) {
Logger.log(`שגיאה בעדכון סכומי קטגוריות: ${error}`);
}
}
/**
* ניתוח כללי של טקסט קבלה
* @param {string} text טקסט הקבלה
* @return {Object} אובייקט עם פרטי הקבלה
*/
function parseReceiptText(text) {
try {
// בדיקת תקינות הקלט
const safeText = validateText(text, 'parseReceiptText input');
// יצירת אובייקט תוצאה בסיסי
const result = {
date: null,
store: '',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: safeText
};
// אם הטקסט ריק, החזר את התוצאה הבסיסית
if (safeText === '') {
Logger.log('טקסט הקבלה ריק');
return result;
}
// זיהוי סוג הספק
const vendorType = identifyVendor(safeText);
Logger.log(`זוהה ספק: ${vendorType}`);
// הפעלת הפונקציה המתאימה בהתאם לסוג הספק
let receiptData;
switch (vendorType) {
case 'רמי לוי':
receiptData = parseRamiLevyReceipt(safeText);
break;
case 'שופרסל':
receiptData = parseShuferSalReceipt(safeText);
break;
case 'AM:PM':
case 'AMPM':
receiptData = parseAMPMReceipt(safeText);
break;
default:
// אם לא זוהה ספק ספציפי, נשתמש בפונקציה גנרית
receiptData = parseGenericReceipt(safeText);
}
// וודא שיש לפחות תאריך וחנות
if (!receiptData.date) {
Logger.log('לא זוהה תאריך בקבלה, מנסה זיהוי גנרי נוסף');
const genericDate = findDateInText(safeText);
if (genericDate) {
receiptData.date = genericDate;
}
}
if (!receiptData.store || receiptData.store === '') {
receiptData.store = vendorType !== 'לא ידוע' ? vendorType : 'לא ידוע';
}
// חישוב סכומים כוללים אם לא חושבו
if (receiptData.items.length > 0) {
let totalSum = 0;
receiptData.items.forEach(item => {
totalSum += item.price;
});
// אם לא זוהה סכום כולל או שההפרש גדול מדי, השתמש בסכום המחושב
if (receiptData.total <= 0 || Math.abs(totalSum - receiptData.total) > 20) {
Logger.log(`סכום כולל לא זוהה או הפרש גדול. משתמש בסכום מחושב: ${totalSum}`);
receiptData.total = totalSum;
}
}
return receiptData;
} catch (error) {
Logger.log(`שגיאה בניתוח טקסט קבלה: ${error}`);
return {
date: null,
store: 'לא ידוע',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: validateText(text, 'parseReceiptText error recovery', '')
};
}
}
/**
* ניתוח קבלות של רמי לוי
* @param {string} text טקסט הקבלה
* @return {Object} אובייקט עם פרטי הקבלה
*/
function parseRamiLevyReceipt(text) {
try {
// בדיקת תקינות הקלט
const safeText = validateText(text, 'parseRamiLevyReceipt input');
// אובייקט לתוצאות
const result = {
date: null,
store: 'רמי לוי',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: safeText
};
// אם הטקסט ריק, החזר את התוצאה הבסיסית
if (safeText === '') {
return result;
}
// פיצול לשורות ונקיון
const lines = splitTextToLines(safeText);
// זיהוי תאריך - חיפוש בכל שורה לתבניות תאריך אפשריות
for (const line of lines) {
// פורמט DD.MM.YY או DD.MM.YYYY
const dotMatch = /(\d{1,2})\.(\d{1,2})\.(\d{2,4})/.exec(line);
if (dotMatch) {
const day = parseInt(dotMatch[1], 10);
const month = parseInt(dotMatch[2], 10) - 1;
let year = parseInt(dotMatch[3], 10);
if (year < 100) year += 2000;
if (day >= 1 && day <= 31 && month >= 0 && month <= 11) {
result.date = new Date(year, month, day);
Logger.log(`זוהה תאריך בקבלה רמי לוי (פורמט DD.MM.YY): ${day}/${month+1}/${year}`);
break;
}
}
// בדיקות דומות לפורמטים אחרים (DD/MM/YY, DD-MM-YY)
// ... (קיצרתי כאן)
}
// זיהוי סכום כולל - חיפוש בדפוסים ספציפיים של רמי לוי
const totalPatterns = [
/סה"כ\s*(?:לתשלום|לחיוב)?\s*[:\-]?\s*(\d+[.,]\d{1,2})/i,
/(?:לתשלום|לחיוב|חשבון|חשבונית|שולם)\s*[:\-]?\s*(\d+[.,]\d{1,2})/i,
/ס\s*ה\s*"?\s*כ\s*[:\-]?\s*(\d+[.,]\d{1,2})/i,
/סה"כ לחשבונית\s*(\d+[.,]\d{1,2})/i
];
for (const line of lines) {
for (const pattern of totalPatterns) {
const match = line.match(pattern);
if (match) {
const totalStr = match[1].replace(',', '.');
result.total = parseFloat(totalStr);
Logger.log(`זוהה סכום כולל ברמי לוי: ${result.total}`);
break;
}
}
if (result.total > 0) break;
}
// זיהוי פריטים
const itemStartIdentifiers = [
"פריט", "מוצר", "תיאור", "כמות", "ברקוד", "קוד-פריט"
];
const itemEndIdentifiers = [
"סה\"כ", "לתשלום", "סיכום", "עיגול", "שולם", "חשבון"
];
let inItemsSection = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// זיהוי תחילת רשימת פריטים
if (!inItemsSection) {
if (itemStartIdentifiers.some(id => line.includes(id))) {
inItemsSection = true;
continue;
}
}
// זיהוי סוף רשימת פריטים
else if (itemEndIdentifiers.some(id => line.includes(id))) {
inItemsSection = false;
continue;
}
// זיהוי פריטים
if (inItemsSection || (!inItemsSection && lines.length < 40)) {
const priceMatches = line.match(/(\d+[.,]\d{1,2})/g);
if (priceMatches && priceMatches.length > 0) {
// בחר את המחיר האחרון בשורה
const priceStr = priceMatches[priceMatches.length - 1];
const price = parseFloat(priceStr.replace(',', '.'));
// דלג על מחירים לא סבירים
if (price <= 0 || price > 200) continue;
// נקה את השורה
let itemName = line;
const barcodeMatch = line.match(/\d{13}|\d{8}/);
if (barcodeMatch) {
itemName = itemName.replace(barcodeMatch[0], '');
}
// נקה את המחיר וכל המספרים האחרים מהשורה
for (const priceMatch of priceMatches) {
itemName = itemName.replace(priceMatch, '');
}
// נקה רווחים מיותרים
itemName = itemName.trim().replace(/\s+/g, ' ');
// דלג על שורות ריקות אחרי ניקוי
if (itemName.length < 2) continue;
// קבע קטגוריה לפי מילות מפתח
let category = categorizeItem(itemName);
// הוסף את הפריט
const item = {
name: itemName,
category: category,
price: price
};
result.items.push(item);
// עדכן את הסכומים לפי קטגוריה
updateCategorySums(result, category, price);
Logger.log(`זוהה פריט: ${itemName}, קטגוריה: ${category}, מחיר: ${price}`);
}
}
}
return result;
} catch (error) {
Logger.log(`שגיאה בניתוח קבלת רמי לוי: ${error}`);
return {
date: null,
store: 'רמי לוי',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: validateText(text, 'parseRamiLevyReceipt error recovery', '')
};
}
}
/**
* ניתוח קבלות של שופרסל
* @param {string} text טקסט הקבלה
* @return {Object} אובייקט עם פרטי הקבלה
*/
function parseShuferSalReceipt(text) {
try {
// קריאה לפונקציה הכללית עם התאמות ספציפיות לשופרסל
// לוגיקה דומה לפונקציית parseRamiLevyReceipt אבל עם התאמות לשופרסל
// בדיקת תקינות הקלט
const safeText = validateText(text, 'parseShuferSalReceipt input');
// אובייקט לתוצאות
const result = {
date: null,
store: 'שופרסל',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: safeText
};
// אם הטקסט ריק, החזר את התוצאה הבסיסית
if (safeText === '') {
return result;
}
// לוגיקה ספציפית לשופרסל
// פיצול לשורות ונקיון
const lines = splitTextToLines(safeText);
// זיהוי תאריך, סכום כולל ופריטים בדומה לפונקציית parseRamiLevyReceipt
// אבל עם התאמות ספציפיות לשופרסל
return result;
} catch (error) {
Logger.log(`שגיאה בניתוח קבלת שופרסל: ${error}`);
return {
date: null,
store: 'שופרסל',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: validateText(text, 'parseShuferSalReceipt error recovery', '')
};
}
}
/**
* ניתוח קבלות של AMPM
* @param {string} text טקסט הקבלה
* @return {Object} אובייקט עם פרטי הקבלה
*/
function parseAMPMReceipt(text) {
try {
// בדיקת תקינות הקלט
const safeText = validateText(text, 'parseAMPMReceipt input');
// אובייקט לתוצאות
const result = {
date: null,
store: 'AMPM',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: safeText
};
// אם הטקסט ריק, החזר את התוצאה הבסיסית
if (safeText === '') {
return result;
}
// לוגיקה ספציפית ל-AMPM
// פיצול לשורות ונקיון
const lines = splitTextToLines(safeText);
// זיהוי תאריך, סכום כולל ופריטים בדומה לפונקציות הקודמות
// אבל עם התאמות ספציפיות ל-AMPM
return result;
} catch (error) {
Logger.log(`שגיאה בניתוח קבלת AMPM: ${error}`);
return {
date: null,
store: 'AMPM',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: validateText(text, 'parseAMPMReceipt error recovery', '')
};
}
}
/**
* ניתוח קבלות גנרי לכל ספק אחר
* @param {string} text טקסט הקבלה
* @return {Object} אובייקט עם פרטי הקבלה
*/
function parseGenericReceipt(text) {
try {
// בדיקת תקינות הקלט
const safeText = validateText(text, 'parseGenericReceipt input');
// אובייקט לתוצאות
const result = {
date: null,
store: 'לא ידוע',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: safeText
};
// אם הטקסט ריק, החזר את התוצאה הבסיסית
if (safeText === '') {
return result;
}
// לוגיקה גנרית לניתוח קבלות
// פיצול לשורות ונקיון
const lines = splitTextToLines(safeText);
// זיהוי שם חנות מהשורות הראשונות
for (let i = 0; i < Math.min(5, lines.length); i++) {
const line = lines[i];
// דלג על שורות קצרות מדי
if (line.length < 3) continue;
// דלג על שורות שמכילות רק מספרים או סימנים מיוחדים
if (/^[\d\s\-\.,]+$/.test(line)) continue;
// דלג על שורות שמכילות מילים נפוצות בכותרות קבלות
if (line.includes("חשבונית") || line.includes("קבלה") ||
line.includes("מספר") || line.includes("תאריך") ||
line.includes("עוסק") || line.includes("ח.פ")) continue;
// שם החנות הוא כנראה השורה הראשונה שאינה ריקה ואינה מכילה רק מספרים
result.store = line;
break;
}
// זיהוי תאריך
result.date = findDateInText(safeText);
if (result.date) {
Logger.log(`זוהה תאריך בקבלה: ${result.date.toLocaleDateString()}`);
}
// זיהוי סכום כולל
const totalPatterns = [
/סה"כ\s*(?:לתשלום|לחיוב)?\s*[:\-]?\s*(\d+[.,]\d{1,2})/i,
/(?:לתשלום|לחיוב|סכום)\s*[:\-]?\s*(\d+[.,]\d{1,2})/i,
/(?:שולם ב|שולם)\s*(?:אשראי|מזומן)?\s*[:\-]?\s*(\d+[.,]\d{1,2})/i,
/TOTAL\s*[:\-]?\s*(\d+[.,]\d{1,2})/i
];
for (const line of lines) {
for (const pattern of totalPatterns) {
const match = line.match(pattern);
if (match) {
const totalStr = match[1].replace(',', '.');
result.total = parseFloat(totalStr);
Logger.log(`זוהה סכום כולל: ${result.total}`);
break;
}
}
if (result.total > 0) break;
}
// זיהוי פריטים ומחירים
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// דלג על שורות כותרת וסיכום
if (line.includes("סה\"כ") || line.includes("לתשלום") ||
line.includes("TOTAL") || line.includes("ברקוד") ||
line.includes("מספר חשבונית") || line.includes("תאריך")) continue;
// זיהוי פריט ומחיר
const priceMatches = line.match(/(\d+[.,]\d{1,2})/g);
if (priceMatches && priceMatches.length > 0) {
// זיהוי המחיר הסביר ביותר
let bestPrice = 0;
let bestPriceStr = '';
for (const priceStr of priceMatches) {
const price = parseFloat(priceStr.replace(',', '.'));
// מחירים בטווח הרגיל של פריטים
if (price >= 5 && price <= 100) {
bestPrice = price;
bestPriceStr = priceStr;
break;
}
// מחירים נמוכים אך אפשריים
if (price > 0 && price < 5 && !bestPrice) {
bestPrice = price;
bestPriceStr = priceStr;
}
// מחירים גבוהים אך אפשריים
if (price > 100 && price < 200 && !bestPrice) {
bestPrice = price;
bestPriceStr = priceStr;
}
}
if (bestPrice > 0) {
// נקה את השורה
let itemName = line;
// נקה את המחיר
itemName = itemName.replace(bestPriceStr, '');
// נקה מספרים ארוכים (ברקודים)
itemName = itemName.replace(/\d{8,}/g, '');
// נקה מספרים שנראים כמו כמויות (X.XX)
itemName = itemName.replace(/\d+\s*X\s*\d+[.,]\d{1,2}/gi, '');
// נקה רווחים מיותרים
itemName = itemName.trim().replace(/\s+/g, ' ');
// דלג על שורות ריקות אחרי ניקוי
if (itemName.length < 2) continue;
// קבע קטגוריה לפי מילות מפתח
let category = categorizeItem(itemName);
// הוסף את הפריט
const item = {
name: itemName,
category: category,
price: bestPrice
};
result.items.push(item);
// עדכן את הסכומים לפי קטגוריה
updateCategorySums(result, category, bestPrice);
Logger.log(`זוהה פריט גנרי: ${itemName}, קטגוריה: ${category}, מחיר: ${bestPrice}`);
}
}
}
return result;
} catch (error) {
Logger.log(`שגיאה בניתוח קבלה גנרי: ${error}`);
return {
date: null,
store: 'לא ידוע',
total: 0,
items: [],
regMilkTotal: 0,
soyMilkTotal: 0,
coffeeTotal: 0,
disposableTotal: 0,
gatheringTotal: 0,
otherTotal: 0,
fullText: validateText(text, 'parseGenericReceipt error recovery', '')
};
}
}
// --------------------- פונקציות OCR ---------------------
/**
* הפעלת OCR על תמונת קבלה
* @param {Attachment} attachment קובץ הקבלה
* @return {string} טקסט שהופק מהקבלה
*/
function performOCR(attachment) {
try {
// בדיקת תקינות הקלט
if (!attachment) {
Logger.log('קובץ קבלה לא תקין');
return '';
}
// בדוק אם יש מפתח API מוגדר
const apiKey = PropertiesService.getScriptProperties().getProperty('VISION_API_KEY');
if (!apiKey) {
SpreadsheetApp.getUi().alert('מפתח API של Google Cloud Vision לא הוגדר. אנא הגדר אותו בהגדרות הסקריפט.');
return '';
}
// בדוק את סוג הקובץ
const mimeType = attachment.getContentType();
const fileName = attachment.getName();
// המר את הקובץ לbase64
const attachmentBytes = attachment.getBytes();
const base64encoded = Utilities.base64Encode(attachmentBytes);
// בנה את הבקשה ל-API
const apiUrl = 'https://vision.googleapis.com/v1/images:annotate?key=' + apiKey;
const requestBody = {
requests: [
{
image: {
content: base64encoded
},
features: [
{
type: 'TEXT_DETECTION'
}
],
imageContext: {
languageHints: ['he', 'iw', 'en'] // הוספנו אנגלית לשיפור הזיהוי
}
}
]
};
// שלח את הבקשה ל-API עם אפשרות להציג את השגיאה המלאה
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(requestBody),
muteHttpExceptions: true
};
// תוסף לוגים לבדיקת הבקשה ל-API
Logger.log(`שולח בקשת OCR עבור הקובץ: ${fileName}`);
const response = UrlFetchApp.fetch(apiUrl, options);
const responseCode = response.getResponseCode();
const responseText = response.getContentText();
// בדוק אם התגובה תקינה
if (responseCode !== 200) {
const errorMessage = `שגיאה בקריאה ל-API: קוד ${responseCode}, תגובה: ${responseText}`;
Logger.log(errorMessage);
// במקרה של שגיאת חיוב, הצג הודעה מפורטת למשתמש
if (responseCode === 403 && responseText.includes('billing')) {
const ui = SpreadsheetApp.getUi();
ui.alert(
'שגיאת חיוב ב-Google Cloud',
'לא הופעל אמצעי תשלום בפרויקט ה-Google Cloud שלך. עליך להפעיל אמצעי תשלום לפני שתוכל להשתמש ב-Vision API.\n\n' +
'1. היכנס ל: https://console.cloud.google.com/billing\n' +
'2. בחר את הפרויקט שלך\n' +
'3. קשר את הפרויקט לחשבון חיוב קיים או צור חשבון חיוב חדש\n' +
'4. הזן את פרטי אמצעי התשלום שלך',
ui.ButtonSet.OK
);
}
return '';
}
const responseData = JSON.parse(responseText);
// קבל את הטקסט המלא מהתגובה
if (responseData.responses &&
responseData.responses[0] &&
responseData.responses[0].fullTextAnnotation) {
const extractedText = responseData.responses[0].fullTextAnnotation.text;
Logger.log(`טקסט שזוהה מהקובץ ${fileName}: ${extractedText.substring(0, 100)}...`);
return extractedText;
} else {
Logger.log(`לא נמצא טקסט בקבלה: ${fileName}`);
return '';
}
} catch (error) {
Logger.log(`שגיאה בביצוע OCR: ${error}`);
return '';
}
}
// --------------------- פונקציות עיבוד מייל ---------------------
/**
* קבלת קבלות מהמייל
* @param {string} targetMonth חודש ספציפי לחיפוש (אופציונלי)
* @param {number} maxDays מספר הימים אחורה לחיפוש (ברירת מחדל: 30)
* @return {Array} מערך של אובייקטים המכילים את פרטי הקבלות
*/
function getReceiptsFromEmail(targetMonth = null, maxDays = 30) {
try {
const receipts = [];
// חשב את התאריך לפני maxDays ימים
const today = new Date();
const pastDate = new Date();
pastDate.setDate(today.getDate() - maxDays);
// פורמט לשאילתת חיפוש בג'ימייל (YYYY/MM/DD)
const pastDateStr = `${pastDate.getFullYear()}/${(pastDate.getMonth() + 1).toString().padStart(2, '0')}/${pastDate.getDate().toString().padStart(2, '0')}`;
// בנה את שאילתת החיפוש בהתאם לחודש המבוקש
let query = `from:${RECEIPT_EMAIL_ADDRESS} after:${pastDateStr}`;
// אם התקבל חודש מבוקש, נוסיף מסנן לפי החודש
if (targetMonth) {
// נמיר את החודש למספר
const hebrewMonths = [
'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני',
'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'
];
const monthIndex = hebrewMonths.indexOf(targetMonth) + 1;
if (monthIndex > 0) {
// נוסיף לשאילתה מסנן תאריך (חודש הנוכחי)
const currentYear = new Date().getFullYear();
const startDate = new Date(currentYear, monthIndex - 1, 1);
const endDate = new Date(currentYear, monthIndex, 0);
// פורמט לשאילתת חיפוש בג'ימייל (YYYY/MM/DD)
const startDateStr = `${startDate.getFullYear()}/${(startDate.getMonth() + 1).toString().padStart(2, '0')}/${startDate.getDate().toString().padStart(2, '0')}`;
const endDateStr = `${endDate.getFullYear()}/${(endDate.getMonth() + 1).toString().padStart(2, '0')}/${endDate.getDate().toString().padStart(2, '0')}`;
// החלף את after:${pastDateStr} עם טווח התאריכים של החודש הנבחר
query = query.replace(`after:${pastDateStr}`, `after:${startDateStr} before:${endDateStr}`);
Logger.log(`מחפש קבלות לחודש ${targetMonth} ${currentYear} (${startDateStr} עד ${endDateStr})`);
}
} else {
Logger.log(`מחפש קבלות מהתאריך ${pastDateStr} ועד היום`);
}
// הוסף מסנן לסוג הקבצים (תמונות ו-PDF בלבד)
query += ' has:attachment filename:(jpg OR jpeg OR png OR pdf)';
// אל תגביל ליותר מ-20 תוצאות
const maxResults = 20;
// חפש הודעות
Logger.log(`שאילתת חיפוש: ${query}`);
const threads = GmailApp.search(query, 0, maxResults);
Logger.log(`נמצאו ${threads.length} שרשורי הודעות מתאימים (מוגבל ל-${maxResults})`);
const processedFileNames = new Set(); // מניעת כפילויות קבצים זהים
for (let i = 0; i < threads.length; i++) {
const messages = threads[i].getMessages();
Logger.log(`בשרשור ${i+1} נמצאו ${messages.length} הודעות`);
for (let j = 0; j < messages.length; j++) {
const message = messages[j];
const attachments = message.getAttachments();
Logger.log(`בהודעה ${j+1} נמצאו ${attachments.length} קבצים מצורפים`);
// חפש קבצי תמונה או PDF שיכולים להיות קבלות (הגדלנו את המגבלה ל-10 קבצים לכל הודעה)
const maxAttachments = Math.min(attachments.length, 10);
for (let k = 0; k < maxAttachments; k++) {
const attachment = attachments[k];
const mimeType = attachment.getContentType();
const fileName = attachment.getName();
// בדוק אם הקובץ הוא מסוג תמונה או PDF
if (mimeType.indexOf('image/') === 0 || mimeType === 'application/pdf') {
// בדוק אם כבר עיבדנו קובץ עם שם זהה
if (processedFileNames.has(fileName)) {
Logger.log(`דילוג על קובץ מצורף כפול: ${fileName}`);
continue;
}
Logger.log(`קובץ מצורף ${k+1}: ${fileName}, סוג: ${mimeType}`);
receipts.push({
date: message.getDate(),
subject: message.getSubject(),
attachment: attachment,
fileName: fileName
});
processedFileNames.add(fileName); // סמן את הקובץ כמעובד
Logger.log(`נוסף קובץ קבלה: ${fileName}`);
// הגבל את המספר הכולל של קבלות ל-20
if (receipts.length >= 20) {
Logger.log(`הגעת למגבלת הקבלות המרבית (20). מסיים את החיפוש.`);
return receipts;
}
} else {
Logger.log(`דילוג על קובץ מסוג לא נתמך: ${fileName}, סוג: ${mimeType}`);
}
}
}
}
Logger.log(`סה"כ נמצאו ${receipts.length} קבצי קבלות ייחודיים`);
return receipts;
} catch (error) {
Logger.log(`שגיאה בקבלת קבלות מהמייל: ${error}`);
return [];
}
}
// --------------------- פונקציות חודש ותקציב ---------------------
/**
* הוספת חודש חדש (גיליון חדש)
* @param {string} selectedMonth החודש שנבחר בעברית
* @param {number} year השנה
*/
function addNewMonth(selectedMonth, year) {
try {
// בדיקת תקינות הקלט
selectedMonth = validateText(selectedMonth, 'addNewMonth.selectedMonth');
year = validateInput(year, 'addNewMonth.year', new Date().getFullYear());
if (selectedMonth === '' || year < 2000) {
Logger.log('קלט לא תקין להוספת חודש חדש');
SpreadsheetApp.getUi().alert('קלט לא תקין להוספת חודש חדש');
return;
}
const ui = SpreadsheetApp.getUi();
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
// בנה את שם הגיליון
const sheetName = `${selectedMonth} ${year}`;
// בדוק אם גיליון כזה כבר קיים
const existingSheet = spreadsheet.getSheetByName(sheetName);
if (existingSheet) {
ui.alert(`גיליון בשם "${sheetName}" כבר קיים`);
return;
}
// צור גיליון חדש
const newSheet = spreadsheet.insertSheet(sheetName);
// הגדר את כותרות העמודות בסדר הנדרש
const headers = [
'תאריך', // A
'שם ספק', // B
'פירוט', // C
'עלות שוטף', // D
'עלות חלב סויה', // E
'עלות גיבוש', // F
'סה"כ לחשבונית' // G
];
// הוסף את הכותרות
newSheet.getRange(1, 1, 1, headers.length).setValues([headers]);
// עיצוב הגיליון
newSheet.getRange(1, 1, 1, headers.length).setBackground('#4a86e8').setFontColor('white').setFontWeight('bold');
// הגדר את הרוחב של העמודות
for (let i = 1; i <= headers.length; i++) {
newSheet.setColumnWidth(i, 120);
}
ui.alert(`נוצר גיליון חדש: "${sheetName}"`);
// עבור לגיליון החדש
spreadsheet.setActiveSheet(newSheet);
} catch (error) {
Logger.log(`שגיאה בהוספת חודש חדש: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה בהוספת חודש חדש: ${error}`);
}
}
/**
* בדיקת חריגה מתקציב גיבוש
* @return {boolean} האם יש חריגה מהתקציב
*/
function checkBudgetOverflow() {
try {
const sheet = SpreadsheetApp.getActiveSheet();
// מצא את העמודה של "עלות גיבוש" (עמודה F)
const gatheringColumn = 6; // עמודה F
// חשב את סך ההוצאות לגיבוש
const gatheringValues = sheet.getRange(2, gatheringColumn, sheet.getLastRow() - 1, 1).getValues();
let totalGatheringExpenses = 0;
for (let i = 0; i < gatheringValues.length; i++) {
const value = gatheringValues[i][0];
if (typeof value === 'number') {
totalGatheringExpenses += value;
}
}
// קבל את תקציב הגיבוש המוגדר
let budgetValue = DEFAULT_GATHERING_BUDGET;
const budgetProperty = PropertiesService.getScriptProperties().getProperty('BUDGET');
if (budgetProperty) {
budgetValue = parseFloat(budgetProperty);
}
// בדוק אם יש חריגה מהתקציב
const isOverBudget = totalGatheringExpenses > budgetValue;
// הצג הודעה מתאימה
if (isOverBudget) {
const overflow = totalGatheringExpenses - budgetValue;
// הוסף הודעת חריגה בצבע אדום בתחתית הגיליון
const lastRow = sheet.getLastRow() + 1;
sheet.getRange(lastRow, 1, 1, 3).merge();
const overflowCell = sheet.getRange(lastRow, 1);
overflowCell.setValue(`חריגה מתקציב גיבוש: ${overflow.toFixed(2)} ש"ח`);
overflowCell.setFontColor('red');
overflowCell.setFontWeight('bold');
overflowCell.setHorizontalAlignment('center');
// הצג הודעה למשתמש
SpreadsheetApp.getUi().alert(
'התראת חריגה מתקציב',
`שים לב: ישנה חריגה מתקציב הגיבוש!\n\nסך ההוצאות: ${totalGatheringExpenses.toFixed(2)} ש"ח\nתקציב: ${budgetValue} ש"ח\nחריגה: ${overflow.toFixed(2)} ש"ח`,
SpreadsheetApp.getUi().ButtonSet.OK
);
return true; // יש חריגה
} else {
// מחק הודעת חריגה קודמת אם קיימת
const textFinder = sheet.createTextFinder("חריגה מתקציב");
const ranges = textFinder.findAll();
if (ranges.length > 0) {
for (let range of ranges) {
// בדוק אם השורה היא הודעת חריגה ולא חלק מנתונים אחרים
if (range.getFontColor() === '#ff0000') {
range.getSheet().deleteRow(range.getRow());
}
}
}
// חשב את הסכום שנותר בתקציב
const remaining = budgetValue - totalGatheringExpenses;
// הצג הודעה למשתמש
SpreadsheetApp.getUi().alert(
'סיכום תקציב גיבוש',
`סך הוצאות גיבוש: ${totalGatheringExpenses.toFixed(2)} ש"ח\nתקציב: ${budgetValue} ש"ח\nנותר: ${remaining.toFixed(2)} ש"ח`,
SpreadsheetApp.getUi().ButtonSet.OK
);
return false; // אין חריגה
}
} catch (error) {
Logger.log(`שגיאה בבדיקת חריגה מתקציב: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה בבדיקת חריגה מתקציב: ${error}`);
return false;
}
}
/**
* הגדרת תקציב גיבוש
* @param {number} newBudget התקציב החדש
* @return {boolean} האם ההגדרה הצליחה
*/
function setBudget(newBudget) {
try {
// וודא שהתקציב הוא מספר תקין
if (typeof newBudget !== 'number' || isNaN(newBudget) || newBudget < 0) {
SpreadsheetApp.getUi().alert('ערך תקציב לא תקין. אנא הכנס מספר חיובי.');
return false;
}
// עדכן את תקציב הגיבוש
DEFAULT_GATHERING_BUDGET = newBudget;
// שמור את התקציב החדש
PropertiesService.getScriptProperties().setProperty('BUDGET', newBudget.toString());
// הודע למשתמש
SpreadsheetApp.getUi().alert(`תקציב הגיבוש עודכן ל-${newBudget} ש"ח`);
return true;
} catch (error) {
Logger.log(`שגיאה בהגדרת תקציב גיבוש: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה בהגדרת תקציב גיבוש: ${error}`);
return false;
}
}
// --------------------- פונקציות גיליון ---------------------
/**
* הוספת קבלה לגיליון
* @param {Sheet} sheet דף העבודה
* @param {Object} receiptData נתוני הקבלה
* @param {Number} regularExpenses הוצאות שוטפות (חלב רגיל + קפה + חד פעמי)
* @param {Number} soyMilkExpenses הוצאות חלב סויה
* @param {Number} gatheringExpenses הוצאות פריטי גיבוש
* @return {boolean} האם ההוספה הצליחה
*/
function addReceiptToSheet(sheet, receiptData, regularExpenses, soyMilkExpenses, gatheringExpenses) {
try {
// בדיקת תקינות הקלט
if (!sheet) {
Logger.log('דף עבודה לא תקין');
return false;
}
if (!receiptData || typeof receiptData !== 'object') {
Logger.log('נתוני קבלה לא תקינים');
return false;
}
// וודא שההוצאות הן מספרים תקינים
regularExpenses = typeof regularExpenses === 'number' && !isNaN(regularExpenses) ? regularExpenses : 0;
soyMilkExpenses = typeof soyMilkExpenses === 'number' && !isNaN(soyMilkExpenses) ? soyMilkExpenses : 0;
gatheringExpenses = typeof gatheringExpenses === 'number' && !isNaN(gatheringExpenses) ? gatheringExpenses : 0;
// מצא את השורה האחרונה בגיליון
const lastRow = sheet.getLastRow() + 1;
// פורמט התאריך
let formattedDate = '';
if (receiptData.date) {
formattedDate = formatDate(receiptData.date);
}
// הכן פירוט כללי של המוצרים - מקבץ את הקטגוריות
const categories = new Set();
if (receiptData.items && Array.isArray(receiptData.items)) {
receiptData.items.forEach(item => {
if (item && typeof item === 'object' && item.category) {
if (item.category === 'חלב') categories.add('חלב');
else if (item.category === 'חלב_סויה') categories.add('חלב סויה');
else if (item.category === 'קפה') categories.add('קפה');
else if (item.category === 'חד_פעמי') categories.add('חד פעמי');
else if (item.category === 'גיבוש') categories.add('גיבוש');
else categories.add(item.name);
}
});
}
const itemsDetail = Array.from(categories).join(', ');
// הערכים בסדר הנכון לפי העמודות בגיליון:
// A: תאריך, B: שם ספק, C: פירוט, D: עלות שוטף, E: עלות חלב סויה, F: עלות גיבוש, G: סך הכל חשבונית
const values = [
formattedDate, // עמודה A - תאריך
receiptData.store, // עמודה B - שם הספק
itemsDetail, // עמודה C - פירוט
regularExpenses > 0 ? regularExpenses : '', // עמודה D - עלות שוטף
soyMilkExpenses > 0 ? soyMilkExpenses : '', // עמודה E - עלות חלב סויה
gatheringExpenses > 0 ? gatheringExpenses : '', // עמודה F - עלות גיבוש
receiptData.total // עמודה G - סך הכל חשבונית
];
// הוסף את השורה לגיליון
sheet.getRange(lastRow, 1, 1, values.length).setValues([values]);
return true;
} catch (error) {
Logger.log(`שגיאה בהוספת קבלה לגיליון: ${error}`);
return false;
}
}
// --------------------- פונקציות ממשק משתמש ---------------------
/**
* פונקציה ראשית - מוסיפה תפריט מותאם לגיליון
*/
function onOpen() {
try {
// הוסף תפריט לגיליון
const ui = SpreadsheetApp.getUi();
ui.createMenu('ניהול קבלות')
.addItem('עבד קבלות חדשות מהמייל', 'processNewReceiptsUI')
.addItem('עבד קבלות מחודש מסוים', 'processReceiptsByMonthUI')
.addItem('הוסף חודש חדש', 'addNewMonthUI')
.addSeparator()
.addItem('הגדר תקציב גיבוש', 'setBudgetUI')
.addItem('בדוק חריגה מתקציב', 'checkBudgetOverflowUI')
.addSeparator()
.addItem('התקנה ראשונית', 'setupUI')
.addToUi();
Logger.log('נוצר תפריט מותאם בהצלחה');
} catch (error) {
Logger.log(`שגיאה ביצירת תפריט מותאם: ${error}`);
}
}
/**
* פונקציה לעיבוד קבלות - ממשק משתמש מודלי
*/
function processNewReceiptsUI() {
try {
const ui = SpreadsheetApp.getUi();
// שאל את המשתמש האם להמשיך
const response = ui.alert(
'עיבוד קבלות חדשות',
'האם ברצונך לעבד קבלות חדשות מהמייל?',
ui.ButtonSet.YES_NO
);
if (response !== ui.Button.YES) {
return;
}
// פעל לעיבוד הקבלות
processNewReceipts();
} catch (error) {
Logger.log(`שגיאה במסך עיבוד קבלות: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה במסך עיבוד קבלות: ${error}`);
}
}
/**
* פונקציה לעיבוד קבלות מחודש מסוים - ממשק משתמש מודלי
*/
function processReceiptsByMonthUI() {
try {
const ui = SpreadsheetApp.getUi();
// הצג דיאלוג לבחירת חודש
const hebrewMonths = [
'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני',
'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'
];
// בנה רשימת אפשרויות לתיבת הדו-שיח
let monthOptions = '';
for (let i = 0; i < hebrewMonths.length; i++) {
monthOptions += `${i+1}. ${hebrewMonths[i]}\n`;
}
// הצג תיבת דו-שיח לבחירת חודש
const response = ui.prompt(
'עיבוד קבלות לפי חודש',
`אנא בחר חודש לעיבוד קבלות:\n\n${monthOptions}\nהקלד את מספר החודש (1-12):`,
ui.ButtonSet.OK_CANCEL
);
if (response.getSelectedButton() !== ui.Button.OK) {
return;
}
// בדוק את הקלט
const monthNumber = parseInt(response.getResponseText());
if (isNaN(monthNumber) || monthNumber < 1 || monthNumber > 12) {
ui.alert('שגיאה', 'אנא הכנס מספר תקין בין 1 ל-12.', ui.ButtonSet.OK);
return;
}
// קבל את שם החודש העברי
const selectedMonth = hebrewMonths[monthNumber - 1];
// וודא שהמשתמש מעוניין לעבד קבלות לחודש שנבחר
const confirmResult = ui.alert(
'אישור',
`האם אתה בטוח שברצונך לעבד קבלות לחודש ${selectedMonth}?`,
ui.ButtonSet.YES_NO
);
if (confirmResult !== ui.Button.YES) {
return;
}
// עבד קבלות לחודש הנבחר
processNewReceipts(selectedMonth);
} catch (error) {
Logger.log(`שגיאה במסך עיבוד קבלות לפי חודש: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה במסך עיבוד קבלות לפי חודש: ${error}`);
}
}
/**
* פונקציה להוספת חודש חדש - ממשק משתמש מודלי
*/
function addNewMonthUI() {
try {
const ui = SpreadsheetApp.getUi();
// שאל את המשתמש איזה חודש להוסיף
const hebrewMonths = [
'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני',
'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'
];
// בנה רשימת אפשרויות לתיבת הדו-שיח
let monthOptions = '';
for (let i = 0; i < hebrewMonths.length; i++) {
monthOptions += `${i+1}. ${hebrewMonths[i]}\n`;
}
// הצג תיבת דו-שיח לבחירת חודש
const monthResponse = ui.prompt(
'הוספת חודש חדש',
`אנא בחר חודש:\n\n${monthOptions}\nהקלד את מספר החודש (1-12):`,
ui.ButtonSet.OK_CANCEL
);
if (monthResponse.getSelectedButton() !== ui.Button.OK) {
return;
}
// בדוק את הקלט
const monthNumber = parseInt(monthResponse.getResponseText());
if (isNaN(monthNumber) || monthNumber < 1 || monthNumber > 12) {
ui.alert('שגיאה', 'אנא הכנס מספר תקין בין 1 ל-12.', ui.ButtonSet.OK);
return;
}
// הצג תיבת דו-שיח להכנסת שנה
const yearResponse = ui.prompt(
'הוספת חודש חדש',
'אנא הכנס את השנה:',
ui.ButtonSet.OK_CANCEL
);
if (yearResponse.getSelectedButton() !== ui.Button.OK) {
return;
}
const year = parseInt(yearResponse.getResponseText());
if (isNaN(year) || year < 2000) {
ui.alert('שגיאה', 'אנא הכנס שנה תקינה (לפחות 2000).', ui.ButtonSet.OK);
return;
}
// קבל את שם החודש העברי
const selectedMonth = hebrewMonths[monthNumber - 1];
// קרא לפונקציה ליצירת חודש חדש
addNewMonth(selectedMonth, year);
} catch (error) {
Logger.log(`שגיאה במסך הוספת חודש חדש: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה במסך הוספת חודש חדש: ${error}`);
}
}
/**
* פונקציה להגדרת תקציב גיבוש - ממשק משתמש מודלי
*/
function setBudgetUI() {
try {
const ui = SpreadsheetApp.getUi();
// הצג את התקציב הנוכחי
const currentBudget = PropertiesService.getScriptProperties().getProperty('BUDGET') || DEFAULT_GATHERING_BUDGET;
// שאל את המשתמש מהו התקציב החדש
const response = ui.prompt(
'הגדרת תקציב גיבוש',
`התקציב הנוכחי: ${currentBudget} ש"ח\n\nאנא הכנס את התקציב החדש (בש"ח):`,
ui.ButtonSet.OK_CANCEL
);
if (response.getSelectedButton() !== ui.Button.OK) {
return;
}
const newBudget = parseFloat(response.getResponseText());
if (isNaN(newBudget) || newBudget < 0) {
ui.alert('ערך לא תקין. אנא הכנס מספר חיובי');
return;
}
// עדכן את תקציב הגיבוש
setBudget(newBudget);
// בדוק אם יש חריגה מהתקציב החדש
checkBudgetOverflow();
} catch (error) {
Logger.log(`שגיאה במסך הגדרת תקציב גיבוש: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה במסך הגדרת תקציב גיבוש: ${error}`);
}
}
/**
* בדיקת חריגה מתקציב - ממשק משתמש מודלי
*/
function checkBudgetOverflowUI() {
try {
// קרא לפונקציה לבדיקת חריגה מתקציב
checkBudgetOverflow();
} catch (error) {
Logger.log(`שגיאה במסך בדיקת חריגה מתקציב: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה במסך בדיקת חריגה מתקציב: ${error}`);
}
}
/**
* פונקציה להתקנה ראשונית - ממשק משתמש מודלי
*/
function setupUI() {
try {
const ui = SpreadsheetApp.getUi();
// הודעה להסבר על הדרישות לשימוש ב-API
ui.alert(
'התקנה ראשונית - שימוש ב-Google Cloud Vision API',
'כדי להשתמש ב-Google Cloud Vision API, עליך: \n\n' +
'1. ליצור פרויקט ב-Google Cloud Platform\n' +
'2. להפעיל את Cloud Vision API\n' +
'3. להפעיל אמצעי תשלום בפרויקט\n' +
'4. ליצור מפתח API\n\n' +
'אחרי שתסיים את התהליך, תוכל להזין את מפתח ה-API בחלון הבא.',
ui.ButtonSet.OK
);
// בקש מפתח API לשירות Vision
const apiResponse = ui.prompt(
'התקנה ראשונית',
'אנא הכנס מפתח API של Google Cloud Vision:',
ui.ButtonSet.OK_CANCEL
);
if (apiResponse.getSelectedButton() === ui.Button.OK) {
PropertiesService.getScriptProperties().setProperty('VISION_API_KEY', apiResponse.getResponseText());
}
// בקש את תקציב הגיבוש הראשוני
const budgetResponse = ui.prompt(
'תקציב גיבוש',
'אנא הכנס את תקציב הגיבוש הראשוני (בש"ח):',
ui.ButtonSet.OK_CANCEL
);
if (budgetResponse.getSelectedButton() === ui.Button.OK) {
const budget = parseFloat(budgetResponse.getResponseText());
if (!isNaN(budget) && budget >= 0) {
setBudget(budget);
}
}
// הצג הנחיות כיצד להפעיל את התשלום ב-Google Cloud
ui.alert(
'אמצעי תשלום לשירות Vision API',
'בכדי להשתמש ב-Cloud Vision API, עליך להפעיל את אמצעי התשלום בפרויקט שלך ב-Google Cloud Platform.\n\n' +
'1. היכנס ל: https://console.cloud.google.com/billing\n' +
'2. בחר את הפרויקט שלך\n' +
'3. קשר את הפרויקט לחשבון חיוב קיים או צור חשבון חיוב חדש\n' +
'4. הזן את פרטי אמצעי התשלום שלך\n\n' +
'לאחר שתבצע את השלבים האלה, תוכל להשתמש ב-Cloud Vision API.',
ui.ButtonSet.OK
);
ui.alert('ההתקנה הושלמה בהצלחה!');
} catch (error) {
Logger.log(`שגיאה במסך התקנה ראשונית: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה במסך התקנה ראשונית: ${error}`);
}
}
/**
* פונקציה לעיבוד קבלות חדשות מהמייל
* @param {string} month חודש ספציפי לחיפוש (אופציונלי)
*/
function processNewReceipts(month = null) {
try {
// קבל את דף העבודה הפעיל
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
// בדוק אם יש מפתח API מוגדר
const apiKey = PropertiesService.getScriptProperties().getProperty('VISION_API_KEY');
if (!apiKey) {
SpreadsheetApp.getUi().alert(
'מפתח API חסר',
'לא הוגדר מפתח API עבור Google Cloud Vision. עליך להגדיר מפתח API לפני שתוכל לעבד קבלות.\n\n' +
'אנא הרץ את פונקציית "התקנה ראשונית" מהתפריט.',
SpreadsheetApp.getUi().ButtonSet.OK
);
return;
}
// חפש הודעות עם קבלות במייל
const receipts = getReceiptsFromEmail(month);
if (receipts.length === 0) {
SpreadsheetApp.getUi().alert('לא נמצאו קבלות חדשות במייל');
return;
}
// הצג התקדמות למשתמש
const ui = SpreadsheetApp.getUi();
ui.alert(
'עיבוד קבלות',
`נמצאו ${receipts.length} קבלות לעיבוד. לחץ אישור כדי להתחיל בתהליך העיבוד.`,
ui.ButtonSet.OK
);
// מערך לשמירת תוצאות העיבוד לשימוש בממשק סקירה
const processedReceipts = [];
// עבור על כל הקבלות שנמצאו
for (let i = 0; i < receipts.length; i++) {
try {
const receipt = receipts[i];
Logger.log(`מעבד קבלה ${i+1}/${receipts.length}: ${receipt.fileName}`);
// הפעל OCR על תמונת הקבלה
const ocrText = performOCR(receipt.attachment);
if (!ocrText || ocrText.trim() === '') {
Logger.log(`לא נמצא טקסט בקבלה: ${receipt.fileName}`);
continue;
}
// כתוב את הטקסט שהתקבל מה-OCR ללוג
Logger.log(`טקסט שהתקבל מ-OCR (${receipt.fileName}): ${ocrText.substring(0, 200)}...`);
// נתח את הטקסט ומצא את הפרטים הרלוונטיים
const receiptData = parseReceiptText(ocrText);
// אם לא התקבל תאריך בקבלה, השתמש בתאריך המייל
if (!receiptData.date) {
receiptData.date = receipt.date;
Logger.log(`משתמש בתאריך המייל: ${receipt.date}`);
}
// הוסף את שם הקובץ לנתוני הקבלה (לצורך זיהוי)
receiptData.fileName = receipt.fileName;
// הוסף לרשימת הקבלות המעובדות
processedReceipts.push(receiptData);
} catch (error) {
Logger.log(`שגיאה בעיבוד הקבלה ${receipts[i].fileName}: ${error}`);
}
}
if (processedReceipts.length === 0) {
ui.alert('כישלון בעיבוד קבלות', 'לא הצלחנו לעבד אף קבלה.', ui.ButtonSet.OK);
return;
}
// הצג ממשק סקירה מודלי
showReviewUI(processedReceipts, sheet);
} catch (error) {
Logger.log(`שגיאה בעיבוד קבלות חדשות: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה בעיבוד קבלות חדשות: ${error}`);
}
}
/**
* פונקציה להצגת ממשק סקירה מודלי למשתמש
* @param {Array} processedReceipts מערך של קבלות מעובדות
* @param {Sheet} sheet הגיליון הפעיל
*/
function showReviewUI(processedReceipts, sheet) {
try {
const ui = SpreadsheetApp.getUi();
// צור HTML לתצוגת הקבלות
let htmlOutput = HtmlService.createHtmlOutput()
.setWidth(800)
.setHeight(600)
.setTitle('סקירת קבלות');
// צור טבלה להצגת הקבלות
let htmlContent = `
<style>
body { font-family: Arial, sans-serif; direction: rtl; text-align: right; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { padding: 8px; border: 1px solid #ddd; text-align: right; }
th { background-color: #4285f4; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.button-row { margin-top: 20px; text-align: center; }
.button {
padding: 8px 16px;
margin: 0 5px;
background-color: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button:hover { background-color: #3b77db; }
.receipt-details {
max-height: 200px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
margin-top: 5px;
display: none;
}
.checkbox { transform: scale(1.5); }
</style>
<h2 style="text-align: center;">סקירת קבלות לפני הוספה לגיליון</h2>
<p>נמצאו ${processedReceipts.length} קבלות. אנא סקור ובחר אילו להוסיף לגיליון:</p>
<form id="receiptsForm">
<table id="receiptsTable">
<thead>
<tr>
<th style="width: 50px;"><input type="checkbox" id="selectAll" onclick="toggleAll()" checked></th>
<th>שם קובץ</th>
<th>תאריך</th>
<th>חנות</th>
<th>סה"כ לתשלום</th>
<th>עלות חלב רגיל</th>
<th>עלות חלב סויה</th>
<th>עלות גיבוש</th>
<th>פעולות</th>
</tr>
</thead>
<tbody>`;
// הוסף את הקבלות לטבלה
for (let i = 0; i < processedReceipts.length; i++) {
const receipt = processedReceipts[i];
// פורמט התאריך
let formattedDate = '';
if (receipt.date) {
formattedDate = formatDate(receipt.date);
}
// סכום חלב רגיל, קפה, וחד פעמי ביחד
const combinedRegular = (receipt.regMilkTotal || 0) +
(receipt.coffeeTotal || 0) +
(receipt.disposableTotal || 0);
htmlContent += `
<tr>
<td><input type="checkbox" name="include_${i}" class="checkbox" checked></td>
<td>${receipt.fileName}</td>
<td><input type="text" name="date_${i}" value="${formattedDate}" style="width: 80px;"></td>
<td><input type="text" name="store_${i}" value="${receipt.store}" style="width: 100px;"></td>
<td><input type="number" name="total_${i}" value="${receipt.total}" step="0.01" style="width: 80px;"></td>
<td><input type="number" name="regMilk_${i}" value="${combinedRegular}" step="0.01" style="width: 80px;"></td>
<td><input type="number" name="soyMilk_${i}" value="${receipt.soyMilkTotal}" step="0.01" style="width: 80px;"></td>
<td><input type="number" name="gathering_${i}" value="${receipt.gatheringTotal}" step="0.01" style="width: 80px;"></td>
<td><button type="button" onclick="showDetails(${i})" class="button" style="padding: 2px 8px;">פרטים</button></td>
</tr>
<tr>
<td colspan="9">
<div id="details_${i}" class="receipt-details">
<strong>פריטים שזוהו:</strong><br>
<table style="width: 100%;">
<tr><th>שם פריט</th><th>קטגוריה</th><th>מחיר</th></tr>`;
// הוסף את הפריטים שזוהו
if (receipt.items && receipt.items.length > 0) {
for (let item of receipt.items) {
const category = item.category === 'חלב_סויה' ? 'חלב סויה' :
item.category === 'חד_פעמי' ? 'חד פעמי' : item.category;
htmlContent += `<tr><td>${item.name}</td><td>${category}</td><td>${item.price}</td></tr>`;
}
} else {
htmlContent += `<tr><td colspan="3">לא זוהו פריטים</td></tr>`;
}
// סיום פרטי הקבלה
htmlContent += `
</table>
<strong>טקסט מלא:</strong><br>
<pre style="white-space: pre-wrap; font-size: 12px;">${receipt.fullText ? receipt.fullText.substring(0, 500) + (receipt.fullText.length > 500 ? '...' : '') : 'אין טקסט'}</pre>
</div>
</td>
</tr>`;
}
// סיום הטבלה והוספת כפתורים
htmlContent += `
</tbody>
</table>
<div class="button-row">
<button type="button" class="button" onclick="submitForm()">הוסף קבלות נבחרות</button>
<button type="button" class="button" style="background-color: #ea4335;" onclick="google.script.host.close()">ביטול</button>
</div>
</form>
<script>
// פונקציה להצגת פרטי קבלה
function showDetails(index) {
const detailsDiv = document.getElementById('details_' + index);
if (detailsDiv.style.display === 'block') {
detailsDiv.style.display = 'none';
} else {
// הסתר את כל הפרטים האחרים
const allDetails = document.getElementsByClassName('receipt-details');
for (let i = 0; i < allDetails.length; i++) {
allDetails[i].style.display = 'none';
}
detailsDiv.style.display = 'block';
}
}
// פונקציה לסימון/ביטול סימון של כל הקבלות
function toggleAll() {
const selectAll = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('input[type="checkbox"].checkbox');
for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = selectAll.checked;
}
}
// פונקציה לשליחת הטופס
function submitForm() {
const form = document.getElementById('receiptsForm');
const formData = new FormData(form);
// יצירת אובייקט לשליחה
const data = {
receipts: []
};
// מעבר על כל הקבלות
for (let i = 0; i < ${processedReceipts.length}; i++) {
if (formData.get('include_' + i) === 'on') {
data.receipts.push({
index: i,
date: formData.get('date_' + i),
store: formData.get('store_' + i),
total: formData.get('total_' + i),
regMilkTotal: formData.get('regMilk_' + i),
soyMilkTotal: formData.get('soyMilk_' + i),
gatheringTotal: formData.get('gathering_' + i)
});
}
}
// שליחת הנתונים לסקריפט
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onFailure)
.processSelectedReceipts(data);
}
// פונקציה שתרוץ בהצלחה
function onSuccess(message) {
alert(message);
google.script.host.close();
}
// פונקציה שתרוץ בכשלון
function onFailure(error) {
alert('שגיאה: ' + error);
}
</script>
`;
// הוסף את התוכן להודעה
htmlOutput.setContent(htmlContent);
// שמור את נתוני הקבלות בפרופרטיס
for (let i = 0; i < processedReceipts.length; i++) {
PropertiesService.getDocumentProperties().setProperty(`receipt_${i}`, JSON.stringify(processedReceipts[i]));
}
// הצג את החלון המודלי
ui.showModalDialog(htmlOutput, 'סקירת קבלות');
} catch (error) {
Logger.log(`שגיאה בהצגת ממשק סקירה: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה בהצגת ממשק סקירה: ${error}`);
}
}
/**
* פונקציה לעיבוד הקבלות שנבחרו מהממשק המודלי
* @param {Object} data אובייקט עם הקבלות שנבחרו
* @return {string} הודעת הצלחה
*/
function processSelectedReceipts(data) {
try {
const sheet = SpreadsheetApp.getActiveSheet();
const selectedReceipts = data.receipts;
if (!selectedReceipts || !Array.isArray(selectedReceipts) || selectedReceipts.length === 0) {
return 'לא נבחרו קבלות להוספה';
}
let addedCount = 0;
for (const selectedReceipt of selectedReceipts) {
try {
// קבל את הנתונים המלאים של הקבלה
const receiptData = JSON.parse(PropertiesService.getDocumentProperties().getProperty(`receipt_${selectedReceipt.index}`));
if (!receiptData) {
continue;
}
// עדכן את הנתונים לפי הערכים שנערכו בממשק
receiptData.date = parseDate(selectedReceipt.date);
receiptData.store = selectedReceipt.store;
receiptData.total = parseFloat(selectedReceipt.total);
// חשב כמה מהסכום המשולב שייך לכל קטגוריה (שמירה על יחסים)
const origRegTotal = (receiptData.regMilkTotal || 0) +
(receiptData.coffeeTotal || 0) +
(receiptData.disposableTotal || 0);
// אם יש שינוי בסכום הכולל, חשב מחדש את החלוקה
const newRegTotal = parseFloat(selectedReceipt.regMilkTotal);
if (origRegTotal > 0 && Math.abs(origRegTotal - newRegTotal) > 0.01) {
const factor = newRegTotal / origRegTotal;
receiptData.regMilkTotal = (receiptData.regMilkTotal || 0) * factor;
receiptData.coffeeTotal = (receiptData.coffeeTotal || 0) * factor;
receiptData.disposableTotal = (receiptData.disposableTotal || 0) * factor;
}
// הוסף את הקבלה לגיליון
addReceiptToSheet(
sheet,
receiptData,
parseFloat(selectedReceipt.regMilkTotal),
parseFloat(selectedReceipt.soyMilkTotal),
parseFloat(selectedReceipt.gatheringTotal)
);
addedCount++;
} catch (error) {
Logger.log(`שגיאה בהוספת קבלה: ${error}`);
}
}
// אם הוספו קבלות, בדוק חריגה מתקציב
if (addedCount > 0) {
try {
checkBudgetOverflow();
} catch (error) {
Logger.log(`שגיאה בבדיקת חריגה מתקציב: ${error}`);
}
}
// נקה את הפרופרטיס
const props = PropertiesService.getDocumentProperties();
const propKeys = props.getKeys();
for (const key of propKeys) {
if (key.startsWith('receipt_')) {
props.deleteProperty(key);
}
}
return `נוספו ${addedCount} קבלות בהצלחה`;
} catch (error) {
Logger.log(`שגיאה בעיבוד קבלות שנבחרו: ${error}`);
return `שגיאה בעיבוד הקבלות: ${error}`;
}
}
/**
* פונקציה ראשית ליישום מלא של כל התהליך
*/
function main() {
// פונקציה זו משמשת כהדגמה ליישום מלא של התהליך
try {
// 1. הגדרת תקציב גיבוש
setBudgetUI();
// 2. הוספת חודש חדש
addNewMonthUI();
// 3. עיבוד קבלות חדשות
processNewReceiptsUI();
// 4. בדיקת חריגה מתקציב
checkBudgetOverflowUI();
} catch (error) {
Logger.log(`שגיאה בהרצת התהליך המלא: ${error}`);
SpreadsheetApp.getUi().alert(`שגיאה בהרצת התהליך המלא: ${error}`);
}
}
// ייצוא פונקציות לשימוש גלובלי
// חשוב לחשוף את כל הפונקציות שיקראו מממשק המשתמש
const EXPORTED_FUNCTIONS = {
// פונקציות ראשיות
onOpen: onOpen,
processNewReceiptsUI: processNewReceiptsUI,
processReceiptsByMonthUI: processReceiptsByMonthUI,
addNewMonthUI: addNewMonthUI,
setBudgetUI: setBudgetUI,
checkBudgetOverflowUI: checkBudgetOverflowUI,
setupUI: setupUI,
// פונקציות שנקראות מממשק משתמש מודלי
processSelectedReceipts: processSelectedReceipts
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment