jeancz/czech-vat

Czech VAT Return (DPHDP3) and VAT Control Statement (DPHKH1) XML generator

Maintainers

Package info

github.com/jeancz/czech-vat

pkg:composer/jeancz/czech-vat

Statistics

Installs: 50

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

dev-master 2026-04-24 06:59 UTC

This package is auto-updated.

Last update: 2026-04-29 12:46:25 UTC


README

PHP knihovna pro generování XML souborů daňového přiznání k DPH (DPHDP3) a kontrolního hlášení DPH (DPHKH1) ve formátu požadovaném portálem EPO Finanční správy ČR.

Obsah

Požadavky

  • PHP 8.4+
  • Rozšíření ext-dom a ext-libxml (součást standardní instalace PHP)
  • Žádné další závislosti

Instalace

composer require jean/czech-vat

Rychlý start

use JeanCz\CzechVat\VatFilingGenerator;
use JeanCz\CzechVat\Enum\VatRateType;
use JeanCz\CzechVat\Model\Invoice\Invoice;
use JeanCz\CzechVat\Model\Invoice\InvoiceLine;
use JeanCz\CzechVat\Model\Invoice\InvoiceCollection;
use JeanCz\CzechVat\Model\Period\TaxPeriod;
use JeanCz\CzechVat\Model\Taxpayer\Taxpayer;
use JeanCz\CzechVat\Model\VatRates\VatRates;

// 1. Sazebník platný pro dané období
$rates = VatRates::current(); // od 1.1.2024: základní 21 %, snížená 12 %

// 2. Plátce
$taxpayer = Taxpayer::legalEntity(
    vatId:        'CZ12345678',
    taxOfficeCode: '452',
    companyName:  'Moje Firma s.r.o.',
    street:       'Hlavní',
    houseNumber:  '1',
    city:         'Praha',
    postalCode:   '11000',
    email:        'ucetni@mojefirma.cz',
);

// 3. Zdaňovací období
$period = TaxPeriod::monthly(2025, 1);

// 4. Faktury
$invoices = (new InvoiceCollection())
    ->addIssued(
        // Vydaná faktura nad 10 000 Kč — vykazuje se jednotlivě v KH
        new Invoice(
            lines: [
                new InvoiceLine(VatRateType::Standard, taxBase: 100_000, vat: 21_000, rates: $rates),
            ],
            partnerVatId:  'CZ87654321',
            documentNumber: 'FAK-2025-001',
            taxPointDate:  new DateTimeImmutable('2025-01-15'),
        ),
        // Vydaná faktura do 10 000 Kč — souhrnně v sekci A.5 KH
        new Invoice([
            new InvoiceLine(VatRateType::Standard, taxBase: 5_000, vat: 1_050, rates: $rates),
        ]),
    )
    ->addReceived(
        // Přijatá faktura nad 10 000 Kč — sekce B.2 KH
        new Invoice(
            lines: [
                new InvoiceLine(VatRateType::Standard, taxBase: 50_000, vat: 10_500, rates: $rates),
            ],
            partnerVatId:           'CZ11111111',
            documentNumber:         'MUJ-REF-001',
            supplierDocumentNumber: 'DOD-INV-999',
            taxPointDate:           new DateTimeImmutable('2025-01-10'),
        ),
    );

// 5. Generování
$generator = new VatFilingGenerator($taxpayer, $period, $invoices);

file_put_contents('kontrolni_hlaseni.xml', $generator->generateControlStatement());
file_put_contents('danove_priznani.xml',   $generator->generateVatReturn());

Sazby DPH

Sazby nejsou součástí enumu, ale konfigurovatelného objektu VatRates. Oddělení sémantiky od konkrétních procent zajišťuje, že při legislativní změně stačí aktualizovat sazebník — ne upravovat enum a s ním veškerý kód, který na něj závisí.

Předdefinované sazebníky

use JeanCz\CzechVat\Model\VatRates\VatRates;

