Każda firma B2B zna ten problem: wystawiasz fakturę z 14-dniowym terminem płatności, a po trzech tygodniach okazuje się, że klient "nie zauważył maila". Dzwonisz, piszesz, przepisz się przez CRM – a mogłeś to zautomatyzować jednym skryptem w arkuszu, który już masz.
W tym artykule buduję kompletny system przypomnień o przeterminowanych fakturach oparty na Google Sheets i Apps Script. Bez płatnych narzędzi, bez integracji z zewnętrznym CRM, bez kodowania od zera – gotowy skrypt do skopiowania, który codziennie rano sprawdza Twój arkusz faktur i wysyła maila, gdy któryś termin płatności minął.
Pokazuję dwa warianty: przypomnienie wewnętrzne (zbiorczy mail do Ciebie z listą zaległości) i przypomnienie do klienta (indywidualny mail z grzeczną prośbą o płatność). Oba z szablonami wiadomości, obsługą powtórnych przypomnień i logowaniem historii w arkuszu.
Struktura arkusza faktur
Zanim napiszesz jedną linijkę kodu, potrzebujesz arkusza z odpowiednią strukturą. Oto minimalne kolumny, które musi mieć Twój arkusz faktur, żeby skrypt mógł z niego korzystać:
| Kolumna | Nazwa w arkuszu | Format | Przykład |
|---|---|---|---|
| A | Numer faktury | Tekst | FV/2026/05/001 |
| B | Kontrahent | Tekst | Firma XYZ Sp. z o.o. |
| C | Email kontrahenta | Tekst | jan@firmaxyz.pl |
| D | Kwota brutto | Liczba (PLN) | 12 300,00 zł |
| E | Termin płatności | Data | 2026-05-10 |
| F | Status | Tekst | Nieopłacona / Zapłacona / Przypomniano |
| G | Data przypomnienia | Data | 2026-05-15 |
Kolumny F (Status) i G (Data przypomnienia) są kluczowe dla logiki skryptu. Status pozwala odfiltrować zapłacone faktury, a data przypomnienia zapobiega wysyłaniu maili codziennie dla tej samej faktury – skrypt wysyła przypomnienie raz na 7 dni.
Wariant 1: zbiorczy mail do Ciebie
Prostszy wariant – skrypt sprawdza arkusz raz dziennie i wysyła jeden zbiorczy mail na Twój adres z listą wszystkich przeterminowanych faktur. Nie kontaktuje się z klientami, tylko daje Ci poranną listę zaległości.
/**
* Wysyła zbiorczy mail z listą przeterminowanych faktur.
* Uruchom ręcznie lub ustaw trigger dzienny (8:00).
*/
function sprawdzPlatnosciZbiorczy() {
var arkusz = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('Faktury');
var dane = arkusz.getDataRange().getValues();
var dzis = new Date();
dzis.setHours(0, 0, 0, 0);
var zaleglosci = [];
var sumaZaleglosci = 0;
// Pomijamy wiersz nagłówkowy (i = 0)
for (var i = 1; i < dane.length; i++) {
var status = dane[i][5]; // kolumna F
var termin = new Date(dane[i][4]); // kolumna E
termin.setHours(0, 0, 0, 0);
// Pomijamy zapłacone
if (status === 'Zapłacona') continue;
// Sprawdzamy czy termin minął
if (termin < dzis) {
var dniSpoznienia = Math.floor(
(dzis - termin) / (1000 * 60 * 60 * 24)
);
zaleglosci.push({
numer: dane[i][0],
kontrahent: dane[i][1],
kwota: dane[i][3],
termin: Utilities.formatDate(termin, 'Europe/Warsaw', 'dd.MM.yyyy'),
dni: dniSpoznienia
});
sumaZaleglosci += dane[i][3];
}
}
if (zaleglosci.length === 0) {
Logger.log('Brak przeterminowanych faktur. Miłego dnia!');
return;
}
// Buduj treść maila
var tresc = '<h2>Przeterminowane faktury – ' +
Utilities.formatDate(dzis, 'Europe/Warsaw', 'dd.MM.yyyy') +
'</h2>';
tresc += '<p>Liczba zaległych faktur: <strong>' +
zaleglosci.length + '</strong> | Łączna kwota: <strong>' +
sumaZaleglosci.toFixed(2) + ' zł</strong></p>';
tresc += '<table border="1" cellpadding="8" cellspacing="0" ' +
'style="border-collapse:collapse;font-family:Arial,sans-serif">';
tresc += '<tr style="background:#f0f0f0">' +
'<th>Numer</th><th>Kontrahent</th>' +
'<th>Kwota</th><th>Termin</th><th>Dni spóźnienia</th></tr>';
for (var j = 0; j < zaleglosci.length; j++) {
var z = zaleglosci[j];
var kolor = z.dni > 30 ? '#ffe0e0' : (z.dni > 14 ? '#fff3cd' : '#ffffff');
tresc += '<tr style="background:' + kolor + '">' +
'<td>' + z.numer + '</td>' +
'<td>' + z.kontrahent + '</td>' +
'<td style="text-align:right">' + z.kwota.toFixed(2) + ' zł</td>' +
'<td>' + z.termin + '</td>' +
'<td style="text-align:center;font-weight:bold">' + z.dni + '</td>' +
'</tr>';
}
tresc += '</table>';
GmailApp.sendEmail(
Session.getActiveUser().getEmail(), // do Ciebie
'⚠️ ' + zaleglosci.length + ' przeterminowanych faktur (' +
sumaZaleglosci.toFixed(2) + ' zł)',
'', // plaintext fallback
{htmlBody: tresc}
);
Logger.log('Wysłano zestawienie: ' + zaleglosci.length + ' zaległości.');
}
Skrypt robi trzy rzeczy:
- Iteruje po wierszach arkusza "Faktury", pomijając zapłacone (status = "Zapłacona").
- Dla każdej faktury, której termin płatności jest wcześniejszy niż dziś, oblicza liczbę dni spóźnienia i dodaje ją do listy zaległości.
- Buduje tabelę HTML z kolorowym kodowaniem (czerwone > 30 dni, żółte > 14 dni) i wysyła ją na Twój adres email.
Jeśli nie ma żadnych zaległości – skrypt nie wysyła maila. Żadnego spamu o "wszystko OK".
Wariant 2: indywidualne maile do klientów
Bardziej zaawansowany wariant – skrypt wysyła grzeczne przypomnienie bezpośrednio na adres email kontrahenta. Każdy klient dostaje spersonalizowaną wiadomość z numerem faktury, kwotą i terminem. Skrypt loguje datę przypomnienia w arkuszu i nie wysyła ponownego maila przez 7 dni.
/**
* Wysyła indywidualne przypomnienia do klientów
* o przeterminowanych fakturach.
*/
function wyslijPrzypomnieniaDlaKlientow() {
var arkusz = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('Faktury');
var dane = arkusz.getDataRange().getValues();
var dzis = new Date();
dzis.setHours(0, 0, 0, 0);
var wyslano = 0;
for (var i = 1; i < dane.length; i++) {
var status = dane[i][5]; // F: Status
var termin = new Date(dane[i][4]); // E: Termin
var dataPrzypomnienia = dane[i][6]; // G: Data przypomnienia
termin.setHours(0, 0, 0, 0);
// Pomijamy zapłacone
if (status === 'Zapłacona') continue;
// Sprawdzamy czy termin minął
if (termin >= dzis) continue;
// Sprawdzamy czy przypomnienie było wysłane < 7 dni temu
if (dataPrzypomnienia) {
var ostatnie = new Date(dataPrzypomnienia);
ostatnie.setHours(0, 0, 0, 0);
var dniOdOstatniego = Math.floor(
(dzis - ostatnie) / (1000 * 60 * 60 * 24)
);
if (dniOdOstatniego < 7) continue;
}
var email = dane[i][2]; // C: Email
var kontrahent = dane[i][1]; // B: Kontrahent
var numer = dane[i][0]; // A: Numer faktury
var kwota = dane[i][3]; // D: Kwota
if (!email) continue; // brak adresu – pomijamy
var dniSpoznienia = Math.floor(
(dzis - termin) / (1000 * 60 * 60 * 24)
);
// Wybierz szablon w zależności od dni spóźnienia
var temat, tresc;
if (dniSpoznienia <= 7) {
temat = 'Przypomnienie o płatności – ' + numer;
tresc = szablonPierwsze(kontrahent, numer, kwota, termin);
} else if (dniSpoznienia <= 21) {
temat = 'Drugie przypomnienie – faktura ' + numer;
tresc = szablonDrugie(kontrahent, numer, kwota, termin, dniSpoznienia);
} else {
temat = 'Pilne – zaległa płatność ' + numer;
tresc = szablonTrzecie(kontrahent, numer, kwota, termin, dniSpoznienia);
}
GmailApp.sendEmail(email, temat, '', {htmlBody: tresc});
// Zapisz datę i status w arkuszu
arkusz.getRange(i + 1, 6).setValue('Przypomniano');
arkusz.getRange(i + 1, 7).setValue(
Utilities.formatDate(dzis, 'Europe/Warsaw', 'yyyy-MM-dd')
);
wyslano++;
Utilities.sleep(500); // pauza między mailami
}
Logger.log('Wysłano ' + wyslano + ' przypomnień.');
}
Kluczowa logika: skrypt sprawdza kolumnę G (Data przypomnienia) i jeśli od ostatniego maila minęło mniej niż 7 dni – pomija fakturę. Dzięki temu klient nie dostaje maila codziennie, tylko raz w tygodniu. Po wysłaniu skrypt aktualizuje status na "Przypomniano" i zapisuje dzisiejszą datę.
Szablony wiadomości (kopiuj i wklej)
Ton maila ma znaczenie. Pierwsze przypomnienie powinno być grzeczne i zakładać dobrą wolę. Drugie – rzeczowe. Trzecie – stanowcze. Poniżej trzy funkcje-szablony do wklejenia pod główny skrypt:
/** Pierwsze przypomnienie (1–7 dni po terminie) */
function szablonPierwsze(kontrahent, numer, kwota, termin) {
var data = Utilities.formatDate(termin, 'Europe/Warsaw', 'dd.MM.yyyy');
return '<div style="font-family:Arial,sans-serif;max-width:600px">'
+ '<p>Dzień dobry,</p>'
+ '<p>uprzejmie przypominam o płatności za fakturę '
+ '<strong>' + numer + '</strong> na kwotę '
+ '<strong>' + kwota.toFixed(2) + ' zł</strong>, '
+ 'której termin płatności upłynął ' + data + '.</p>'
+ '<p>Jeśli płatność została już zrealizowana – proszę '
+ 'zignorować tę wiadomość. Czasem przelewy krzyżują '
+ 'się z przypomnieniami.</p>'
+ '<p>W razie pytań – proszę o kontakt.</p>'
+ '<p>Pozdrawiam,<br/>Mikołaj Szeląg</p>'
+ '</div>';
}
/** Drugie przypomnienie (8–21 dni po terminie) */
function szablonDrugie(kontrahent, numer, kwota, termin, dni) {
var data = Utilities.formatDate(termin, 'Europe/Warsaw', 'dd.MM.yyyy');
return '<div style="font-family:Arial,sans-serif;max-width:600px">'
+ '<p>Dzień dobry,</p>'
+ '<p>wracam z przypomnieniem o niezapłaconej fakturze '
+ '<strong>' + numer + '</strong> na kwotę '
+ '<strong>' + kwota.toFixed(2) + ' zł</strong>. '
+ 'Termin płatności (' + data + ') upłynął ' + dni + ' dni temu.</p>'
+ '<p>Proszę o informację, kiedy mogę spodziewać się '
+ 'realizacji płatności.</p>'
+ '<p>Pozdrawiam,<br/>Mikołaj Szeląg</p>'
+ '</div>';
}
/** Trzecie przypomnienie (22+ dni po terminie) */
function szablonTrzecie(kontrahent, numer, kwota, termin, dni) {
var data = Utilities.formatDate(termin, 'Europe/Warsaw', 'dd.MM.yyyy');
return '<div style="font-family:Arial,sans-serif;max-width:600px">'
+ '<p>Dzień dobry,</p>'
+ '<p>informuję, że faktura <strong>' + numer + '</strong> '
+ 'na kwotę <strong>' + kwota.toFixed(2) + ' zł</strong> '
+ 'pozostaje nieopłacona od ' + dni + ' dni '
+ '(termin: ' + data + ').</p>'
+ '<p>Proszę o niezwłoczne uregulowanie należności. '
+ 'W przypadku braku płatności w ciągu 7 dni roboczych '
+ 'będę zmuszony podjąć dalsze kroki windykacyjne.</p>'
+ '<p>Pozdrawiam,<br/>Mikołaj Szeląg</p>'
+ '</div>';
}
Konfiguracja triggera – codziennie o 8:00
Skrypt uruchomiony ręcznie działa, ale sens automatyzacji polega na tym, żeby działał sam. Konfiguracja triggera:
- W edytorze Apps Script kliknij ikonę zegara (Wyzwalacze) po lewej stronie.
- Kliknij + Dodaj wyzwalacz.
- Ustaw parametry:
- Funkcja:
sprawdzPlatnosciZbiorczy(wariant 1) lubwyslijPrzypomnieniaDlaKlientow(wariant 2) - Źródło zdarzenia: Czasowy
- Typ: Wyzwalacz dzienny
- Pora dnia: 8:00 – 9:00
- Funkcja:
- Kliknij Zapisz i zaakceptuj uprawnienia.
Dlaczego 8:00? Maile wysyłane o tej porze trafiają na górę skrzynki – klient widzi je zaraz po włączeniu komputera. Maile wysłane w nocy lub w weekend giną w szumie. Jeśli Twoi klienci pracują w innej strefie czasowej – dostosuj godzinę.
Możesz też ustawić oba warianty jednocześnie: zbiorczy mail do siebie o 7:30 (żebyś wiedział czego się spodziewać) i maile do klientów o 9:00.
Eskalacja: pierwsze, drugie i trzecie przypomnienie
System eskalacji działa na podstawie liczby dni po terminie – nie na podstawie liczby wysłanych maili. To prostsze w implementacji i bardziej przewidywalne:
| Poziom | Dni po terminie | Ton | Częstotliwość |
|---|---|---|---|
| Pierwsze | 1–7 dni | Grzeczne przypomnienie, zakładające dobrą wolę | Jednorazowo |
| Drugie | 8–21 dni | Rzeczowe, pytanie o termin zapłaty | Co 7 dni |
| Trzecie | 22+ dni | Stanowcze, zapowiedź windykacji | Co 7 dni (max 3 razy) |
Jeśli chcesz dodać limit powtórzeń (np. max 3 trzecie przypomnienia), dodaj kolumnę H ("Liczba przypomnień") i inkrementuj ją w skrypcie po każdym wysłanym mailu. Przy wartości > 3 – skrypt pomija fakturę i loguje ją jako "Do windykacji ręcznej".
Limity Gmaila i dobre praktyki
Limity wysyłki:
- Darmowe konto Gmail: 100 maili dziennie przez Apps Script.
- Google Workspace (firmowe): 1 500 maili dziennie.
- Każde wywołanie
GmailApp.sendEmail()= 1 mail. CC/BCC nie zwiększają licznika, ale każdy osobnysendEmail()tak.
Przy typowej firmie B2B (20–50 aktywnych faktur) nigdy nie zbliżysz się do tych limitów. Problem pojawia się dopiero przy masowej wysyłce – ale wtedy powinieneś używać dedykowanego narzędzia do email marketingu, nie Apps Script.
Dobre praktyki:
- Zawsze testuj na sobie. Przed włączeniem wariantu "do klienta" zmień adres odbiorcy na swój i sprawdź jak wygląda mail w skrzynce. Sprawdź czy formatowanie HTML się nie sypie w różnych klientach pocztowych.
- Dodaj stopkę z danymi firmy. Profesjonalne przypomnienie powinno zawierać nazwę firmy, NIP i dane kontaktowe. Dodaj je do szablonów.
- Loguj wszystko. Kolumna G (Data przypomnienia) to minimum. Rozważ dodanie kolumny "Historia" z pełnym logiem (np. "2026-05-10: 1. przypomnienie, 2026-05-17: 2. przypomnienie").
- Nie wysyłaj w weekendy i święta. Dodaj na początku skryptu sprawdzenie dnia tygodnia:
if (dzis.getDay() === 0 || dzis.getDay() === 6) return;
Bonus: dashboard zaległości w Data Studio
Skoro masz już arkusz z fakturami, statusami i datami przypomnień – podłącz go do Data Studio i zbuduj dashboard, który pokaże sytuację płatniczą na jeden rzut oka:
- KPI karty: Łączna kwota zaległości, liczba przeterminowanych faktur, średnie dni spóźnienia, % faktur zapłaconych w terminie.
- Wykres kołowy: Rozkład statusów (Zapłacona / Nieopłacona / Przypomniano).
- Tabela: Lista przeterminowanych faktur posortowana po dniach spóźnienia (malejąco) – najgorsze przypadki na górze.
- Wykres liniowy: Trend zaległości w czasie – czy sytuacja się poprawia czy pogarsza?
Formuła na % zapłaconych w terminie (pole obliczeniowe w Data Studio):
SUM(CASE WHEN Status = "Zapłacona" THEN 1 ELSE 0 END)
/ COUNT(Numer_faktury)
Formuła na średnie dni spóźnienia (tylko dla nieopłaconych):
SUM(
CASE WHEN Status != "Zapłacona"
THEN DATE_DIFF(TODAY(), Termin_platnosci)
ELSE 0
END
)
/
SUM(CASE WHEN Status != "Zapłacona" THEN 1 ELSE 0 END)
Taki dashboard, wyświetlany na ekranie w biurze, potrafi zmotywować zespół do szybszego reagowania na zaległości. Widoczna kwota zaległości ma zupełnie inną moc niż ukryty wiersz w arkuszu.