beziworld / laravel-edi-epp
Read and write InsERT EDI++ (.epp) exchange files for Subiekt GT, Subiekt nexo, Rewizor and Rachmistrz from Laravel: typed builders, streaming reader, spec validation.
Requires
- php: ^8.3
- ext-iconv: *
- ext-json: *
- ext-mbstring: *
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 20:08:04 UTC
README
Read and write InsERT EDI++ (.epp) exchange files from Laravel. EDI++ is the text format that Subiekt GT,
Subiekt nexo, Rachmistrz GT, Rewizor GT and other InsERT programs use to import and export contractors, products and
documents. Hand your users a file they can import, or read the files their Subiekt exports.
- writer with the exact file format: Windows-1250 (or 852) encoding, CRLF,
[INFO],[NAGLOWEK]and[ZAWARTOSC]sections, quoted text with doubled quotes, decimals with a dot and four places,yyyymmddhhnnssdates - typed builders: contractors (KONTRAHENCI with groups, features, extras), products and services (TOWARY with prices, VAT rates, units, EAN, CN codes, GTU markers), sales and purchase documents with lines, corrections, VAT summary rows, cash and bank payments (KP, KW, BP, BW) and document-scoped sections
- document set exporter: one file with the dictionaries followed by the documents; contractors and products used on documents are added to the dictionaries automatically
- line values, VAT summary per rate and totals calculated exactly (net or gross priced documents, corrections)
- streaming reader into typed DTOs with conversion to UTF-8, robust to unknown sections, keywords and document types (kept verbatim) and to field count differences (reported as issues, or thrown in strict mode)
- validation against the specification: record layouts per version (1.05 to 1.11), field types, enumerations, text lengths, sections and document types not available in the selected version
- download responses with the right charset,
epp:inspectArtisan command
Why this package
Packagist has no EDI++ library: searching for "epp" returns Extensible Provisioning Protocol clients for domain
registries, "edi++" returns UN/EDIFACT and X12 tools, and the Subiekt packages (lukeotdr/api-subiekt-gt,
asocial-media/subiekt-sfera) wrap the Sfera COM API, which needs a Windows machine next to the client's database
and a separate licence. The open-source EPP tools on GitHub are one-way converters in C#, Python, Go or Java
(CSV/XLSX/XML to EPP, EPP to JSON). This package is a PHP reader and writer with the record layouts of every
public specification version, validation and Laravel integration, so a shop, SaaS or ERP connector can exchange
data with InsERT programs through plain files.
Specification coverage
Layouts were transcribed from the public InsERT documents "Komunikacja - Struktura danych EDI++" for versions
1.05.1, 1.05.3, 1.09 and 1.11 (the newest public document). The writer supports versions 1.05, 1.05.1 to 1.05.5,
1.06, 1.07, 1.08, 1.09, 1.10 and 1.11 (default). Sections introduced in newer versions (for example
DOKUMENTYKSEF from 1.12) are not documented publicly; the reader keeps them as UnknownSection objects.
| Area | Sections |
|---|---|
| File header | [INFO] (24 fields) |
| Trade and warehouse documents | FS, FZ, PA, RS, RZ, FR, KFS, KFZ, KRS, KRZ, ZK, ZD, WZ, PZ, VWZ, VPZ, PW, RW, MMW, ZW; FWN, FWO, KWN, KWO, FM, KFM (accounting office) |
| Content | lines (22 fields, 31 on corrections) or VAT summary rows (5, 14 or 18 fields) |
| Payments | KP, KW, BP, BW (56 fields) |
| Accounting office | settlements RLS, RLZ; fiscal reports RF, PRF, NZ |
| Dictionaries | KONTRAHENCI, GRUPYKONTRAHENTOW, CECHYKONTRAHENTOW, DODATKOWEKONTRAHENTOW, TOWARY, CENNIK, GRUPYTOWAROW, CECHYTOWAROW, DODATKOWETOWAROW, TOWARYKODYCN, TOWARYGRUPYJPKVAT, PRACOWNICY, URZEDY |
| Document-scoped | DATYZAKONCZENIA, WYMAGALNOSCMPP, PRZYCZYNYKOREKT, NUMERYIDENTYFIKACYJNENABYWCOW, DOKUMENTYFISKALNEVAT, OPLATYDODATKOWE, OPLATYSPECJALNE, IDENTYFIKATORYPLATNOSCI, DOKUMENTYZNACZNIKIJPKVAT, DOSTAWYMARZOWEDLAFM, OPLATACUKROWA, SPECYFIKACJATOWAROWAWSTO, PLATNOSCI, INFORMACJEWSTO, DATYUJECIAKOREKT, STAWKIVATZAGRANICZNE |
Installation
composer require beziworld/laravel-edi-epp
php artisan vendor:publish --tag=edi-epp-config # optional
Requires PHP 8.3+ with ext-iconv and ext-mbstring, Laravel 12 or 13. Optional .env settings:
EDI_EPP_VERSION=1.11 # 1.05 ... 1.11 EDI_EPP_PURPOSE=3 # 0 accounting office (Rachmistrz/Rewizor), 3 other (Subiekt) EDI_EPP_CODE_PAGE=1250 # 1250 or 852 EDI_EPP_TIMEZONE=Europe/Warsaw EDI_EPP_PROGRAM="My Shop" EDI_EPP_SENDER_CODE=SHOP EDI_EPP_SENDER_NAME="My Shop Spółka z o.o." EDI_EPP_SENDER_NIP=1234563218 EDI_EPP_SENDER_STREET="ul. Prosta 2" EDI_EPP_SENDER_POST_CODE=30-001 EDI_EPP_SENDER_CITY=Kraków EDI_EPP_WAREHOUSE_CODE=MAG EDI_EPP_STRICT_ENCODING=true # throw on characters missing in the code page EDI_EPP_TRUNCATE_TEXT=false # cut texts longer than the specification allows EDI_EPP_STRICT_READING=false # throw on the first problem while reading
Exporting documents for Subiekt
use Beziworld\EdiEpp\Builders\CashDocumentBuilder; use Beziworld\EdiEpp\Builders\ContractorBuilder; use Beziworld\EdiEpp\Builders\DocumentBuilder; use Beziworld\EdiEpp\Builders\LineBuilder; use Beziworld\EdiEpp\Builders\ProductBuilder; use Beziworld\EdiEpp\Data\VatRate; use Beziworld\EdiEpp\Facades\EdiEpp; $customer = ContractorBuilder::make('ACME') ->name('ACME Spółka z o.o.', 'ACME') ->nip('5261040828') ->address('ul. Długa 1', '00-001', 'Warszawa', 'Polska') ->email('biuro@acme.pl') ->group('Hurt'); $chalk = ProductBuilder::goods('KREDA-01', 'Kreda szkolna żółta') ->ean('5901234123457') ->unit('opak.') ->vat(23) // or VatRate::exempt(), VatRate::reverseCharge(), ... ->price('Detaliczna', net: '5.00') // gross 6.15 is calculated ->price('Hurtowa', net: '4.80') ->cn('25090000') ->gtu(6); $invoice = DocumentBuilder::salesInvoice('FS 15/2026') ->issueDate('2026-11-04') ->saleDate('2026-11-03') ->contractor($customer) // header data copied, contractor added to KONTRAHENCI ->payment('Przelew', dueDate: '2026-11-18') ->line(LineBuilder::for($chalk, 20)) // code, unit, VAT and first price taken from the product ->line(LineBuilder::oneOffService('Transport', 1, '10.00')->vat(8)) ->completionDate('2026-11-03') // DATYZAKONCZENIA ->splitPayment(false); // WYMAGALNOSCMPP $payment = CashDocumentBuilder::bankReceipt('BP 5/2026', '133.80') ->issueDate('2026-11-10') ->contractor($customer) ->settles($invoice) // title = "FS 15/2026" ->bank('mBank', '11 1140 2004 0000 3002 0135 5387'); $export = EdiEpp::export() // version, purpose, sender, warehouse from config ->createdBy('Jan Kowalski') ->documents([$invoice, $payment]); return EdiEpp::download($export, 'sprzedaz-listopad.epp'); // or EdiEpp::write($export), EdiEpp::save($export, $path)
Missing line values (net and gross price, net, VAT and gross value), the VAT summary per rate, the header totals,
the line count and the amount due are calculated with exact decimal arithmetic. Documents priced net (default)
calculate VAT from the sum of net values per rate; ->grossPrices() calculates it from the gross sum. Every value can
also be set explicitly, and any field of any record can be set by its key with ->set('fieldKey', $value).
Corrections
$correction = DocumentBuilder::salesCorrection('KFS 2/2026', 'FS 15/2026', '2026-11-04') ->issueDate('2026-11-06') ->contractor($customer) ->line(LineBuilder::for($chalk, 15, '5.00')->before(20)) // 20 before, 15 after ->correctionReason('Zwrot części towaru', 1); // header totals: -25.00 net, -5.75 VAT, -30.75 gross (after minus before)
Files for Rachmistrz and Rewizor
With purpose 0 (accounting office) trade documents carry VAT summary rows instead of lines. The rows are calculated from the lines or set explicitly:
use Beziworld\EdiEpp\Builders\FileBuilder; use Beziworld\EdiEpp\Enums\Version; $file = FileBuilder::make(Version::V1_11) ->forAccountingOffice() ->period('2026-10-01', '2026-10-31') ->document(DocumentBuilder::purchaseInvoice('FZ 9/2026', supplierNumber: 'F/2026/10/77') ->issueDate('2026-10-15') ->contractor('HURTOWNIA') ->vatRow(23, net: '900.00', vat: '207.00', gross: '1107.00') ->vatRow(VatRate::exempt(), '200.00', '0.00', '200.00') ->jpkMarkers(['mpp' => true, 'gtu06' => true])) // DOKUMENTYZNACZNIKIJPKVAT ->build();
The VAT row layout follows the version: 5 fields in 1.05, 14 in 1.05.1, 18 from 1.05.3.
Products only (Subiekt nexo product import)
$file = EdiEpp::export() ->products(Product::query()->cursor()->map(fn ($product) => ProductBuilder::goods($product->sku, $product->name) ->ean($product->ean) ->vat($product->vat_rate) ->price('Detaliczna', gross: $product->price))) ->build();
InsERT notes that Subiekt nexo limits product symbols to 20 characters; the validator enforces the 20-character code of the specification.
Reading files
$file = EdiEpp::read(storage_path('import/export.epp')); // path or stream; EdiEpp::parse($contents) for strings $file->info->version(); // "1.11" $file->info->program(); // "Subiekt GT" $file->info->senderName(); // UTF-8, converted from code page 1250 or 852 foreach ($file->documents as $document) { $document->type(); // DocumentType::SalesInvoice $document->fullNumber(); // "FS 15/2026" $document->issueDate(); // CarbonImmutable (Europe/Warsaw) $document->contractorName(); $document->grossValue(); // "133.8000" (decimal string) $document->completionDate();// linked from DATYZAKONCZENIA foreach ($document->lines as $line) { $line->productCode(); // "KREDA-01" $line->quantity(); // "20.0000" $line->netValue(); $line->decimal('quantityBefore'); // correction data } $document->vatRows; // accounting office files } $file->cashDocuments; // KP, KW, BP, BW $file->contractor('ACME')?->groups(); $file->product('KREDA-01')?->price('Hurtowa')?->net(); $file->records; // PRACOWNICY, URZEDY, ... and records without an owner $file->unknownSections; // sections of newer versions, kept verbatim $file->issues; // field count or value problems, with line numbers
Every DTO keeps the normalised values in $fields (accessible with get(), text(), integer(), decimal(),
flag(), date()) and the values as read in $raw. Large files can be streamed without linking:
foreach (EdiEpp::stream($path, fn (Issue $issue) => logger()->warning((string) $issue)) as $item) { // Info, Contractor, Product, Price, Document, CashDocument, Record, UnknownSection }
Files declaring code page 1250 but saved as UTF-8 by other tools (or starting with a UTF-8 BOM) are detected and read
correctly. Pass EdiEpp::reader(strict: true) to throw a ParseException on the first problem.
Validation
$violations = EdiEpp::validate($export); // list of Violation; empty means the file can be written foreach ($violations as $violation) { echo $violation; // 'TOWARY KREDA-01 field "name": Text is longer than 50 characters.' }
The writer validates before writing and throws InvalidRecord with all violations. Checked: known version, field
types, enumerated values (document types, statuses, contractor and product types, VAT symbols, code page, ...),
maximum text lengths, fields and sections newer than the file version, document types reserved for accounting office
files, required identifying fields (codes, full numbers, issue dates) and unique codes.
Artisan
php artisan epp:inspect storage/import/export.epp php artisan epp:inspect export.epp --json --limit=100 php artisan epp:inspect export.epp --strict
Prints the header, the number of contractors, products and documents per type, a document table and the issues.
Errors
| Exception | When |
|---|---|
InvalidRecord |
data violates the specification; $violations lists every problem |
EncodingException |
a character does not exist in the code page (strict encoding), unsupported code page |
ParseException |
missing [INFO], unreadable file, any problem in strict reading mode ($issue) |
UnsupportedVersion |
unknown version literal, a section newer than the file version |
InvalidValue |
a value cannot be converted to the field type, unknown field key |
InvalidConfiguration |
invalid version, purpose, code page or time zone in the configuration |
All exceptions extend Beziworld\EdiEpp\Exceptions\EdiEppException.
Notes and limits
- The public documents contain no example records; tests use fixtures written from the field tables and round trips.
- Fields whose introduction version could not be confirmed: the ISO country code of contractors (written from 1.09), the JPK document type marker (from 1.08). Rewizor and Rachmistrz layouts from 1.06 onward are written with 18-field VAT rows.
- Text with line breaks is written on one line (breaks replaced with spaces); the reader accepts quoted values that span lines.
- Unknown sections are kept when reading but are not written back.
- Importing documents (as opposed to products) from EPP into Subiekt nexo is not described in public InsERT materials; test with your target program.
Development
composer install vendor/bin/pint vendor/bin/phpstan analyse --memory-limit=1G vendor/bin/pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
Need an integration? Get in touch through beziworld.eu.
License
MIT. See LICENSE.md. InsERT, Subiekt, Rachmistrz, Rewizor and EDI++ are names of InsERT S.A. This package is not affiliated with or endorsed by InsERT S.A.