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.

💡 Wskazówka: Jeśli prowadzisz już arkusz faktur w innym formacie – nie przerabiaj go od zera. Dodaj brakujące kolumny (Status, Data przypomnienia) i dostosuj numery kolumn w skrypcie. Poniżej pokazuję, które linie zmienić.

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:

  1. Iteruje po wierszach arkusza "Faktury", pomijając zapłacone (status = "Zapłacona").
  2. 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.
  3. 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>';
}
⚠️ Uwaga prawna: Automatyczne wysyłanie przypomnień do klientów wymaga podstawy prawnej w rozumieniu RODO. Najczęściej jest to art. 6 ust. 1 lit. b (wykonanie umowy) lub lit. f (prawnie uzasadniony interes – dochodzenie roszczeń). Skonsultuj się z prawnikiem jeśli masz wątpliwości. Skrypt to narzędzie – odpowiedzialność za treść i częstotliwość przypomnień leży po Twojej stronie.

Konfiguracja triggera – codziennie o 8:00

Skrypt uruchomiony ręcznie działa, ale sens automatyzacji polega na tym, żeby działał sam. Konfiguracja triggera:

  1. W edytorze Apps Script kliknij ikonę zegara (Wyzwalacze) po lewej stronie.
  2. Kliknij + Dodaj wyzwalacz.
  3. Ustaw parametry:
    • Funkcja: sprawdzPlatnosciZbiorczy (wariant 1) lub wyslijPrzypomnieniaDlaKlientow (wariant 2)
    • Źródło zdarzenia: Czasowy
    • Typ: Wyzwalacz dzienny
    • Pora dnia: 8:00 – 9:00
  4. 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 osobny sendEmail() 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.