// Aktuální sazby (od 1. 1. 2024)
// Základní: 21 %, Snížená: 12 %, Nulová: 0 %
$rates = VatRates::current();

// Stejné, explicitnější název
$rates = VatRates::validFrom20240101();

// Sazby platné do 31. 12. 2023
// Základní: 21 %, Snížená: 15 %, Druhá snížená: 10 %, Nulová: 0 %
$rates = VatRates::validUntil20231231();

Vlastní sazebník

use JeanCz\CzechVat\Enum\VatRateType;

$rates = VatRates::custom([
    VatRateType::Standard->value => 21,
    VatRateType::Reduced->value  => 12,
    VatRateType::Zero->value     => 0,
]);

Práce se sazebníkem

// Zjistit procento pro daný typ
$rates->percentage(VatRateType::Standard); // → 21

// Přeložit konkrétní procento na sémantický typ
$rates->resolve(12); // → VatRateType::Reduced

// Ověřit, zda je sazba registrována
$rates->has(12);                          // → true
$rates->hasType(VatRateType::SecondReduced); // → false (v aktuálním sazebníku)

Typy sazeb (VatRateType)

Case Popis EPO XML atribut
VatRateType::Standard Základní sazba zakl_dane1 / dan1
VatRateType::Reduced Snížená sazba zakl_dane2 / dan2
VatRateType::SecondReduced Druhá snížená (do 31. 12. 2023) zakl_dane3 / dan3
VatRateType::Zero Nulová sazba nevykazuje se v řádcích DPH

Faktury a řádky

InvoiceLine — jeden řádek faktury

use JeanCz\CzechVat\Model\Invoice\InvoiceLine;
use JeanCz\CzechVat\Enum\VatRateType;

$rates = VatRates::current();

// Explicitní základ daně a DPH
$line = new InvoiceLine(
    vatRateType: VatRateType::Standard,
    taxBase:     10_000.0,
    vat:         2_100.0,
    rates:       $rates,
);

// Automatický výpočet DPH ze základu
$line = InvoiceLine::fromTaxBase(
    vatRateType: VatRateType::Reduced,
    taxBase:     5_000.0,
    rates:       $rates,
);
// → vat = 600.00 (12 % ze 5 000)

Dobropis se vyjádří záporným základem daně i DPH:

$creditNote = new InvoiceLine(VatRateType::Standard, taxBase: -10_000.0, vat: -2_100.0, rates: $rates);

Invoice — celá faktura

Faktura přijímá kolekci řádků s (potenciálně různými) sazbami. Pokud celková částka včetně DPH překračuje 10 000 Kč, jsou pole partnerVatId, documentNumber a taxPointDate povinná — jejich absence způsobí výjimku při konstrukci.

use JeanCz\CzechVat\Model\Invoice\Invoice;

// Faktura nad prahem — vykazuje se jednotlivě v kontrolním hlášení
$invoice = new Invoice(
    lines: [
        new InvoiceLine(VatRateType::Standard, taxBase: 80_000, vat: 16_800, rates: $rates),
        new InvoiceLine(VatRateType::Reduced,  taxBase: 20_000, vat:  2_400, rates: $rates),
    ],
    partnerVatId:           'CZ87654321',   // povinné nad 10 000 Kč
    documentNumber:         'FAK-2025-042', // povinné nad 10 000 Kč
    supplierDocumentNumber: 'DOD-789',      // číslo dodavatele (pro přijaté faktury)
    taxPointDate:           new DateTimeImmutable('2025-01-20'), // povinné nad 10 000 Kč
    isReverseCharge:        false,          // přenesení daňové povinnosti
);

// Faktura pod prahem — nevyžaduje DIČ partnera ani datum
$smallInvoice = new Invoice([
    new InvoiceLine(VatRateType::Standard, taxBase: 4_000, vat: 840, rates: $rates),
]);

InvoiceCollection — kolekce faktur

use JeanCz\CzechVat\Model\Invoice\InvoiceCollection;

