Checks your pharmacy inventory sheet every morning and emails the medicines running low or near expiry, marking each in the sheet.

UsesGmailGoogle Sheets

Low stock and expiry alerts for your pharmacy

Every morning at 09:00, the inventory tab of your spreadsheet is read and each medicine is checked two ways: its quantity against its reorder level, and its expiry date against today. Everything at or below the reorder level, already expired, or expiring within your warning window goes into one email to the pharmacist, split into what is new today and what is still outstanding from earlier runs.

Each alert is then written into the sheet's Alert Status column with the date it was first raised, and cleared again once the medicine has been restocked or replaced, so the sheet always shows the current state. Only that one cell of a row is written. A row whose quantity or expiry date cannot be read is listed in the same email rather than skipped, and a blank tab or a missing column stops the run with a message saying what to fix. When every medicine is stocked and in date, nothing is sent.

Before the first run you need a spreadsheet tab with a header row naming the medicine, quantity and expiry date columns, a column headed Alert Status, an optional Reorder Level column, and the pharmacist's email address.

ScheduleRun every day at 09:00.Run every day at 09:00.
ValuesSet inventorySheetUrl to blank.Set inventorySheetUrl to blank.
ValuesSet inventoryTab to Inventory.Set inventoryTab to Inventory.
ValuesSet pharmacistEmail to blank.Set pharmacistEmail to blank.
ValuesSet lowStockThreshold to 10.Set lowStockThreshold to 10.
ValuesSet expiryWarningDays to 30.Set expiryWarningDays to 30.
ValuesSet datesAreDayFirst to false.Set datesAreDayFirst to false.
Google SheetsGoogle SheetsRead the rows of inventorySheetUrl. Save it as inventory.Read the rows of inventorySheetUrl. Save it as inventory.
ValuesSet rows to inventory.rows.Set rows to inventory.rows.
ValuesSet headers to inventory.headers.Set headers to inventory.headers.
Decisions & LoopsIf rows is emptyIf rows is empty
Yes
Decisions & LoopsStop and report a problem: The tab '@inventoryTab' has no medicines under its header row. Check the tab name in the inventoryTab step and that the inventory starts on row 2, then run it again.Stop and report a problem: The tab '@inventoryTab' has no medicines under its header row. Check the tab name in the inventoryTab step and that the inventory starts on row 2, then run it again.
No
No steps
CodeRun Python code on rows, headers, today, low_stock_threshold, expiry_warning_days, and dates_are_day_first and save it as analysis.Run Python code on rows, headers, today, low_stock_threshold, expiry_warning_days, and dates_are_day_first and save it as analysis.
Decisions & LoopsIf analysis.error isn't emptyIf analysis.error isn't empty
Yes
Decisions & LoopsStop and report a problem: analysis.error.Stop and report a problem: analysis.error.
No
No steps
Decisions & LoopsIf analysis.has_alerts is yesIf analysis.has_alerts is yes
Yes
GmailGmailSend an email to pharmacistEmail.Send an email to pharmacistEmail. Subject: analysis.email_subjectanalysis.email_body
No
No steps
ValuesSet failedUpdates to an empty list.Set failedUpdates to an empty list.
Decisions & LoopsFor each update in analysis.updates:For each update in analysis.updates:
Decisions & LoopsIf a step below fails, save the error as writeFailedIf a step below fails, save the error as writeFailed
Try
Google SheetsGoogle SheetsOverwrite row update.row_number of inventoryTab in inventorySheetUrl.Overwrite row update.row_number of inventoryTab in inventorySheetUrl.
On error
ListsAdd row update.row_number (update.medicine): writeFailed.message to the end of failedUpdates and save it as failedUpdates.Add row update.row_number (update.medicine): writeFailed.message to the end of failedUpdates and save it as failedUpdates.
Decisions & LoopsIf failedUpdates isn't emptyIf failedUpdates isn't empty
Yes
Decisions & LoopsStop and report a problem: The Alert Status column could not be updated for failedUpdates joined by ; . Any alert email due today was already sent; these medicines will be reported as new again on the next run.Stop and report a problem: The Alert Status column could not be updated for failedUpdates joined by ; . Any alert email due today was already sent; these medicines will be reported as new again on the next run.
No
No steps
Decisions & LoopsIf analysis.has_alerts is yesIf analysis.has_alerts is yes
Yes
Decisions & LoopsFinish the run: Sent '@analysis.email_subject' to pharmacistEmail and updated the Alert Status of the length of analysis.updates rows.Finish the run: Sent '@analysis.email_subject' to pharmacistEmail and updated the Alert Status of the length of analysis.updates rows.
No
Decisions & LoopsFinish the run: All medicines checked (analysis.checked) are stocked and in date. Nothing was sent.Finish the run: All medicines checked (analysis.checked) are stocked and in date. Nothing was sent.