{"files":[{"id":"302070c6-b81e-49d7-97dd-292f82d397ff","name":"appsscript","type":"json","source":"{\n  \"timeZone\": \"Asia/Yangon\",\n  \"dependencies\": {\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}"},{"id":"0438ee0b-29a0-40ab-8db2-160702701eee","name":"Code","type":"server_js","source":"// \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d CONFIGURATION \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n// --- UPDATE THESE VALUES WITH YOUR DETAILS ---\n\n// 1. Google Drive Folder ID where the weekly sheets will be saved.\nconst FOLDER_ID \u003d \u00271WY4Thfn7-PYs-W1Mp8enLgrskPQR-Saa\u0027;\n\n// 2. MySQL Database Connection Details\nconst DB_HOST \u003d \u0027db-distribution-accounting-do-user-2718740-0.h.db.ondigitalocean.com\u0027;\nconst DB_PORT \u003d 25060;\nconst DB_NAME \u003d \u0027das-db\u0027;\nconst DB_USER \u003d \u0027thansoeaung\u0027;\nconst DB_PASSWORD \u003d \u0027cW8w3EQxFFWZfKBZeWjNbQ\u003d\u003d\u0027;\n\n// 3. Your Full SQL Queries and Target Sheet (Tab) Names\nconst QUERIES_TO_RUN \u003d [\n  {\n    sheetName: \u0027data\u0027,\n    query: `\n    WITH LatestPO AS (\n    SELECT product_id, MAX(id) AS last_detail_id \n    FROM purchase_order_details GROUP BY product_id\n),\nBaseData AS (\n    SELECT \n        p.*,\n        NULLIF(TRIM(SUBSTRING_INDEX(p.description, \u0027;\u0027, 1)), \u0027\u0027) AS Ref_SKU_From_Product,\n        COALESCE(NULLIF(TRIM(p.purchased_sku), \u0027\u0027), p.sku) AS Derived_Purchased_SKU\n    FROM products AS p\n    WHERE p.business_id \u003d \u002784093770-29ad-4e8e-9da1-babe583c0d69\u0027\n      AND p.is_sellable \u003d TRUE AND p.is_active \u003d TRUE\n),\nSupplierSKULookup AS (\n    SELECT \n        bd.id AS product_id,\n        CASE \n            WHEN bd.is_purchasable \u003d TRUE THEN \n                bd.Derived_Purchased_SKU\n            ELSE \n                COALESCE(p_parent.purchased_sku, bd.sku)\n        END AS Supplier_SKU\n    FROM BaseData bd\n    LEFT JOIN products AS p_parent ON p_parent.sku \u003d bd.Derived_Purchased_SKU \n        AND p_parent.business_id \u003d bd.business_id\n),\nSurveyAverages AS (\n    SELECT \n        TRIM(SUBSTRING_INDEX(p_surveyed.description, \u0027;\u0027, 1)) AS Ref_SKU_From_Note,\n        ROUND(AVG(CASE WHEN sg.name \u003d \u0027Retail\u0027 THEN (surv.unit_price / NULLIF(p_surveyed.weight, 0)) * p_ref.weight END), 2) AS Avg_Ref_Price_Retail_30D,\n        ROUND(AVG(CASE WHEN sg.name \u003d \u0027Street\u0027 THEN (surv.unit_price / NULLIF(p_surveyed.weight, 0)) * p_ref.weight END), 2) AS Avg_Ref_Price_Street_30D,\n        ROUND(AVG(CASE WHEN sg.name \u003d \u0027B2B\u0027 THEN (surv.unit_price / NULLIF(p_surveyed.weight, 0)) * p_ref.weight END), 2) AS Avg_Ref_Price_B2B_30D\n    FROM price_surveys AS surv\n    LEFT JOIN products AS p_surveyed ON p_surveyed.id \u003d surv.product_id\n    LEFT JOIN suppliers AS sup ON sup.id \u003d surv.supplier_id\n    LEFT JOIN supplier_groups AS sg ON sup.group_id \u003d sg.id \n    LEFT JOIN products AS p_ref ON p_ref.sku \u003d TRIM(SUBSTRING_INDEX(p_surveyed.description, \u0027;\u0027, 1)) \n        AND p_ref.business_id \u003d surv.business_id\n    WHERE surv.business_id \u003d \u002784093770-29ad-4e8e-9da1-babe583c0d69\u0027\n      AND surv.date \u003e\u003d DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)\n    GROUP BY 1\n),\nPriceCalculation AS (\n    SELECT\n        bd.sku, bd.name AS Product_Name, bd.weight AS Product_Weight, bd.length AS Product_Repacking_Cost, \n        bd.sales_price AS Catalogue_Price, bd.is_sellable AS Sellable, bd.is_purchasable AS Purchasable,\n        sskul.Supplier_SKU, p_selling.sales_price AS Supplier_Price_Raw,\n        (pod.detail_total_amount / NULLIF(pod.detail_qty, 0)) AS Direct_LastPOPrice,\n        (pod_origin.detail_total_amount / NULLIF(pod_origin.detail_qty, 0)) AS Origin_LastPOPrice,\n        plri_corp.unit_price AS Corp_Price_Num, plri_b2b.unit_price AS B2B_Price_Num,\n        p_ref.weight AS Ref_SKU_Weight_Actual,\n        sa.Avg_Ref_Price_Retail_30D, sa.Avg_Ref_Price_Street_30D, sa.Avg_Ref_Price_B2B_30D,\n        COALESCE(pc2.name, pc1.name, pc.name) AS Category_L1,\n        CASE WHEN pc2.name IS NOT NULL THEN pc1.name WHEN pc1.name IS NOT NULL THEN pc.name ELSE NULL END AS Category_L2,\n        CASE WHEN pc2.name IS NOT NULL THEN pc.name ELSE NULL END AS Category_L3,\n        CASE WHEN bd.is_purchasable \u003d TRUE THEN bd.weight ELSE p_origin.weight END AS Purchased_SKU_Weight,\n        bd.Derived_Purchased_SKU AS Purchased_SKU,\n        bd.Ref_SKU_From_Product, p_ref.name AS Ref_SKU_Name_Actual\n    FROM BaseData AS bd\n    JOIN SupplierSKULookup sskul ON sskul.product_id \u003d bd.id\n    LEFT JOIN products AS p_selling ON p_selling.sku \u003d sskul.Supplier_SKU AND p_selling.business_id \u003d \u00277ef7c6c0-b59d-4be9-960c-7fa573038642\u0027\n    LEFT JOIN products AS p_origin ON p_origin.sku \u003d bd.Derived_Purchased_SKU AND p_origin.business_id \u003d bd.business_id\n    LEFT JOIN LatestPO lp ON lp.product_id \u003d bd.id\n    LEFT JOIN purchase_order_details AS pod ON pod.id \u003d lp.last_detail_id\n    LEFT JOIN products AS p_ref ON p_ref.sku \u003d bd.Ref_SKU_From_Product AND p_ref.business_id \u003d bd.business_id\n    LEFT JOIN SurveyAverages sa ON sa.Ref_SKU_From_Note \u003d bd.Ref_SKU_From_Product\n    LEFT JOIN LatestPO lp_origin ON lp_origin.product_id \u003d p_origin.id\n    LEFT JOIN purchase_order_details AS pod_origin ON pod_origin.id \u003d lp_origin.last_detail_id\n    LEFT JOIN product_categories AS pc ON bd.category_id \u003d pc.id\n    LEFT JOIN product_categories AS pc1 ON pc.parent_category_id \u003d pc1.id\n    LEFT JOIN product_categories AS pc2 ON pc1.parent_category_id \u003d pc2.id\n    LEFT JOIN price_lists pl_corp ON pl_corp.name \u003d \u0027Corporate\u0027 AND pl_corp.business_id \u003d bd.business_id\n    LEFT JOIN price_list_rule_items plri_corp ON plri_corp.price_list_id \u003d pl_corp.id AND plri_corp.product_id \u003d bd.id\n    LEFT JOIN price_lists pl_b2b ON pl_b2b.name \u003d \u0027B2B\u0027 AND pl_b2b.business_id \u003d bd.business_id\n    LEFT JOIN price_list_rule_items plri_b2b ON plri_b2b.price_list_id \u003d pl_b2b.id AND plri_b2b.product_id \u003d bd.id\n),\nFinalCalculations AS (\n    SELECT \n        pc.*,\n        ROUND(Product_Weight / NULLIF(Purchased_SKU_Weight, 0), 4) AS Purchased_Conversion_Rate,\n        ROUND(Supplier_Price_Raw * (Product_Weight / NULLIF(Purchased_SKU_Weight, 0)), 2) AS Calculated_Purchasing_Price,\n        CASE \n            WHEN Purchasable \u003d TRUE THEN Direct_LastPOPrice\n            ELSE ROUND((Product_Weight / NULLIF(Purchased_SKU_Weight, 0)) * Origin_LastPOPrice, 2)\n        END AS Calculated_Last_PO_Price,\n        ROUND(Product_Weight * 300, 2) AS Delivery_Cost\n    FROM PriceCalculation pc\n)\nSELECT\n    SKU, Product_Name, Category_L1, Category_L2, Category_L3,\n    Sellable, Purchasable,\n    Purchased_SKU, Supplier_SKU,\n    Product_Weight, Purchased_SKU_Weight, Purchased_Conversion_Rate,\n    \n    ROUND(Supplier_Price_Raw, 2) AS Supplier_Price,\n    Calculated_Purchasing_Price AS Purchasing_Price,\n    Calculated_Last_PO_Price AS Last_PO_Price,\n    \n    CASE \n        WHEN Calculated_Last_PO_Price IS NULL THEN \u0027No PO price known\u0027\n        WHEN Calculated_Purchasing_Price IS NULL THEN \u0027No purchasing price\u0027\n        WHEN Calculated_Purchasing_Price \u003d \u00271\u0027 THEN \u0027No purchasing price\u0027\n        WHEN Calculated_Purchasing_Price \u003e Calculated_Last_PO_Price THEN \u0027Purchasing price increased\u0027\n        WHEN LEAST(Catalogue_Price, Corp_Price_Num, B2B_Price_Num) \u003c Total_Cost THEN \u0027Negative margin\u0027\n        WHEN Sellable \u003d TRUE AND Catalogue_Price \u003d \u00271\u0027 THEN \u0027Catalogue price not configured\u0027\n        ELSE \u0027No alert\u0027\n    END AS Price_Alert,\n    \n    Calculated_Purchasing_Price AS Final_Purchasing_Price,\n    Product_Repacking_Cost, Delivery_Cost, Total_Cost,\n    \n    Catalogue_Price AS Cat_Price,\n    ROUND((Catalogue_Price - Total_Cost) / NULLIF(Catalogue_Price, 0), 4) AS Cat_Margin_Percent,\n    COALESCE(CAST(Corp_Price_Num AS CHAR), \u0027NA\u0027) AS Corp_Price,\n    ROUND((Corp_Price_Num - Total_Cost) / NULLIF(Corp_Price_Num, 0), 4) AS Corp_Margin_Percent,\n    COALESCE(CAST(B2B_Price_Num AS CHAR), \u0027NA\u0027) AS B2B_Price,\n    ROUND((B2B_Price_Num - Total_Cost) / NULLIF(B2B_Price_Num, 0), 4) AS B2B_Margin_Percent,\n\n    -- Final data for easy copy paste\n    Category_L3 AS Category,\n    SKU AS SKU_Copy,\n    Product_Name AS Product,\n    Catalogue_Price AS Catalogue,\n    COALESCE(CAST(Corp_Price_Num AS CHAR), \u0027NA\u0027) AS Corporate,\n    ROUND((Catalogue_Price - Corp_Price_Num) / NULLIF(Catalogue_Price, 0), 4) AS Corporate_discount,\n    COALESCE(CAST(B2B_Price_Num AS CHAR), \u0027NA\u0027) AS B2B,\n    ROUND((Catalogue_Price - B2B_Price_Num) / NULLIF(Catalogue_Price, 0), 4) AS B2B_discount\n\nFROM (\n    SELECT f.*, \n    ROUND(COALESCE(Calculated_Purchasing_Price, 0) + COALESCE(Product_Repacking_Cost, 0) + COALESCE(Delivery_Cost, 0), 2) AS Total_Cost\n    FROM FinalCalculations f\n) Final\nORDER BY Category_L3 ASC, Product_Name ASC, Product_Weight ASC;\n    `\n  }\n];\n\n// 4. Email Notification Configuration\n\nconst SPREADSHEET_ID \u003d \"17BxuaTjxvYqDytbMU7VRceNIqVYUqo0CD_e9LgUhz_o\"; \nconst TAB_ID \u003d 0; // 0 is typically the very first tab in a sheet, so this is fine!\nconst ss \u003d SpreadsheetApp.openById(SPREADSHEET_ID);\nvar EMAIL_RECIPIENTS \u003d \"\";\n\n// Fixed: changed ss.getSheet() to ss.getSheets()\nconst email_sheet \u003d ss.getSheets().find(s \u003d\u003e s.getSheetId() \u003d\u003d\u003d TAB_ID);\n\nif (email_sheet) {\n  EMAIL_RECIPIENTS \u003d email_sheet.getRange(\"J2\").getValue();\n  Logger.log(EMAIL_RECIPIENTS);\n} else {\n  Logger.log(\"Error: Could not find a tab with ID \" + TAB_ID);\n}\n\n\n// \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d END OF CONFIGURATION \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n\n\n/**\n * The main function to be scheduled weekly.\n */\nfunction mainWeeklyDataPull() {\n  try {\n    // --- 1. Calculate Dates and Filename ---\n    const today \u003d new Date();\n    // To get the date of the most recent Sunday for the filename.\n    const lastSunday \u003d new Date(today.setDate(today.getDate() - today.getDay()));\n    const timezone \u003d Session.getScriptTimeZone();\n    const baseName \u003d Utilities.formatDate(lastSunday, timezone, \"\u0027W\u0027ww/yy \u0027M-Kitchen Product Pricelist\u0027\");\n\n    // --- 2. Create the new Spreadsheet ---\n    const parentFolder \u003d DriveApp.getFolderById(FOLDER_ID);\n    const newSheet \u003d SpreadsheetApp.create(baseName);\n    const tempFile \u003d DriveApp.getFileById(newSheet.getId());\n    parentFolder.addFile(tempFile);\n    DriveApp.getRootFolder().removeFile(tempFile);\n    \n    Logger.log(`Successfully created new sheet: \"${baseName}\" (ID: ${newSheet.getId()})`);\n\n    // --- 3. Establish Database Connection ---\n    const dbUrl \u003d \"jdbc:mysql://\" + DB_HOST + \":\" + DB_PORT + \"/\" + DB_NAME;\n    const conn \u003d Jdbc.getConnection(dbUrl, DB_USER, DB_PASSWORD);\n    Logger.log(\"Successfully connected to the database.\");\n\n    // --- 4. Loop Through Each Query and Populate Sheets ---\n    QUERIES_TO_RUN.forEach(item \u003d\u003e {\n      Logger.log(`Fetching data for sheet: ${item.sheetName}`);\n      \n      const data \u003d fetchData(conn, item.query);\n      \n      if (data \u0026\u0026 data.length \u003e 1) { \n        Logger.log(`Retrieved ${data.length - 1} rows of data.`);\n        writeDataToSheet(newSheet, item.sheetName, data);\n      } else {\n        Logger.log(`No data found for sheet: ${item.sheetName}.`);\n        writeDataToSheet(newSheet, item.sheetName, [[\"No data found for the provided query\"]]);\n      }\n    });\n\n    // --- 5. Clean up Default Tabs ---\n    const defaultSheet \u003d newSheet.getSheetByName(\u0027Sheet1\u0027);\n    if (defaultSheet) {\n      newSheet.deleteSheet(defaultSheet);\n    }\n    conn.close();\n    Logger.log(\"Process complete. Database connection closed.\");\n\n    // --- 6. Export Target Tab to CSV and Send Email ---\n    Logger.log(\"Preparing email notification...\");\n    sendCsvEmail(newSheet, QUERIES_TO_RUN[0].sheetName, baseName);\n    \n  } catch (e) {\n    Logger.log(`An error occurred: ${e.message} \\nStack: ${e.stack}`);\n  }\n}\n\n/**\n * Fetches data from the database using a given query.\n */\nfunction fetchData(conn, query) {\n  const stmt \u003d conn.createStatement();\n  const rs \u003d stmt.executeQuery(query);\n  const numCols \u003d rs.getMetaData().getColumnCount();\n  const headers \u003d [];\n  for (let col \u003d 1; col \u003c\u003d numCols; col++) {\n    headers.push(rs.getMetaData().getColumnLabel(col));\n  }\n  \n  const data \u003d [headers];\n  \n  while (rs.next()) {\n    const row \u003d [];\n    for (let col \u003d 1; col \u003c\u003d numCols; col++) {\n      row.push(rs.getString(col));\n    }\n    data.push(row);\n  }\n  \n  rs.close();\n  stmt.close();\n  return data;\n}\n\n/**\n * Writes a 2D array of data to a specified sheet.\n */\nfunction writeDataToSheet(ss, sheetName, data) {\n  let sheet \u003d ss.getSheetByName(sheetName);\n  if (!sheet) {\n    sheet \u003d ss.insertSheet(sheetName);\n  }\n  sheet.clear();\n  \n  if (data \u0026\u0026 data.length \u003e 0) {\n    const range \u003d sheet.getRange(1, 1, data.length, data[0].length);\n    range.setValues(data);\n    sheet.setFrozenRows(1);\n    Logger.log(`Wrote data to sheet: \"${sheetName}\"`);\n  }\n}\n\n/**\n * Converts a specific sheet tab to a CSV blob and emails it to the team.\n * @param {Spreadsheet} ss The spreadsheet object.\n * @param {string} targetSheetName The specific tab name to convert.\n * @param {string} baseName The base filename used for subject and attachment.\n */\nfunction sendCsvEmail(ss, targetSheetName, baseName) {\n  const targetSheet \u003d ss.getSheetByName(targetSheetName);\n  if (!targetSheet) {\n    Logger.log(`Error: Could not find tab \"${targetSheetName}\" to export to CSV.`);\n    return;\n  }\n\n  // Get data from the sheet and convert to a CSV string format\n  const data \u003d targetSheet.getDataRange().getValues();\n  let csvString \u003d \"\";\n  \n  for (let i \u003d 0; i \u003c data.length; i++) {\n    const row \u003d data[i].map(cell \u003d\u003e {\n      // Escape inner quotes and wrap cells containing commas/newlines in quotes\n      let cellStr \u003d cell.toString();\n      if (cellStr.includes(\",\") || cellStr.includes(\"\\n\") || cellStr.includes(\u0027\"\u0027)) {\n        cellStr \u003d \u0027\"\u0027 + cellStr.replace(/\"/g, \u0027\"\"\u0027) + \u0027\"\u0027;\n      }\n      return cellStr;\n    });\n    csvString +\u003d row.join(\",\") + \"\\r\\n\";\n  }\n\n  // Convert string to an email attachment blob\n  const attachmentFilename \u003d `${baseName}.csv`;\n  const csvBlob \u003d Utilities.newBlob(csvString, \u0027text/csv\u0027, attachmentFilename);\n\n  // Draft and send email\n  const subject \u003d `Weekly Data Update: ${baseName}`;\n  const body \u003d `Hello Team,\\n\\nPlease find attached the weekly automated CSV data snapshot for: ${baseName}.\\n\\nThis is an automated system email.`;\n\n  MailApp.sendEmail({\n    to: EMAIL_RECIPIENTS,\n    subject: subject,\n    body: body,\n    attachments: [csvBlob]\n  });\n\n  Logger.log(`Notification email successfully sent to: ${EMAIL_RECIPIENTS}`);\n}\n\n/**\n * ONE-TIME SETUP: Run this function manually ONCE to create the weekly trigger.\n */\nfunction createWeeklyTrigger() {\n  const allTriggers \u003d ScriptApp.getProjectTriggers();\n  for (const trigger of allTriggers) {\n    if (trigger.getHandlerFunction() \u003d\u003d\u003d \u0027mainWeeklyDataPull\u0027) {\n      ScriptApp.deleteTrigger(trigger);\n    }\n  }\n  \n  ScriptApp.newTrigger(\u0027mainWeeklyDataPull\u0027)\n    .timeBased()\n    .onWeekDay(ScriptApp.WeekDay.MONDAY)\n    .atHour(4) // Triggers between 4:00 AM and 5:00 AM every Monday\n    .create();\n    \n  Logger.log(\u0027Successfully created the weekly trigger for Monday mornings.\u0027);\n}"}]}