$invoices = (new InvoiceCollection())
    ->addIssued($invoice1, $invoice2, $invoice3)
    ->addReceived($received1, $received2);

Metody addIssued() a addReceived() jsou immutable — vracejí novou instanci.

Zdaňovací období

use JeanCz\CzechVat\Model\Period\TaxPeriod;

// Měsíční plátce (nebo kontrolní hlášení — vždy měsíční)
$period = TaxPeriod::monthly(2025, 1);

// Čtvrtletní plátce (daňové přiznání)
$period = TaxPeriod::quarterly(2025, 1); // Q1

// Odvození z DateTimeInterface
$period = TaxPeriod::fromDate(new DateTimeImmutable('2025-03-15'));
// → monthly(2025, 3)

// Dotazování
$period->isMonthly();   // true / false
$period->isQuarterly(); // true / false
$period->startDate();   // DateTimeImmutable — první den období
$period->endDate();     // DateTimeImmutable — poslední den období
(string) $period;       // '2025-01' nebo '2025-Q1'

Poznámka: Kontrolní hlášení se podává vždy za kalendářní měsíc, i pro čtvrtletní plátce DPH. Čtvrtletní plátce podá tři měsíční kontrolní hlášení a jedno čtvrtletní daňové přiznání.

Plátce

Právnická osoba

use JeanCz\CzechVat\Model\Taxpayer\Taxpayer;
use JeanCz\CzechVat\Enum\VatPayerType;

$taxpayer = Taxpayer::legalEntity(
    vatId:                 'CZ12345678',
    taxOfficeCode:         '452',        // kód FÚ z číselníku UFO
    companyName:           'Moje Firma s.r.o.',
    street:                'Obchodní',
    houseNumber:           '15',
    city:                  'Brno',
    postalCode:            '60200',
    country:               'ČESKÁ REPUBLIKA', // výchozí hodnota
    vatPayerType:          VatPayerType::VatPayer, // výchozí hodnota
    email:                 'dph@mojefirma.cz',
    taxOfficeWorkplaceCode: '2401',      // územní pracoviště (volitelné)
);

Fyzická osoba

$taxpayer = Taxpayer::naturalPerson(
    vatId:                'CZ7001011234',
    taxOfficeCode:        '451',
    firstName:            'Jan',
    lastName:             'Novák',
    street:               'Lipová',
    houseNumber:          '3',
    city:                 'Praha',
    postalCode:           '13000',
    title:                'Ing.',          // titul (volitelné)
    phone:                '123456789',     // telefon (volitelné, max 14 znaků)
    orientationNumber:    '5a',            // číslo orientační (volitelné, max 4 znaky)
    mainEconomicActivity: 6201,            // CZ-NACE kód hlavní ekonomické činnosti (volitelné)
);

Přehled polí specifických pro fyzické osoby:

Parametr XML atribut Umístění Popis
title titul VetaP (KH i DP3) Akademický nebo profesní titul
phone c_telef VetaP (KH i DP3) Kontaktní telefon (max 14 znaků)
orientationNumber c_orient VetaP (KH i DP3) Číslo orientační adresy (max 4 znaky)
mainEconomicActivity c_okec VetaD (pouze DP3) Kód CZ-NACE převažující ekonomické činnosti

Typy plátce (VatPayerType)

Case §ZDPH Popis
VatPayer § 6–6fa Plátce DPH (výchozí)
IdentifiedPerson § 6g–6l Identifikovaná osoba
Group § 5a Skupina
NonPayer108 § 108 Neplátce s povinností přiznat daň
NonPayerAcquisition § 19c Neplátce — pořízení NDP
NonPayerDelivery § 19b Neplátce — dodání NDP

Generování XML

Vstupním bodem je fasáda VatFilingGenerator:

use JeanCz\CzechVat\VatFilingGenerator;

$generator = new VatFilingGenerator(
    taxpayer:        $taxpayer,
    period:          $period,
    invoices:        $invoices,
    softwareName:    'Moje Aplikace',   // volitelné, výchozí 'jeancz/czech-vat'
    softwareVersion: '1.0',             // volitelné
);

