Fakturowanie
OficjalnyDarmowyDokumenty sprzedaży przy zamówieniach: rejestr faktur, szybkie i ręczne wystawianie przez podłączony program księgowy (np. wFirma).
- Licencja
- Bezpłatny
- Wersja
- 0.17.4
- Kategoria
- Finanse i księgowość
Moduł fakturowy (core.invoicing)
Bazowy moduł do zbierania informacji o dostępnych integracjach systemów księgowych w systemie. Założenie jest takie, że za pomocą hooków inne moduły poniekąd "rejestrują" swoją gotowość do wystawiania dokumentów sprzedażowych.
Port modułu core.invoicing ze starego systemu (ordershub) na SDK @orbito/api-lib. Najważniejsza zmiana architektoniczna: w orbito-v2 nie ma instancji modułów (instanceId), tożsamością serwisu jest moduleCode, a wywołania między modułami odbywają się przez Core.callModule(moduleCode, method, params). Izolację danych per firma wymusza rdzeń — model warunków nie zawiera companyId.
Hooks
Emitowane przez moduł
Moduł emituje hook core.invoicing.getService. Integracje, które chcą być uwzględniane jako moduły do wystawiania dokumentów, muszą na ten hook odpowiedzieć tablicą:
[
{
name: "wFirma", // przyjazna nazwa serwisu
moduleCode: "core/wfirma", // pełny kod modułu — `Core.getModuleInfo().code`
quickIssueMethod: "quickIssue", // optional — metoda wystawiająca dokument (router `quickIssue`)
quickIssueOptionsMethod: "getQuickIssueOptions", // optional
standardIssueFormMethod: "getStandardIssueForm", // optional — formularz ręcznego wystawiania (router `getStandardIssueForm`)
standardIssueMethod: "standardIssue", // optional — wystawienie z danych formularza (router `standardIssue`)
quickIssueParamsMethod: "...", // optional
getInvoiceFileMethod: "..." // optional
}
]
Metoda wskazana w quickIssueMethod (lub w method opcji szybkiego wystawiania) po wystawieniu dokumentu musi zwrócić jego dane — na tej podstawie router quickIssue tworzy wpis w centralnym rejestrze dokumentów:
{
documentNo: "string",
issuer: "string|optional", // nazwa serwisu; domyślnie `name` z definicji serwisu
documentType: "string|optional", // opisowa nazwa dokumentu, np. "Faktura VAT"
isPaid: "boolean|optional",
amount: "number|optional", // całkowita kwota dokumentu
currency: "string|optional", // domyślnie 'PLN'
issuerData: "object|optional", // dane specyficzne modułu (np. ID dokumentu w zewnętrznym systemie)
printFID: "string|optional" // ID pliku wydruku, jeśli został wygenerowany
}
Metoda wskazana w getInvoiceFileMethod otrzymuje w parametrach dane wpisu z centralnego rejestru: invoiceId (_id wpisu), orderId, documentNo oraz issuerData — i zwraca plik dokumentu:
{
contentBase64: "string",
mimeType: "string|optional", // domyślnie 'application/pdf'
filename: "string|optional" // gdy brak — nazwę buduje hub z `documentNo`
}
Nazwę pliku ustala hub: moduł księgowy zna tylko swój plik, a numer dokumentu i tak przychodzi do niego z centralnego rejestru. Numery z systemów księgowych zawierają ukośniki (FV 1/09/2026), więc znaki niedozwolone w nazwie pliku hub zamienia na -.
Zgodność wstecz: hub przyjmuje również stary kształt {content, encoding: 'base64', format} (tak zwracają moduły sprzed tego kontraktu) i normalizuje go do InvoiceFile (src/invoice-files.js). Odpowiedź bez treści pliku kończy się błędem.
Metoda wskazana w quickIssueOptionsMethod musi zwracać tablicę opcji Szybkiego wystawienia w formacie:
[{
title: "string",
iconSrc: "string",
documentType: "string", // opisowa nazwa dokumentu, np. "Faktura VAT", "Paragon", "Faktura Proforma"
isPaid: "boolean|optional", // czy dokument będzie oznaczony jako opłacony
moduleCode: "string", // kod modułu, który będzie obsługiwał daną opcję szybkiego wystawiania
method: "string", // nazwa metody, która będzie wywołana w danym module po wyborze tej opcji
params: "object|optional", // parametry, które zostaną przekazane do metody modułu
}]
W parametrach wywołania metoda zawsze otrzyma orderId (oraz extraOptions z dopasowanego warunku, jeśli został zdefiniowany).
Metoda wskazana w quickIssueParamsMethod dostarcza formularz parametrów warunku (extraOptions) — to jedyny kanał, którym moduł księgowy opisuje swoje pola, bo hub nie zna ustawień żadnego systemu księgowego. Woła ją conditions.getParamsForm:
// parametry
{
values: {}, // dotychczasowe `extraOptions` warunku (nigdy `undefined`)
conditionId: "string" // `_id` warunku — moduł wstawia go w akcję zapisu formularza
}
// odpowiedź
{ view: { type: "...", ... } } // gotowy schemat OVE formularza
Moduł buduje widok sam (hub sprawdza tylko, czy view.type jest stringiem — resztę waliduje schemaGuard w przeglądarce, więc bez html, cssbox i klucza components). Wartości z danych użytkownika wstawiane w value/content muszą iść do widoku jako zwykły tekst, nie przez {{ }} — Handlebars escapuje takie treści. Brak konfiguracji po stronie modułu to nie błąd: wtedy zwraca widok text z komunikatem.
Zapis formularza idzie przez conditions.setExtraOptions (identyfikator warunku moduł ma z conditionId) — nie przez conditions.update, które wymaga kompletu pól warunku. Puste pole ma znaczyć „użyj globalnej konfiguracji modułu", więc metoda odfiltrowuje klucze o wartości ''/null/undefined przed zapisem.
Para pól standardIssueFormMethod + standardIssueMethod włącza ręczne (standardowe) wystawianie: użytkownik dostaje formularz dokumentu, edytuje go i wysyła. Kontrakt wymaga obu pól naraz (supportsStandardIssue w src/validators.js) — bez formularza nie ma czego pokazać, bez metody wystawiającej nie ma czego zapisać. Moduł z jednym polem jest pomijany w getStandardIssueOptions i odrzucany przez standardIssue błędem INVOICE_ISSUE_METHOD_NOT_ALLOWED.
// standardIssueFormMethod — woła `getStandardIssueForm`
{ orderId: "string", ...extraOptions } // `extraOptions` dopasowanego warunku
{ view: { type: "...", ... } } // gotowy schemat OVE formularza
{ error: "string" } // albo powód, dla którego formularza nie ma
// standardIssueMethod — woła `standardIssue`
{
orderId: "string",
document: {}, // treść formularza; kontrakt `document` należy do modułu księgowego
operationKey: "string", // klucz operacji nadany przez hub (nowy przy każdym wysłaniu)
idempotencyKey: "string", // równy `operationKey`
...extraOptions
}
// → `IssuedDocument` — dokładnie ten sam kształt co przy `quickIssueMethod`
Formularz buduje moduł (hub sprawdza tylko, czy view.type jest stringiem), a document jest dla huba nieprzezroczysty — nie zna pól żadnego systemu księgowego, więc walidacja treści dokumentu należy do metody modułu.
Front nie woła modułów księgowych bezpośrednio: po wyborze opcji szybkiego wystawiania woła na tym module quickIssue({orderId, moduleCode, method, params}) z danymi wybranej opcji — albo quickIssue({orderId, moduleCode: 'auto'}), aby moduł sam dobrał serwis wg warunków i jego quickIssueMethod. Przy wystawianiu ręcznym formularz przychodzi z getStandardIssueForm, a jego przycisk zapisu woła standardIssue({orderId, moduleCode, document}) — też na tym module.
Konsumowane przez moduł
views.orderDetails
Moduł rejestruje handler hooka views.orderDetails (src/hooks/order-details-view.js, podpięty w index.js przez Core.registerHook) i dokłada w ten sposób kartę „Dokumenty sprzedaży” do strony szczegółów zamówienia. Zależność jest jednostronna: kontrakt hooka należy do core.orders — to on emituje hook, waliduje fragmenty (odrzuca te bez key i view.type, deduplikuje po key) i skleja je w kolejności order.
// argument (od core.orders)
{ orderId: 'string', order: 'object' } // order — surowy dokument zamówienia
// odpowiedź tego modułu (brak `orderId` albo błąd → null)
{
key: 'core.invoicing',
title: 'Dokumenty sprzedaży',
order: 30, // podpowiedź kolejności sklejania
view: { ... } // schemat OVE z pages/slots/order-details.yaml, z podstawionym @orderId
}
Handler ładuje pages/slots/order-details.yaml (src/lib/view-schema.js: loadViewSchema
bindPlaceholders) i podstawia placeholder@orderIdw całym drzewie widoku. Nie sprawdza
uprawnień — robi to emitent, a metody wołane z fragmentu odpowiadają za własne. Protokół hooka nie ma kanału błędów (wyjątek daje pustą odpowiedź), więc handler owija wszystko w try/catch, loguje (Log.error) i zwraca null.
Fragment nie zapisuje żadnych zmiennych strony emitenta — pobranie pliku to jedno callMethod z akcją downloadFile w onSuccess, bez stanu pośredniego. Treść base64 idzie tam przez valueFrom, nigdy przez {{ }}: Handlebars escapuje padding = na = i atob w FileService wywala się na takiej treści. Identyfikatory widoków są prefiksowane inv — na jednej stronie stoją obok fragmentów innych modułów.
orders.flags
Moduł rejestruje też handler hooka orders.flags (src/hooks/order-flags.js) i dokłada flagę „Faktura” do wierszy listy zamówień. Kontrakt hooka również należy do core.orders — emitent woła go raz na stronę wyników (≤ 100 zamówień) i skleja opisy flag ze wszystkich modułów. Flaga invoice ma jednego właściciela: ten moduł. core.orders nie liczy jej natywnie.
// argument (od core.orders) — lekka projekcja, żeby handler nie wołał z powrotem core/orders
{
orderIds: ['<id>', ...],
orders: [{ _id: '<id>', invoice: { isNeeded: bool }, ... }],
}
// odpowiedź tego modułu (brak zamówień albo błąd → null)
{
key: 'invoice',
label: 'Faktura',
icon: 'file-invoice',
order: 20,
inactive: { title: 'Brak faktury', summary: 'Nie wystawiono dokumentu sprzedaży' },
states: {
'<orderId>': { active: true, tone: 'success', title: 'Faktura wystawiona', summary: 'Numer: FV/2026/0001' },
},
}
Stan powstaje tylko dla zamówień aktywnych:
| Sytuacja zamówienia | tone | title | summary |
|---|---|---|---|
wystawiono dokument (rejestr invoices) | success | Faktura wystawiona albo Dokumenty: N | Numer: … (unikalne documentNo) i Typ: …, gdy documentType jest ustawiony |
brak dokumentu, ale order.invoice.isNeeded | info | Faktura wymagana | Zamówienie oznaczone do fakturowania, dokument nie został jeszcze wystawiony |
| brak dokumentu i brak oznaczenia | — | brak wpisu w states — emitent pokaże inactive |
Szczegóły są eager: handler ma jedno zapytanie Invoice.find({ orderId: { $in } }) na całą stronę wyników, więc title/summary wracają od razu, bez drugiego wywołania po szczegóły flagi. Identyfikatory spoza kontraktu (puste, nie-tekstowe, duplikaty) są pomijane. Jak przy views.orderDetails, handler jest w całości w try/catch → Log.error i null, bo awaria flagi nie może zabrać listy zamówień.
Eventy emitowane przy zmianach warunków
event:core.invoicing.quick_issue_condition.createdevent:core.invoicing.quick_issue_condition.updatedevent:core.invoicing.quick_issue_condition.removed
Eventy rejestru dokumentów
event:core.invoicing.invoice.issued({orderId, invoiceId, moduleCode, documentNo}) —
po zapisaniu dokumentu w centralnym rejestrze (router quickIssue lub invoices.register). Natywny wyzwalacz automatyzacji Nowy dokument sprzedaży jest uruchamiany bezpośrednio w tej samej instancji modułu; hook służy pozostałym modułom. Dzięki temu automatyzacja nie zależy od tego, czy broker odeśle hook do jego nadawcy. Ewentualne echo własnego hooka jest pomijane, a deterministyczny klucz INVOICE_ISSUED_<invoiceId> zabezpiecza retry.
Metody
| Metoda | Opis |
|---|---|
issuer.get | Lista modułów księgowych zebrana hookiem core.invoicing.getService (z walidacją kontraktu) |
issuer.getForOrder (orderId) | Lista modułów księgowych dostępnych dla zamówienia po zastosowaniu warunków; brak warunków → wszyscy, warunki bez dopasowania → [] |
getQuickIssueOptions (orderId) | Zebrane opcje szybkiego wystawienia dokumentu od wszystkich pasujących serwisów; [], gdy zamówienie ma już dokument sprzedaży |
quickIssue (orderId, moduleCode?, method?, params?) | Router wystawiania: wybiera moduł księgowy (moduleCode lub 'auto' — pierwszy pasujący wg warunków), woła jego metodę wystawiającą i zapisuje zwrócony dokument w centralnym rejestrze; chroniony lockiem per orderId, odrzuca zamówienie z dokumentem błędem INVOICE_ALREADY_ISSUED |
getStandardIssueOptions (orderId) | Moduły księgowe ([{moduleCode, name}]), które mogą wystawić dokument ręcznie — pasujące do zamówienia wg warunków i ogłaszające oba pola kontraktu; [], gdy zamówienie ma już dokument sprzedaży |
getStandardIssueForm (orderId, moduleCode?) | Schemat OVE formularza ręcznego wystawiania pobrany z standardIssueFormMethod wskazanego modułu; bez moduleCode (modal budowany razem ze stroną zamówienia) oraz przy braku modułu lub metody zwraca widok zastępczy text z komunikatem |
standardIssue (orderId, moduleCode, document) | Wystawia dokument z danych formularza przez standardIssueMethod modułu i zapisuje go w centralnym rejestrze; te same zabezpieczenia co quickIssue (lock zamówienia, INVOICE_ALREADY_ISSUED), bez wyboru 'auto' |
invoices.register | Rejestruje dokument wystawiony poza routerem (np. z akcji automatyzacji modułu księgowego) |
invoices.getByOrderId (orderId) | Dokumenty sprzedaży wystawione do zamówienia (wszystkie moduły księgowe) |
operations.inspect (orderId, operationKey?, issuerInstanceId?) | Odczyt operacji powiązanego zamówienia, błędów, historii uzgodnień i tokenu rewizji; wyłącznie uwierzytelniony administrator firmy |
operations.confirmNotIssued (orderId, operationKey, issuerInstanceId, revisionToken, confirmedNotIssued, reason, evidence) | Jawne potwierdzenie administratora, że sprawdził brak dokumentu u dokładnie wskazanego wystawcy; zapisuje zgodę na jedną następną próbę użytkownika, bez wystawiania dokumentu |
invoices.getFile (invoiceId) | Plik dokumentu jako InvoiceFile (contentBase64, mimeType, filename) — routing do getInvoiceFileMethod modułu, który go wystawił, i normalizacja odpowiedzi |
conditions.search (pagination?) | Lista warunków z paginacją (limit ≤ 200) |
conditions.create | Tworzy warunek; puste amountFrom/amountTo zapisuje jako null, błędne widełki odrzuca błędem INVALID_AMOUNT_RANGE |
conditions.update (_id) | Aktualizuje warunek; granica kwoty pominięta w params zachowuje zapisaną wartość, pusta (''/null) jest czyszczona, walidacja jak w conditions.create |
conditions.remove (_id) | Usuwa warunek |
conditions.getParamsForm (_id?) | Schemat OVE formularza parametrów warunku pobrany z quickIssueParamsMethod modułu wskazanego w warunku; bez _id (modal budowany razem ze stroną) oraz przy braku warunku, modułu lub metody zwraca widok zastępczy text z komunikatem |
conditions.setExtraOptions (_id, extraOptions) | Zapisuje wyłącznie extraOptions warunku (kanał zapisu dla formularza wyżej); puste wartości (''/null/undefined) odfiltrowuje, żeby nie przykryły globalnej konfiguracji modułu |
Potok wystawiania (src/issue-pipeline.js)
Obie ścieżki wystawiania — quickIssue (opcja z listy) i standardIssue (formularz) — dzielą jeden potok: rezerwacja operacji (QuickIssueOperation), wywołanie metody modułu z limitem czasu, walidacja zwrotki (checkIssuedDocument), zapis w rejestrze i domknięcie stanu operacji. Same metody są cienkie: uprawnienie, blokada zamówienia, dobór wystawcy (findIssuerForOrder), ustalenie operationKey i wywołanie potoku. Pola tożsamości operacji (orderId, operationKey, idempotencyKey) potok dokłada do parametrów modułu na końcu, więc ani konfiguracja warunku, ani dane z frontendu ich nie nadpiszą.
Blokada zamówienia jest wspólna (core.invoicing:issue:<orderId>, withIssueLock) — zamówienie ma najwyżej jeden dokument sprzedaży, więc obie ścieżki muszą blokować się nawzajem.
operationKey różni się między ścieżkami:
quickIssue— stabilny (quickIssue:<orderId>:<moduleCode>:<method>albo klucz klienta):
ponowione kliknięcie tej samej opcji jest tą samą operacją,
standardIssue— nowy przy każdym wysłaniu formularza
(standardIssue:<orderId>:<moduleCode>:<uuid>). Kanał RPC przenosi tylko treść błędu modułu, więc hub nie odróżni „system księgowy odrzucił dane" od „połączenie padło po wystawieniu"; stabilny klucz blokowałby na zawsze poprawkę literówki w formularzu. Przed podwójnym dokumentem chroni lock zamówienia i findInvoiceForOrder, a maszyna stanów operacji zostaje jako audyt i ochrona przed replayem tego samego wysłania. Po błędzie niepewnym użytkownik dostaje komunikat „sprawdź system księgowy przed ponowieniem".
Operację uncertain z domyślnym stabilnym kluczem quickIssue można uzgodnić przez operations.inspect, a po rzeczywistym sprawdzeniu systemu księgowego przez operations.confirmNotIssued. Wymagane są bieżący token rewizji, jednoznaczna zapisana instancja wystawcy, uzasadnienie i dowody sprawdzenia. Metody odrzucają kontekst modułu i automatyzacji, nawet z uprawnieniem *. Dokument ukryty również blokuje zgodę; nie może istnieć inna nierozstrzygnięta operacja powiązanego zamówienia. Warunki są sprawdzane ponownie przy następnej próbie. Stan retryApproved zużywa atomowo dopiero kolejne świadome wywołanie quickIssue przez zalogowanego użytkownika, z tą samą tożsamością operacji. Pozostają oryginalny błąd, klucz i audyt administratora oraz użytkownika. Zgoda nie usuwa problemu i nie wykonuje efektu zewnętrznego. Inne klucze, w tym stare wysłania formularzy z UUID, obsługują wyłącznie inspekcję.
Zgłaszanie problemów (src/problems.js)
Nieudane wystawienie dokumentu trafia do rejestru problemów firmy (Problems, SDK >= 3.9.0) — użytkownik widzi je w Ustawienia → Problemy i na liczniku dzwonka. Zgłasza wyłącznie hub: jest właścicielem operacji „wystaw dokument sprzedaży", zna zamówienie i dostaje treść błędu modułu księgowego kanałem błędu metody. Moduły księgowe raportują tylko to, czego hub nie widzi (np. własne rezerwacje magazynowe) — dzięki temu jedna nieudana próba to jeden wpis na liście.
problemKey | invoicing.issue:<orderId> (deterministyczny — ponowienie aktualizuje wpis zamiast dokładać nowy) |
code | INVOICING_ISSUE_FAILED |
link | { module: 'core/orders', path: 'details/<orderId>' } — odwołanie do strony szczegółów zamówienia (core.orders rejestruje /details/:orderId); adres w aplikacji składa front |
entityName | numer zamówienia — etykieta encji do wyświetlenia na liście problemów |
desc | mapa językowa { pl, en } (nie string): zespół firmy bywa wielojęzyczny, a wszyscy widzą te same problemy, więc tłumaczenie siedzi w rekordzie — front wybiera wartość po locale. Każda wartość jest przycinana osobno do 300 znaków |
Zgłoszenie idzie w catch potoku i nie zmienia przepływu błędów — pierwotny wyjątek leci dalej do wywołującego. Udane wystawienie kasuje wpis (Problems.resolve z tym samym kluczem), więc problem znika z listy sam po poprawnym ponowieniu. Równoległa próba (INVOICE_ISSUE_PENDING) nie jest zgłaszana: dokument wystawia ta druga próba i to ona zgłosi problem albo skasuje zgłoszenie.
Strony (OVE)
Widoki w formacie OVE (YAML) leżą w katalogu pages/ i są rejestrowane przez src/pages.js (Pages.registerPages, SDK >= 3.3.3) — przy starcie modułu oraz ponownie w Core.onEnabled, żeby po przełączeniu wersji ostatni zapis widoku należał do właśnie włączonego kontenera. Rdzeń zapisuje treść plików w bazie (CompanyPage) i serwuje je frontendowi bez budzenia kontenera. Plik strony to czysty root widget OVE (bez nagłówka title/template).
| Strona | Opis |
|---|---|
/conditions | Zarządzanie warunkami szybkiego wystawiania: tabela warunków z tworzeniem, edycją i usuwaniem (modal ze wspólnym formularzem; widełki kwoty brutto edytowalne także w wierszu tabeli), lista zarejestrowanych modułów księgowych (issuer.get) oraz opis działania warunków. Akcja wiersza „Parametry modułu” otwiera modal z formularzem dostarczonym przez wskazany w warunku moduł księgowy (conditions.getParamsForm), który zapisuje extraOptions przez conditions.setExtraOptions. |
Strona ma placement: { location: 'settings', section: 'orders', icon: 'file-invoice' }, więc rdzeń sam dodaje do niej link w menu ustawień, w sekcji „Zamówienia”. Sekcji orders moduł nie rejestruje — robi to core.orders, od którego zależymy (dependsOn w module.json); tutaj wskazujemy ją wyłącznie kluczem w placement.section.
Fragment strony zamówienia (pages/slots/order-details.yaml) nie jest rejestrowaną stroną (nie ma go w Pages.registerPages) — dostarcza go handler hooka views.orderDetails. Zawiera kartę „Dokumenty sprzedaży”: listę dokumentów zamówienia (invoices.getByOrderId) z kwotą, oznaczeniem opłacenia i pobraniem pliku (invoices.getFile → akcja downloadFile) oraz listę opcji szybkiego wystawiania (getQuickIssueOptions → quickIssue). Sekcja wystawiania ma renderIf na pierwszym wierszu listy dokumentów, więc po wystawieniu dokumentu znika w całości — DynamicListView publikuje swoje dane dopiero, gdy coś przyszło, więc pusty rejestr (i nieudane pobranie) zostawia sekcję widoczną. Fragment renderuje emitent jako schemat z zewnątrz, więc obowiązują ograniczenia schemaGuard: bez html, bez cssbox, bez klucza components.
Druga lista w sekcji wystawiania to ręczne wystawianie: przycisk „Wystaw ręcznie (nazwa serwisu)” na każdy moduł księgowy zwrócony przez getStandardIssueOptions (pusty emptyMessage, bo brak modułu z formularzem to nie błąd). Jeżeli temu samemu modułowi odpowiada kilka pasujących szablonów, przycisk ręcznego wystawiania jest pokazywany tylko raz. Kafelki szybkiego wystawiania pozostają osobne dla każdego pasującego szablonu, ponieważ mogą mieć różne parametry dokumentu. Kliknięcie zapisuje wybrany moduł w zmiennej invStdModuleCode w zakresie view — page zapisuje się do localStorage pod identyfikatorem strony, a /details/:orderId to jeden dokument dla wszystkich zamówień, więc wartość przeciekałaby między nimi — potem przeładowuje widok formularza i dopiero na końcu otwiera modal invStdModal; kolejność jest istotna, bo dynamic rozwiązuje swoje parametry dopiero przy pobraniu, a sama zmiana zmiennej go nie przebudowuje. Modal renderuje formularz modułu księgowego przez widok dynamic ze schemaFrom: view (getStandardIssueForm), bez renderIf: treść modala powstaje razem ze stroną, więc pierwsze wywołanie idzie z pustym moduleCode i dostaje tani widok zastępczy, bez budzenia kontenerów modułów księgowych.
Formularz jest obcym schematem i nie zna identyfikatorów karty, więc wynik wysyłki wraca nazwanymi akcjami z actionDefinitions karty (action: run, silnik szuka definicji w górę drzewa widoków): invStandardIssued — zamknięcie modala, wyzerowanie invStdModuleCode, przeładowanie invDocsList, invQuickIssueOptions i invStdOptions oraz toast sukcesu; invStandardIssueFailed — toast z treścią błędu ({{ $args.message }}), modal zostaje otwarty, więc użytkownik może poprawić dane i wysłać ponownie.
Testy
npm test
Testy (node:test) uruchamiane z --test-force-exit, więc wymagają Node ≥ 22:
| Plik | Zakres |
|---|---|
tests/javascript-syntax.test.js | Parsowalność plików źródłowych modułu |
tests/order-details-view-hook.test.js | Fragment hooka: key/title/order/view, podstawienie @orderId, brak html/cssbox/components, prefiks inv w identyfikatorach, brak zapisu zmiennych strony, pobranie pliku przez valueFrom, istnienie wołanych metod core/invoicing |
tests/order-flags-hook.test.js | Flaga invoice dla listy zamówień: kontrakt opisu, jedno zapytanie $in z limitem 100 identyfikatorów, success z numerami i licznikiem dokumentów, info dla zamówienia oznaczonego do fakturowania, brak wpisu bez dokumentu i oznaczenia, błąd bazy → null |
tests/invoice-files.test.js | Normalizacja odpowiedzi modułu księgowego do InvoiceFile: oba kształty wejścia, domyślny mimeType, nazwa pliku z numeru dokumentu, błąd przy braku treści |
tests/index-registration.test.js | Start modułu: Utils.loadMethods, automatyzacje, uprawnienia, handlery hooków views.orderDetails i orders.flags, strony OVE |
tests/automation.test.js | Wyzwalacz invoice_issued: lokalny dispatch z pełnym kontekstem dokumentu, deterministyczny klucz zdarzenia, próba każdego nasłuchu, filtrowanie typu i wystawcy, pominięcie echa własnego hooka |
tests/permissions.test.js | Katalog uprawnień, requirePermission/requireSystemContext i sprawdzenie uprawnienia w każdej metodzie frontendowej |
tests/invoices.test.js | registerInvoice: lokalne uruchomienie triggera i niezależna publikacja hooka, odporność na awarię jednego kanału, deduplikacja po operationKey i po orderId + moduleCode + documentNo, konflikt operationKey z innym zamówieniem, ponawialne zdarzenie invoice.issued, lock dokumentu; findInvoiceForOrder |
tests/conditions-search.test.js | Paginacja i limity conditions.search |
tests/condition-amount-range.test.js | Widełki kwoty warunku: normalizacja pustych wartości i stringów liczbowych, odrzucenie ujemnych, nie-liczb i od >= do; dopasowanie „od” włącznie / „do” wyłącznie, jedna granica, brak kwoty |
tests/conditions-create-update.test.js | Zapis widełek w conditions.create/conditions.update: puste wartości → null, zachowanie zapisanych granic przy update bez tych pól, odrzucenie błędnych widełek bez zapisu |
tests/issuer-selection.test.js | getIssuersForOrder: osobne opcje dla szablonów tego samego modułu, zamówienie na granicy sąsiednich widełek kwoty trafia do jednego warunku |
tests/quick-issue.test.js | Router quickIssue: dozwolone metody, timeout, stany niepewne, ponowienia i blokada drugiego dokumentu sprzedaży |
tests/standard-issue.test.js | Router standardIssue: przekazanie document i extraOptions, niedostępny wystawca, dokument już wystawiony, moduł bez kompletu kontraktu, wynik niepewny, niezgodna zwrotka, ponowne wysłanie poprawionego formularza |
tests/get-standard-issue-form.test.js | Widok zastępczy przed sprawdzeniem uprawnienia, błąd doboru wystawcy, moduł bez formularza, przekazanie extraOptions i zwrotki niezgodne z kontraktem |
tests/issue-problems.test.js | Zgłoszenia do rejestru problemów: klucz/kod/link/entityName nieudanego wystawienia, opis w obu językach (desc.pl/desc.en) i skrócenie każdej wartości, resolve po sukcesie, cisza przy równoległej próbie |
tests/get-standard-issue-options.test.js | Filtrowanie wystawców po obu polach kontraktu, pusty wynik przy istniejącym dokumencie |
Rejestr dokumentów (invoices)
Nowo utworzony (nie zdeduplikowany) wpis rejestru tworzy również niezmienne zdarzenie INVOICE_CREATED przez History.add. Zdarzenie ma relację do dokumentu core/invoicing i zamówienia core/orders; dzięki tej relacji core/orders czyta je ze wspólnej historii rdzenia i pokazuje w modalu historii zamówienia (od 0.11.2 bez kopii przekazywanej do core/orders:history.record). Starsze dokumenty nie są migrowane. Awaria historii nie cofa wystawionego dokumentu.
Centralny rejestr dokumentów sprzedaży wystawionych do zamówień (odpowiednik modelu orders/Invoice ze starego systemu — przeniesiony tutaj, bo moduł zamówień może istnieć bez dokumentów księgowych). Kolekcja invoices (namespace per firma dokleja rdzeń): orderId, moduleCode (moduł, który wystawił), issuer (nazwa serwisu), documentNo, documentType?, isPaid?, amount?, currency (domyślnie PLN), issuerData (dane specyficzne modułu, np. ID dokumentu w zewnętrznym systemie — klucz do invoices.getFile), printFID?, lastMod, createdAt.
Wpisy powstają w routerach wystawiania (quickIssue, standardIssue — z odpowiedzi modułu księgowego) lub przez invoices.register (gdy moduł księgowy wystawia dokument poza routerem, np. z akcji automatyzacji). Zapis emituje event:core.invoicing.invoice.issued.
Zamówienie ma najwyżej jeden dokument sprzedaży. Routery quickIssue i standardIssue sprawdzają rejestr pod wspólnym lockiem zamówienia, zanim dobiorą moduł księgowy, i przy istniejącym dokumencie kończą się błędem INVOICE_ALREADY_ISSUED — niezależnie od tego, który moduł go wystawił. Z tego samego powodu getQuickIssueOptions i getStandardIssueOptions oddają wtedy pustą listę, a fragment strony zamówienia ukrywa sekcję wystawiania. Reguła nie dotyczy invoices.register: dokument wystawiony poza routerem (np. akcją automatyzacji, która ma własny przełącznik „wystaw mimo istniejącego dokumentu”) zawsze trafia do rejestru.
Rejestracja jest deduplikowana po operationKey, a w jego braku po trójce orderId + moduleCode + documentNo (dedupeKey = moduleCode:orderId:documentNo). Dedupe jest zawężone do zamówienia, bo system księgowy potrafi nadać ten sam numer dokumentu ponownie. Jeśli znaleziony wpis należy do innego zamówienia lub modułu, rejestracja kończy się błędem INVOICE_OPERATION_KEY_CONFLICT zamiast zwrócenia cudzego wpisu.
Warunki (quick_issue_conditions)
Warunek decyduje, który moduł księgowy obsłuży zamówienie:
sourceModuleCodes: string[]— zamówienie musi pochodzić z jednego z tych modułów (order.source.moduleCode)countryCodes: string[]— kod kraju z adresu dostawy musi być na liściecarrierNames: string[]— jedna z fraz musi wystąpić w nazwie przewoźnika (case-insensitive)amountFrom: number | null,amountTo: number | null— widełki kwoty brutto zamówienia
(order.summary.gross, waluta ignorowana): amountFrom <= kwota < amountTo; null = brak granicy. Zamówienie bez liczbowej kwoty nie pasuje do warunku z choć jedną granicą. Metody zapisu normalizują pustą wartość formularza do null i odrzucają ujemne kwoty, nie-liczby oraz amountFrom >= amountTo (INVALID_AMOUNT_RANGE)
invoicingModuleCode: string— moduł użyty do wystawienia dokumentu, gdy warunki są spełnioneextraOptions: object— dodatkowe opcje przekazywane do metod modułu księgowego; pola
opisuje sam moduł (quickIssueParamsMethod → conditions.getParamsForm), a zapisuje je conditions.setExtraOptions
TODO — nieprzeniesione ze starego systemu
Akcja automatyzacji "Wyślij dokument sprzedaży mailem" (core:invoicing:send_mail) nie została przeniesiona, bo w orbito-v2 brakuje jej zależności:
- brak dostępu modułów do mailera rdzenia (protokół SDK nie ma komendy wysyłki maili z załącznikami),
- brak odpowiednika modułu problemów (
core:problems), - brak odpowiednika
EventLogdostępnego dla modułów.
Po uzupełnieniu powyższych akcję można przenieść niemal 1:1 (źródło: ordershub/back/modules/core.invoicing/automations/actions/core:invoicing:send_mail.js).
Różnice względem starego modułu
- brak cache metod (
cache: true/this.cache.clear) — SDK nie udostępnia cache; hook jest wykonywany przy każdym wywołaniu, - identyfikatory modułów:
instanceId→moduleCode(w definicji serwisu, opcjach szybkiego wystawienia i warunkach:invoicingModuleId→invoicingModuleCode,sourceModuleIds→sourceModuleCodes), - model danych na
DB.Schema(kolekcjaquick_issue_conditions, namespace per firma dokleja rdzeń). - ręczne wystawianie dokumentu (stary modal
front/src/pages/orders/modals/CreateNewInvoice.vue wfirma/methods/createInvoiceForOrder.js) jest przeniesione, ale front nie zna już pól
dokumentu: formularz buduje moduł księgowy (standardIssueFormMethod), a hub tylko renderuje go w karcie zamówienia, przekazuje document do standardIssueMethod i zapisuje wynik w centralnym rejestrze.
Daty — strefa czasowa firmy
Data utworzenia dokumentu na zamówieniu (invoices.getByOrderId) jest prezentowana w strefie czasowej firmy (Core.getCompany().timezone, przez Time.forCompany() z SDK ≥ 4.3.0), niezależnie od strefy kontenera i przeglądarki. Strefa jest pobierana raz na wywołanie metody. Pola createdAtStr, createdAtDay i createdAtTime są prezentacyjne; createdAt zachowuje oryginalny moment w UTC.
Zmiana uwzględnia czas letni i zimowy. Nie wykonuje migracji istniejących danych.
Ukrywanie dokumentów w Orbito
Akcja invoices.remove (invoiceId) jest dostępna administratorowi przy dokumencie na karcie zamówienia. Ukryte dokumenty nie trafiają na listy i flagi zamówień ani do wyszukiwania po numerze dokumentu; ich plików nie można pobierać. Ponowna rejestracja tego samego dokumentu zachowuje ukrycie i nie emituje zdarzenia wystawienia. Można jawnie wystawić nowy dokument, ale nadal obowiązują klucze idempotencji, blokada niepewnego wyniku i ochrona przed ponowną konwersją tej samej proformy.
Przycisk Ukryj w Orbito wymaga potwierdzenia. Metoda sprawdza uwierzytelniony kontekst użytkownika i uprawnienia administratora (*); zwykła edycja i wewnętrzne wywołania modułów nie dają dostępu. Zapisuje isRemoved, removedAt i removedBy, zachowując dokument, pliki oraz tożsamość źródła. Nie wywołuje anulowania ani usunięcia u dostawcy i nie rozsyła zdarzenia usuwania. Starsze wpisy bez isRemoved pozostają widoczne.
Odświeżanie stanu płatności dokumentu
Wystawca może ogłosić getPaymentStatusMethod w core.invoicing.getService. Metoda jest wywoływana przez kontrakt invoicing.issuer.<getPaymentStatusMethod> z issuerData, invoiceId i orderId; zwraca { isPaid: boolean, paidAmount?: number }. Opcjonalne orderPayment: { method, paidAt, currency, transactionId, sourceName } pozwala uzupełnić wpłatę zamówienia, gdy dokument jest opłacony i kwota jest dodatnia. paidAt wymaga timestampu ze strefą. Klucz transakcji musi rozróżniać konta wystawcy. Hub używa zapisanej issuerInstanceId, nigdy pierwszego konta ani samego kodu modułu. Przy pobieraniu dokumentów zamówienia odczyt jest odświeżany najwyżej raz na minutę (także po błędzie), maksymalnie cztery odczyty jednocześnie. Przycisk „Sprawdź płatność u wystawcy” wymusza odczyt przez invoices.refreshPaymentStatus z uprawnieniem invoices.view. Uzupełnienie wpłaty wymaga dodatkowo orders.edit w uwierzytelnionym kontekście; odczyt dokumentu pozostaje dostępny bez tego uprawnienia. Nie modyfikuje faktury u wystawcy. Błąd zachowuje ostatni stan i pokazuje informację o nieudanej synchronizacji. Nie ma zastępczej etykiety „Do sprawdzenia”.
Wpłata jest przekazywana przez orders.payments.syncExternalTotal, które pod blokadą zamówienia dopisuje tylko brakującą różnicę. Ponowne odświeżenie, proforma i faktura końcowa nie dublują wpłaty. Ręczne korekty i usunięcia pozostają zachowane. Rejestr zapisuje wybraną instancję właściciela wpłat; przy wielu dostawcach bez zapisanej instancji pokazuje błąd i nie wykonuje komendy. Dane source wpłaty zawierają nazwę, kod i konto wystawcy oraz ID dokumentu. Tabela pokazuje „Pobrano z <nazwa>”, zachowując oddzielnie uwierzytelnionego autora operacji. Niepowodzenie zapisu wpłaty nie cofa odczytanego stanu dokumentu. Zamiar komendy jest zapisywany przed jej wysłaniem. Niepewny wynik blokuje następne odświeżenia wpłaty do sprawdzenia stanu; sam odczyt u wystawcy działa dalej.
Dla historycznych dokumentów bez instancji hub wykonuje odczytową weryfikację u wszystkich odkrytych kont zapisanego modułu. Każde musi ogłaszać paymentIdentityVerification: true. Do odczytu przekazywane jest także expectedDocument: { documentNo, amount, currency }. Wystawca sprawdza zewnętrzne ID z issuerData oraz zgodność numeru, kwoty i waluty dokumentu; zwraca identityVerified: false dla niezgodności lub identityVerified: true razem ze statusem płatności. Dopiero jeden potwierdzony dokument, przy ukończonych odczytach wszystkich kandydatów, pozwala atomowo zapisać instancję i status. Brak danych, brak dostawcy, błąd odczytu i wiele dopasowań nie przypisują konta.
