Created
May 5, 2025 18:21
-
-
Save uzim4414/f8432095b8f4e617896b8b72be660916 to your computer and use it in GitHub Desktop.
KUPA 1.31 STABE BUT STILL NOT WORKING AS WELL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* קוד משופר לניתוח קבלות באמצעות 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