Kontrolní hlášení

$xml = $generator->generateControlStatement();

file_put_contents('kh_2025_01.xml', $xml);

Sekce KH, které builder plní automaticky:

Sekce Obsah
VetaA4 Vydaná zdanitelná plnění nad 10 000 Kč (jednotlivě)
VetaA5 Vydaná zdanitelná plnění do 10 000 Kč (souhrnně)
VetaA1 Vydaná plnění v režimu přenesení daňové povinnosti nad prahem
VetaB2 Přijatá zdanitelná plnění nad 10 000 Kč (jednotlivě)
VetaB3 Přijatá zdanitelná plnění do 10 000 Kč (souhrnně)
VetaB1 Přijatá plnění v režimu přenesení daňové povinnosti nad prahem
VetaC Kontrolní součty (křížová kontrola s DP)

Daňové přiznání k DPH

$xml = $generator->generateVatReturn();

file_put_contents('dp_dph_2025_01.xml', $xml);

Sekce DP, které builder plní automaticky:

Sekce Řádky DP Obsah
Veta1 1, 2 Základ daně a daň na výstupu
Veta4 40, 41, 46, 47 Odpočet daně na vstupu
Veta6 62–65 Výsledná daňová povinnost / nadměrný odpočet

Typy podání

Výchozí typ je vždy řádné podání. Ostatní typy se předají jako argument:

use JeanCz\CzechVat\Enum\VatReturnFilingType;
use JeanCz\CzechVat\Enum\ControlStatementFilingType;

// Opravné daňové přiznání (před uplynutím lhůty)
$xml = $generator->generateVatReturn(VatReturnFilingType::Corrective);

// Dodatečné daňové přiznání
$xml = $generator->generateVatReturn(VatReturnFilingType::Supplementary);

// Následné kontrolní hlášení (po zjištění chyby)
$xml = $generator->generateControlStatement(ControlStatementFilingType::Subsequent);

// Následné KH jako odpověď na výzvu správce daně
$xml = $generator->generateControlStatement(
    filingType:             ControlStatementFilingType::Subsequent,
    authorityRequestNumber: '12345678/25/4501-00000-123456',
);

Přehled typů podání

Daňové přiznání (VatReturnFilingType)

Case Kód Popis
Regular B Řádné
Corrective O Opravné (nahrazuje podané řádné)
Supplementary D Dodatečné
SupplementaryCorrective E Dodatečné/opravné

Kontrolní hlášení (ControlStatementFilingType)

Case Kód Popis
Regular B Řádné
RegularCorrective O Řádné/opravné (před uplynutím lhůty)
Subsequent N Následné (po uplynutí lhůty)
SubsequentCorrective E Následné/opravné

XSD validace

Balíček obsahuje přiložená XSD schémata z EPO portálu. Validaci lze volat samostatně kdykoli — například v testech nebo před odesláním.

use JeanCz\CzechVat\Generator\XsdValidator;
use JeanCz\CzechVat\Exception\XmlValidationException;

try {
    XsdValidator::forControlStatement()->validate($ksXml);
    XsdValidator::forVatReturn()->validate($dpXml);
} catch (XmlValidationException $e) {
    echo $e->getMessage();

    // Detailní seznam chyb z libxml
    foreach ($e->errors as $error) {
        echo $error . PHP_EOL;
    }
}

Validátor lze použít i s vlastním XSD souborem:

$validator = new XsdValidator('/cesta/k/memu/schematu.xsd');
$validator->validate($xml);

Výjimky

Všechny výjimky rozšiřují JeanCz\CzechVat\Exception\CzechVatException (která rozšiřuje \RuntimeException), takže je lze chytat hromadně nebo jednotlivě.

