Cyklicznie wystawianie faktur w Infakt dzięki N8N

dogtronic.io 1 dzień temu

Wstęp

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:

  1. Konto w n8n (możesz użyć wersji cloud lub self-hosted).
  2. Arkusz Google Sheets z danymi o klientach i usługach.
  3. Konto w Infakt z dostępem do API (potrzebny klucz API).
  4. Konto w Mailgun (lub inna usługa do wysyłania maili) (potrzebny klucz API).
  5. 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:

  1. Usługi – tu przechowujemy informacje o klientach i ich usługach.
  2. 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 })); 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 }));

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.

  • URL: https://api.infakt.pl/v3/invoices.json
  • Metoda: POST
  • Autentykacja: Header Auth z kluczem API Infakt
  • Body (JSON):
{ "invoice": { "kind": "vat", "number": null, "sell_date": "{{$json.nextInvoiceDate}}", "invoice_date": "{{$json.nextInvoiceDate}}", "payment_to": "{{DateTime.fromISO($json.nextInvoiceDate).plus({days: 7}).toFormat('yyyy-MM-dd')}}", "client_company_name": "{{ $json.client_company_name }}", "services": [ { "name": "{{$json.serviceName}}", "net_price": "{{Math.round($json.price * 100)}}", "unit_net_price": "{{Math.round($json.price * 100)}}", "tax_symbol": "{{$json.vat}}", "quantity": 1 } ] } }{ "invoice": { "kind": "vat", "number": null, "sell_date": "{{$json.nextInvoiceDate}}", "invoice_date": "{{$json.nextInvoiceDate}}", "payment_to": "{{DateTime.fromISO($json.nextInvoiceDate).plus({days: 7}).toFormat('yyyy-MM-dd')}}", "client_company_name": "{{ $json.client_company_name }}", "services": [ { "name": "{{$json.serviceName}}", "net_price": "{{Math.round($json.price * 100)}}", "unit_net_price": "{{Math.round($json.price * 100)}}", "tax_symbol": "{{$json.vat}}", "quantity": 1 } ] } }
Cel: Utworzenie nowej faktury w Infakt z dynamicznymi danymi

Krok 8: Dodaj node Google Sheets (update_sheet)

Po wystawieniu faktury zaktualizuj arkusz „Usługi” z nową datą ostatniej faktury.

  • Operacja: Update
  • Sheet Name: Usługi
  • Columns to update: Ostatnia_faktura z wartością {{ $json.nextInvoiceDate }}
  • Matching column: ID z wartością {{ $json.serviceId }}
  • Cel: Aktualizacja daty ostatniej faktury w arkuszu

Krok 9: Dodaj node Google Sheets (add_to_history)

Dodaj rekord o wystawionej fakturze do zakładki Historia_faktur.

  • Operacja: Append
  • Sheet Name: „Historia_faktur”
  • Columns:
    • ID: {{ $('create_invoice').item.json.uuid }}
    • Data_dodania: {{DateTime.now().toFormat('yyyy-MM-dd H:mm')}}
    • Klient_ID: {{ $json.client_company_name }}
    • Numer_faktury: {{ $('create_invoice').item.json.number }}
    • Data_wystawienia: {{ $('create_invoice').item.json.invoice_date }}
    • Kwota_netto: {{ $('create_invoice').item.json.net_price / 100 }}
    • Status: {{ $('create_invoice').item.json.status }}
    • Url: {{ $('create_invoice').item.json.extensions.payments.link }}
  • Cel: Zapisanie historii wystawionych faktur

Krok 10: Dodaj node HTTP Request (download_invoice)

Pobierz PDF faktury z Infakt.

  • URL: https://api.infakt.pl/v3/invoices/{{ $('create_invoice').item.json.uuid }}/pdf.json?document_type=original
  • Metoda: GET
  • Autentykacja: Header Auth z kluczem API Infakt
  • Cel: Pobranie PDF faktury do załączenia w mailu

Krok 11: Dodaj node Mailgun (send_mail)

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> <div class="payment-section"> <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>
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:

  1. Upewnij się, iż w arkuszu „Usługi” masz co najmniej jeden wiersz z datą ostatniej faktury, która wymaga wystawienia nowej faktury.
  2. Uruchom workflow manualnie, aby sprawdzić, czy faktura jest poprawnie tworzona i wysyłana.
  3. Sprawdź logi w n8n, aby upewnić się, iż nie ma błędów.
  4. Zweryfikuj, czy arkusz „Historia_faktur” został zaktualizowany.
  5. 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.

Powodzenia!

Idź do oryginalnego materiału