Co miesiąc ten sam rytuał. Otwierasz arkusz, sprawdzasz, kto ma abonament, wystawiasz faktury. manualnie. Jedna po drugiej. Czasem zapomnisz, czasem pomylisz datę. Brzmi znajomo? U nas tak było. Różni klienci, różne usługi, różne daty rozpoczęcia usług. Każdego miesiąca traciliśmy cenny czas na mechaniczne czynności. Postanowiłem to zmienić i stworzyłem prosty workflow w n8n, który całkowicie wyeliminował ten problem.
W tym poradniku przeprowadzę Cię przez proces tworzenia automatyzacji, która sprawdzi, komu należy wystawić fakturę, wygeneruje ją automatycznie, wyśle do klienta profesjonalnego maila z linkiem do płatności online i zaktualizuje arkusz z historią faktur. Brzmi skomplikowanie? Spokojnie, rozbijemy to na proste kroki.
Narzędzia, których użyjemy:
n8n – platforma do automatyzacji workflow
Google Sheets – do przechowywania danych o klientach i usługach
Infakt API – do generowania faktur
Mailgun – do wysyłania maili
Wymagania wstępne
Zanim przejdziemy do konfiguracji, upewnij się, iż masz:
Konto w n8n (możesz użyć wersji cloud lub self-hosted).
Arkusz Google Sheets z danymi o klientach i usługach.
Konto w Infakt z dostępem do API (potrzebny klucz API).
Konto w Mailgun (lub inna usługa do wysyłania maili) (potrzebny klucz API).
Podstawową znajomość n8n – jeżeli jesteś początkujący, nie martw się, wyjaśnię wszystko krok po kroku.
Konfiguracja Arkusza Google Sheets
Pierwszym krokiem jest przygotowanie arkusza Google Sheets, który będzie przechowywał dane o Twoich klientach i usługach. Arkusz powinien mieć dwie zakładki:
Usługi – tu przechowujemy informacje o klientach i ich usługach.
Historia_faktur – tu będziemy zapisywać historię wystawionych faktur.
Struktura zakładki „Usługi”
Oto jak powinna wyglądać zakładka „Usługi”:
ID – unikalny identyfikator usługi
Nazwa_firmy – nazwa firmy klienta
E_mail – adres email klienta
Nazwa_usługi – nazwa usługi z placeholderami, np. „Usługa testowa – abonament %month% %year%”
Cena_netto – cena netto usługi
VAT – stawka VAT (np. 23)
Cykl – cykl rozliczeniowy (miesięczny lub roczny)
Data_rozpoczęcia – data rozpoczęcia usługi
Ostatnia_faktura – data ostatniej wystawionej faktury
Status – status usługi (aktywna lub nieaktywna)
Przykład danych w Google Sheets
Zrzut ekranu przedstawia arkusz Google Sheets z zakładką „Usługi”. Ten arkusz jest podstawą automatyzacji, dostarczając dane do generowania faktur.
Konfiguracja n8n
Teraz przejdziemy do n8n, gdzie stworzymy workflow automatyzujący cały proces. Workflow składa się z 11 node, które wykonują poszczególne zadania: od sprawdzenia daty, przez wystawienie faktury, po wysłanie maila do klienta.
Oto jak wygląda gotowy workflow:
Zrzut ekranu przedstawia workflow „Automatyczne wystawianie faktur w Dogtronic” w n8n.
Krok 1: Utwórz nowy workflow
Zaloguj się do n8n i utwórz nowy workflow. Nazwij go np. „Automatyczne wystawianie faktur”.
Krok 2: Dodaj node Schedule Trigger
Ten node uruchomi workflow codziennie o 8:00 rano.
Typ: Schedule Trigger
Konfiguracja: Ustaw Trigger at na 8:00 AM
Cel: Codzienne sprawdzanie, czy należy wystawić nowe faktury
Krok 3: Dodaj node Google Sheets (get_invoices)
Ten node pobierze dane z arkusza „Usługi”.
Typ: Google Sheets
Operacja: Read
Document ID: ID Twojego arkusza Google Sheets
Sheet Name: „Usługi”
Autentykacja: Użyj OAuth2 dla Google Sheets
Cel: Pobranie listy usług i klientów z arkusza
Krok 4: Dodaj node Code (check_date)
Ta noda zawiera skrypt JavaScript, który sprawdzi, dla których usług minął miesiąc lub rok od ostatniej faktury i przygotuje dane do wystawienia nowych faktur.
Przykładowy kod JavaScript:
const today = new Date();
// Ustawiamy godzinę na koniec dnia dla precyzyjniejszego porównania
today.setHours(23, 59, 59, 999);
const items = $input.all();
const invoicesToCreate = [];
// Pobierz aktualny miesiąc i rok
const currentMonth = today.toLocaleString('pl-PL', { month: 'long' }); // styczeń, luty, etc.
const currentYear = today.getFullYear(); // 2025
// Funkcja do bezpiecznego obliczania następnej daty faktury
function calculateNextInvoiceDate(lastInvoiceDate, cycle) {
const lastDate = new Date(lastInvoiceDate);
if (cycle === 'miesięczny') {
// Pobierz dzień miesiąca z ostatniej faktury
const dayOfMonth = lastDate.getDate();
// Utwórz nową datę o miesiąc później
const nextDate = new Date(lastDate.getFullYear(), lastDate.getMonth() + 1, 1);
// Sprawdź ile dni ma następny miesiąc
const daysInNextMonth = new Date(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate();
// Ustaw dzień - jeżeli oryginalny dzień jest większy niż liczba dni w następnym miesiącu,
// ustaw na ostatni dzień miesiąca
const targetDay = Math.min(dayOfMonth, daysInNextMonth);
nextDate.setDate(targetDay);
return nextDate;
} else if (cycle === 'roczny') {
const nextDate = new Date(lastDate);
nextDate.setFullYear(nextDate.getFullYear() + 1);
return nextDate;
}
return null;
}
// Funkcja do formatowania daty dla logów
function formatDate(date) {
return date.toISOString().split('T')[0];
}
console.log(`=== SPRAWDZANIE FAKTUR NA DZIEŃ: ${formatDate(today)} ===`);
for (const item of items) {
const data = item.json;
console.log(`\n--- Sprawdzanie usługi: ${data.Nazwa_usługi} (ID: ${data.ID}) ---`);
if (data.Status !== 'aktywna') {
console.log(`Status: ${data.Status} - pomijam`);
continue;
}
if (!data.Ostatnia_faktura) {
console.log(`Brak daty ostatniej faktury - pomijam`);
continue;
}
const lastInvoice = new Date(data.Ostatnia_faktura);
const startDate = new Date(data.Data_rozpoczęcia);
console.log(`Ostatnia faktura: ${formatDate(lastInvoice)}`);
console.log(`Cykl: ${data.Cykl}`);
// Sprawdź czy data ostatniej faktury jest prawidłowa
if (isNaN(lastInvoice.getTime())) {
console.log(` Nieprawidłowa data ostatniej faktury: ${data.Ostatnia_faktura} - pomijam`);
continue;
}
const nextInvoiceDate = calculateNextInvoiceDate(lastInvoice, data.Cykl);
if (!nextInvoiceDate) {
console.log(` Nieobsługiwany cykl: ${data.Cykl} - pomijam`);
continue;
}
console.log(` Następna faktura powinna być: ${formatDate(nextInvoiceDate)}`);
console.log(` Dzisiaj: ${formatDate(today)}`);
console.log(` Czy czas na fakturę? ${nextInvoiceDate <= today ? 'TAK' : 'NIE'}`);
if (nextInvoiceDate <= today) {
let serviceName;
// Sprawdź czy istnieje numer zamówienia
if (data.Numer_zamowienia && data.Numer_zamowienia.trim() !== '') {
// jeżeli jest numer zamówienia, użyj tego formatu
serviceName = `Usługi zgodnie zamówieniem nr ${data.Numer_zamowienia}`;
} else {
// jeżeli nie ma numeru zamówienia, użyj standardowej nazwy z placeholderami
// Pobierz miesiąc i rok z daty następnej faktury
const invoiceMonth = nextInvoiceDate.toLocaleString('pl-PL', { month: 'long' });
const invoiceYear = nextInvoiceDate.getFullYear();
// Zamień %month% i %year% w nazwie usługi
serviceName = data.Nazwa_usługi
.replace('%month%', invoiceMonth)
.replace('%year%', invoiceYear);
}
const invoice = {
serviceId: data.ID,
clientId: data.Klient_ID,
client_company_name: data.Nazwa_firmy,
client_email: data.E_mail,
serviceName: serviceName,
price: data.Cena_netto,
vat: data.VAT,
cycle: data.Cykl,
// Dodaj dodatkowe informacje do debugowania
lastInvoiceDate: formatDate(lastInvoice),
nextInvoiceDate: formatDate(nextInvoiceDate),
todayDate: formatDate(today)
};
invoicesToCreate.push(invoice);
console.log(`FAKTURA DO UTWORZENIA dla ${data.Nazwa_firmy}`);
} else {
console.log(`Jeszcze nie czas na fakturę`);
}
}
if (invoicesToCreate.length > 0) {
console.log(`\n Lista faktur do utworzenia:`);
invoicesToCreate.forEach((invoice, index) => {
console.log(`${index + 1}. ${invoice.client_company_name} - ${invoice.serviceName}`);
});
}
return invoicesToCreate.map(invoice => ({ json: invoice }));consttoday = newDate();// Ustawiamy godzinę na koniec dnia dla precyzyjniejszego porównaniatoday.setHours(23, 59, 59, 999);constitems = $input.all();constinvoicesToCreate = [];// Pobierz aktualny miesiąc i rokconstcurrentMonth = today.toLocaleString('pl-PL', { month:'long' }); // styczeń, luty, etc.constcurrentYear = today.getFullYear(); // 2025// Funkcja do bezpiecznego obliczania następnej daty fakturyfunctioncalculateNextInvoiceDate(lastInvoiceDate, cycle) {constlastDate = newDate(lastInvoiceDate);if (cycle === 'miesięczny') {// Pobierz dzień miesiąca z ostatniej fakturyconstdayOfMonth = lastDate.getDate();// Utwórz nową datę o miesiąc późniejconstnextDate = newDate(lastDate.getFullYear(), lastDate.getMonth() + 1, 1);// Sprawdź ile dni ma następny miesiącconstdaysInNextMonth = newDate(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate();// Ustaw dzień - jeżeli oryginalny dzień jest większy niż liczba dni w następnym miesiącu,// ustaw na ostatni dzień miesiącaconsttargetDay = Math.min(dayOfMonth, daysInNextMonth);nextDate.setDate(targetDay);returnnextDate; } elseif (cycle === 'roczny') {constnextDate = newDate(lastDate);nextDate.setFullYear(nextDate.getFullYear() + 1);returnnextDate; }returnnull;}// Funkcja do formatowania daty dla logówfunctionformatDate(date) {returndate.toISOString().split('T')[0];}console.log(`=== SPRAWDZANIE FAKTUR NA DZIEŃ: ${formatDate(today)} ===`);for (constitemofitems) {constdata = item.json;console.log(`\n--- Sprawdzanie usługi: ${data.Nazwa_usługi} (ID: ${data.ID}) ---`);if (data.Status !== 'aktywna') {console.log(`Status: ${data.Status} - pomijam`);continue; }if (!data.Ostatnia_faktura) {console.log(`Brak daty ostatniej faktury - pomijam`);continue; }constlastInvoice = newDate(data.Ostatnia_faktura);conststartDate = newDate(data.Data_rozpoczęcia);console.log(`Ostatnia faktura: ${formatDate(lastInvoice)}`);console.log(`Cykl: ${data.Cykl}`);// Sprawdź czy data ostatniej faktury jest prawidłowaif (isNaN(lastInvoice.getTime())) {console.log(` Nieprawidłowa data ostatniej faktury: ${data.Ostatnia_faktura} - pomijam`);continue; }constnextInvoiceDate = calculateNextInvoiceDate(lastInvoice, data.Cykl);if (!nextInvoiceDate) {console.log(` Nieobsługiwany cykl: ${data.Cykl} - pomijam`);continue; }console.log(` Następna faktura powinna być: ${formatDate(nextInvoiceDate)}`);console.log(` Dzisiaj: ${formatDate(today)}`);console.log(` Czy czas na fakturę? ${nextInvoiceDate <= today ? 'TAK' : 'NIE'}`);if (nextInvoiceDate <= today) {letserviceName;// Sprawdź czy istnieje numer zamówieniaif (data.Numer_zamowienia && data.Numer_zamowienia.trim() !== '') {// jeżeli jest numer zamówienia, użyj tego formatuserviceName = `Usługi zgodnie zamówieniem nr ${data.Numer_zamowienia}`; } else {// jeżeli nie ma numeru zamówienia, użyj standardowej nazwy z placeholderami// Pobierz miesiąc i rok z daty następnej fakturyconstinvoiceMonth = nextInvoiceDate.toLocaleString('pl-PL', { month:'long' });constinvoiceYear = nextInvoiceDate.getFullYear();// Zamień %month% i %year% w nazwie usługiserviceName = data.Nazwa_usługi .replace('%month%', invoiceMonth) .replace('%year%', invoiceYear); }constinvoice = {serviceId:data.ID,clientId:data.Klient_ID,client_company_name:data.Nazwa_firmy,client_email:data.E_mail,serviceName:serviceName,price:data.Cena_netto,vat:data.VAT,cycle:data.Cykl,// Dodaj dodatkowe informacje do debugowanialastInvoiceDate:formatDate(lastInvoice),nextInvoiceDate:formatDate(nextInvoiceDate),todayDate:formatDate(today) };invoicesToCreate.push(invoice);console.log(`FAKTURA DO UTWORZENIA dla ${data.Nazwa_firmy}`); } else {console.log(`Jeszcze nie czas na fakturę`); }}if (invoicesToCreate.length > 0) {console.log(`\n Lista faktur do utworzenia:`);invoicesToCreate.forEach((invoice, index) => {console.log(`${index + 1}. ${invoice.client_company_name} - ${invoice.serviceName}`); });}returninvoicesToCreate.map(invoice=> ({ json:invoice }));
Dynamiczne nazwy usług – mały trick, duża oszczędność Zamiast manualnie edytować nazwę usługi co miesiąc, używam placeholderów: W arkuszu wpisuję: Usługa testowa - abonament %month% %year% System automatycznie zamienia to na: Usługa testowa – abonament czerwiec 2025
Cel: Sprawdzenie, które usługi wymagają wystawienia faktury i przygotowanie danych z dynamiczną nazwą usługi (np. "Usługa testowa - abonament czerwiec 2025").
Krok 5: Dodaj node If
Ten node sprawdzi, czy istnieją FV do wystawienia.
Warunek: Sprawdź, czy client_email istnieje i nie jest pusty
Cel: Upewnienie się, iż faktura zostanie wysłana tylko do klientów z poprawnym adresem email
Krok 6: Dodaj node Split in Batches
Ten node podzieli listę faktur do wystawienia na pojedyncze elementy, aby przetwarzać je po kolei.
Konfiguracja: Ustaw batch size na 1
Cel: Przetwarzanie faktur jedna po drugiej z opóźnieniem, aby uniknąć problemów z numeracją faktur w Infakt
Krok 7: Dodaj nodę HTTP Request (create_invoice)
Ten node wyśle żądanie POST do API Infakt w celu utworzenia nowej faktury.
Wyślij maila do klienta z fakturą i linkiem do płatności.
From: Twój adres skrzynki nadawczej
To: {{ $json.client_email }}
CC: Adres np. zarządu
Subject: Nowa faktura do zapłaty ({{ $('create_invoice').item.json.number }})
HTML Body: Profesjonalnie sformatowany mail z danymi faktury i przyciskiem „ZAPŁAĆ TERAZ”
Attachments: PDF faktury
Przykład maila wysłanego do klienta:
Przykład wiadomość e-mail, którą otrzymuje klient.
Fragment HTML maila:
<p>Dzień dobry,</p>
<p>W załączeniu przesyłamy fakturę:</p>
<ul>
<li><strong>Numer faktury:</strong> {{ $('create_invoice').item.json.number }}</li>
<li><strong>Usługa:</strong> {{ $json.serviceName }}</li>
<li><strong>Data wystawienia:</strong> {{ $('create_invoice').item.json.invoice_date }}</li>
<li><strong>Termin płatności:</strong> {{ $('create_invoice').item.json.payment_date }}</li>
<li><strong>Kwota do zapłaty:</strong> {{ $('create_invoice').item.json.gross_price / 100 }} PLN</li>
</ul>
<div>
<p>Zapłać wygodnie i bezpiecznie online:</p>
<a href="{{ $('add_to_history').item.json.Url }}" style="display: inline-block; background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold; margin: 15px 0; text-align: center;">ZAPŁAĆ TERAZ</a>
<p><small>Bezpieczne płatności obsługiwane przez Infakt.pl</small></p>
</div>
<p>Dziękujemy za terminową płatność i dotychczasową współpracę! 🙏</p>
<p>W razie pytań jesteśmy do Państwa dyspozycji.</p>
<hr>
<p><small>Ta wiadomość została wygenerowana automatycznie. Prosimy nie odpowiadać na ten email. W razie wątpliwości prosimy o kontakt na adres hello@dogtronic.io</small></p><p>Dzień dobry,</p><p>W załączeniu przesyłamy fakturę:</p><ul><li><strong>Numer faktury:</strong> {{ $('create_invoice').item.json.number }}</li><li><strong>Usługa:</strong> {{ $json.serviceName }}</li><li><strong>Data wystawienia:</strong> {{ $('create_invoice').item.json.invoice_date }}</li><li><strong>Termin płatności:</strong> {{ $('create_invoice').item.json.payment_date }}</li><li><strong>Kwota do zapłaty:</strong> {{ $('create_invoice').item.json.gross_price / 100 }} PLN</li></ul><divclass="payment-section"><p>Zapłać wygodnie i bezpiecznie online:</p><ahref="{{ $('add_to_history').item.json.Url }}"style="display: inline-block; background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold; margin: 15px 0; text-align: center;">ZAPŁAĆ TERAZ</a><p><small>Bezpieczne płatności obsługiwane przez Infakt.pl</small></p></div><p>Dziękujemy za terminową płatność i dotychczasową współpracę! 🙏</p><p>W razie pytań jesteśmy do Państwa dyspozycji.</p><hr><p><small>Ta wiadomość została wygenerowana automatycznie. Prosimy nie odpowiadać na ten email. W razie wątpliwości prosimy o kontakt na adres hello@dogtronic.io</small></p>
Cel: Wysłanie maila do klienta z fakturą i linkiem do płatności
Krok 12: Dodaj node Wait
Wprowadź opóźnienie 1 sekundy przed przetworzeniem kolejnej faktury.
Amount: 1 second
Cel: Zapobieganie problemom z numeracją faktur w Infakt przez zbyt szybkie wysyłanie żądań
Testowanie Workflow
Po skonfigurowaniu wszystkich nodów, przetestuj workflow z danymi testowymi:
Upewnij się, iż w arkuszu „Usługi” masz co najmniej jeden wiersz z datą ostatniej faktury, która wymaga wystawienia nowej faktury.
Uruchom workflow manualnie, aby sprawdzić, czy faktura jest poprawnie tworzona i wysyłana.
Sprawdź logi w n8n, aby upewnić się, iż nie ma błędów.
Zweryfikuj, czy arkusz „Historia_faktur” został zaktualizowany.
Sprawdź, czy mail został wysłany poprawnie z załącznikiem PDF.
Typowe problemy i rozwiązania:
Błąd autentykacji API: Upewnij się, iż klucze API są poprawne.
Nieprawidłowe daty: Sprawdź format dat w arkuszu i w kodzie JavaScript.
Problemy z numeracją faktur: Upewnij się, iż node Wait jest skonfigurowana poprawnie.
Podsumowanie
Gratulacje! Właśnie stworzyłeś automatyzację, która oszczędzi Ci masę czasu i zminimalizuje ryzyko błędów w procesie wystawiania faktur. Workflow działa sam, bez Twojej interwencji, a Ty możesz skupić się na ważniejszych zadaniach.
Korzyści
Zero pomyłek w datach i kwotach
Automatyczne wysyłanie maili z linkiem do płatności
Pełna historia faktur w arkuszu
Zarząd na bieżąco z informacjami (dzięki CC w mailu)
Zachęcam do dostosowania tego workflow do swoich specyficznych potrzeb. Możesz dodać więcej logiki, np. obsługę różnych walut czy automatyczne przypomnienia o zaległych płatnościach.
Chcesz więcej automatyzacji? Sprawdź nasz wpis o automatycznym oznaczaniu opłaconych faktur.
Dodatkowe zasoby
Pamiętaj, iż ten poradnik jest punktem wyjścia. Automatyzacja to potężne narzędzie, które możesz rozwijać i dostosowywać do swoich potrzeb.