Výjimka Kdy se vyhodí
InvalidInvoiceException Prázdný seznam řádků; záporný základ s kladným DPH; faktura nad 10 000 Kč bez povinných polí
InvalidVatRateException Dotaz na typ sazby, který není v sazebníku; záporné procento v VatRates::custom()
InvalidTaxpayerException Chybný formát DIČ; chybějící jméno / název firmy
InvalidPeriodException Měsíc mimo 1–12; čtvrtletí mimo 1–4
XmlGenerationException Interní chyba DOM při sestavování XML
XmlValidationException XML neodpovídá XSD schématu; obsahuje seznam chyb v $e->errors

Architektura

src/
├── VatFilingGenerator.php          ← fasáda, hlavní vstupní bod
│
├── Builder/
│   ├── AbstractXmlBuilder.php      ← sdílené DOM helpery, formátování
│   ├── ControlStatementBuilder.php ← sestavuje DPHKH1
│   └── VatReturnBuilder.php        ← sestavuje DPHDP3
│
├── Contract/
│   ├── XmlGeneratorInterface.php   ← generate(): string
│   └── XmlValidatorInterface.php   ← validate(string): void
│
├── Enum/
│   ├── VatRateType.php             ← Standard | Reduced | SecondReduced | Zero
│   ├── VatReturnFilingType.php     ← Regular | Corrective | Supplementary | …
│   ├── ControlStatementFilingType.php
│   ├── TaxpayerType.php            ← NaturalPerson | LegalEntity
│   └── VatPayerType.php            ← VatPayer | IdentifiedPerson | …
│
├── Exception/                      ← typovaná hierarchie výjimek
│
├── Generator/
│   └── XsdValidator.php            ← volitelná XSD validace přes libxml
│
└── Model/
    ├── Invoice/
    │   ├── InvoiceLine.php         ← jeden řádek: VatRateType + základ + DPH
    │   ├── Invoice.php             ← sada řádků + metadata + validace prahu
    │   └── InvoiceCollection.php   ← vydané + přijaté faktury, agregace
    ├── Period/
    │   └── TaxPeriod.php           ← měsíční nebo čtvrtletní období
    ├── Taxpayer/
    │   └── Taxpayer.php            ← plátce (FO / PO), named constructors
    └── VatRates/
        └── VatRates.php            ← konfigurovatelný sazebník DPH

Klíčové principy:

  • Žádné závislosti — pouze standardní PHP s ext-dom a ext-libxml.
  • Immutable value objectsInvoice, InvoiceLine, TaxPeriod, Taxpayer a InvoiceCollection jsou neměnné po vytvoření.
  • Validace v modeluInvoice vynucuje pravidla (práh 10 000 Kč, povinná pole) při konstrukci, ne až při generování XML.
  • Sazby oddělené od sémantikyVatRateType popisuje kategorii sazby, VatRates nese konkrétní procenta pro dané období.
  • EPO mapování inkapsulovano — historické pojmenování atributů XSD (zakl_dane1, obrat23, dan5, …) je skryto uvnitř builderů a enumu VatRateType::epoAttributeIndex().

Rozšíření

Vlastní builder

Implementujte XmlGeneratorInterface a předejte výsledné XML přímo do XsdValidator:

use JeanCz\CzechVat\Contract\XmlGeneratorInterface;

final class MyCustomVatReturnBuilder implements XmlGeneratorInterface
{
    public function generate(): string
    {
        // vlastní implementace
    }
}

$xml = (new MyCustomVatReturnBuilder(...))->generate();
XsdValidator::forVatReturn()->validate($xml);

Vlastní validátor

Implementujte XmlValidatorInterface pro vlastní validační logiku (např. odesílání na testovací endpoint EPO):

use JeanCz\CzechVat\Contract\XmlValidatorInterface;

final class EpoSandboxValidator implements XmlValidatorInterface
{
    public function validate(string $xml): void
    {
        // HTTP požadavek na testovací EPO endpoint
    }
}

Testování

composer install
./vendor/bin/phpunit

Testovací sada pokrývá unit testy modelů (InvoiceLine, Invoice, TaxPeriod, VatRates) i integrační testy s XSD validací obou výstupních formátů.