Platby + import plateb a nastavení
parent
99dfc9770b
commit
4b4e3acafe
|
|
@ -7,11 +7,39 @@ use Nette\Database\Explorer;
|
||||||
|
|
||||||
class FakturaFacade
|
class FakturaFacade
|
||||||
{
|
{
|
||||||
|
public const STAV_VYSTAVENO = 'ISSUED';
|
||||||
|
public const STAV_CASTECNE = 'PARTIAL';
|
||||||
|
public const STAV_ZAPLACENO = 'PAID';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Explorer $kufrikExplorer
|
private Explorer $kufrikExplorer
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stav úhrady podle součtu plateb vůči částce faktury.
|
||||||
|
*/
|
||||||
|
public static function stavZCastek(float $celkem, float $zaplaceno): string
|
||||||
|
{
|
||||||
|
if ($zaplaceno <= 0.004) {
|
||||||
|
return self::STAV_VYSTAVENO;
|
||||||
|
}
|
||||||
|
if ($celkem > 0 && $zaplaceno + 0.004 >= $celkem) {
|
||||||
|
return self::STAV_ZAPLACENO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAV_CASTECNE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function stavPopis(string $stav): string
|
||||||
|
{
|
||||||
|
return match ($stav) {
|
||||||
|
self::STAV_ZAPLACENO => 'Zaplaceno',
|
||||||
|
self::STAV_CASTECNE => 'Částečně uhrazeno',
|
||||||
|
default => 'Neuhrazeno',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vrátí seznam faktur s dopočítanou celkovou částkou z položek.
|
* Vrátí seznam faktur s dopočítanou celkovou částkou z položek.
|
||||||
*/
|
*/
|
||||||
|
|
@ -36,7 +64,8 @@ class FakturaFacade
|
||||||
$sql = "
|
$sql = "
|
||||||
SELECT
|
SELECT
|
||||||
F.*,
|
F.*,
|
||||||
ISNULL((SELECT SUM(P.CENA) FROM dbo.POLOZKA P WHERE P.FAKTURA = F.ID), 0) AS CASTKA_CELKEM
|
ISNULL((SELECT SUM(P.CENA) FROM dbo.POLOZKA P WHERE P.FAKTURA = F.ID), 0) AS CASTKA_CELKEM,
|
||||||
|
ISNULL((SELECT SUM(PL.CASTKA) FROM dbo.PLATBA PL WHERE PL.FAKTURA = F.ID), 0) AS ZAPLACENO
|
||||||
FROM dbo.FAKTURA F
|
FROM dbo.FAKTURA F
|
||||||
{$whereSql}
|
{$whereSql}
|
||||||
ORDER BY F.DATUM_VYSTAVENI DESC, F.ID DESC
|
ORDER BY F.DATUM_VYSTAVENI DESC, F.ID DESC
|
||||||
|
|
@ -181,7 +210,6 @@ class FakturaFacade
|
||||||
$values = [
|
$values = [
|
||||||
'DATUM_SPLATNOSTI' => $data['DATUM_SPLATNOSTI'],
|
'DATUM_SPLATNOSTI' => $data['DATUM_SPLATNOSTI'],
|
||||||
'DATUM_DUZP' => !empty($data['DATUM_DUZP']) ? $data['DATUM_DUZP'] : null,
|
'DATUM_DUZP' => !empty($data['DATUM_DUZP']) ? $data['DATUM_DUZP'] : null,
|
||||||
'STAV' => $data['STAV'] ?? 'ISSUED',
|
|
||||||
'CINNOSTI' => !empty($data['CINNOSTI']) ? $data['CINNOSTI'] : null,
|
'CINNOSTI' => !empty($data['CINNOSTI']) ? $data['CINNOSTI'] : null,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -296,20 +324,171 @@ class FakturaFacade
|
||||||
return $this->getNextCislo($firmaId, $datum)['cisloAbs'];
|
return $this->getNextCislo($firmaId, $datum)['cisloAbs'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleUhrada(int $id): string
|
/**
|
||||||
|
* Faktury dřív označené jen stavem Zaplaceno dostanou jednu platbu,
|
||||||
|
* a stav všech faktur se srovná se součtem plateb.
|
||||||
|
*/
|
||||||
|
public function reconcileUhrady(): void
|
||||||
{
|
{
|
||||||
$faktura = $this->getById($id);
|
$this->kufrikExplorer->query("
|
||||||
|
INSERT INTO dbo.PLATBA (FAKTURA, CASTKA, DATUM, POZNAMKA)
|
||||||
|
SELECT
|
||||||
|
F.ID,
|
||||||
|
ISNULL((SELECT SUM(P.CENA) FROM dbo.POLOZKA P WHERE P.FAKTURA = F.ID), 0),
|
||||||
|
CAST(ISNULL(F.DATUM_SPLATNOSTI, F.DATUM_VYSTAVENI) AS datetime2),
|
||||||
|
N'Doplněno podle původního stavu Zaplaceno'
|
||||||
|
FROM dbo.FAKTURA F
|
||||||
|
WHERE F.STAV = 'PAID'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM dbo.PLATBA PL WHERE PL.FAKTURA = F.ID)
|
||||||
|
AND ISNULL((SELECT SUM(P.CENA) FROM dbo.POLOZKA P WHERE P.FAKTURA = F.ID), 0) > 0
|
||||||
|
");
|
||||||
|
|
||||||
|
$this->kufrikExplorer->query("
|
||||||
|
UPDATE F
|
||||||
|
SET STAV = CASE
|
||||||
|
WHEN CAST(ISNULL(U.ZAPLACENO, 0) AS decimal(19,4)) <= 0 THEN 'ISSUED'
|
||||||
|
WHEN CAST(ISNULL(C.CELKEM, 0) AS decimal(19,4)) > 0
|
||||||
|
AND CAST(ISNULL(U.ZAPLACENO, 0) AS decimal(19,4)) + 0.004 >= CAST(ISNULL(C.CELKEM, 0) AS decimal(19,4)) THEN 'PAID'
|
||||||
|
ELSE 'PARTIAL'
|
||||||
|
END
|
||||||
|
FROM dbo.FAKTURA F
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT FAKTURA, SUM(CASTKA) AS ZAPLACENO
|
||||||
|
FROM dbo.PLATBA
|
||||||
|
GROUP BY FAKTURA
|
||||||
|
) U ON U.FAKTURA = F.ID
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT FAKTURA, SUM(CENA) AS CELKEM
|
||||||
|
FROM dbo.POLOZKA
|
||||||
|
GROUP BY FAKTURA
|
||||||
|
) C ON C.FAKTURA = F.ID
|
||||||
|
WHERE F.STAV <> CASE
|
||||||
|
WHEN CAST(ISNULL(U.ZAPLACENO, 0) AS decimal(19,4)) <= 0 THEN 'ISSUED'
|
||||||
|
WHEN CAST(ISNULL(C.CELKEM, 0) AS decimal(19,4)) > 0
|
||||||
|
AND CAST(ISNULL(U.ZAPLACENO, 0) AS decimal(19,4)) + 0.004 >= CAST(ISNULL(C.CELKEM, 0) AS decimal(19,4)) THEN 'PAID'
|
||||||
|
ELSE 'PARTIAL'
|
||||||
|
END
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlatby(int $fakturaId): array
|
||||||
|
{
|
||||||
|
return $this->kufrikExplorer->query('
|
||||||
|
SELECT PL.*
|
||||||
|
FROM dbo.PLATBA PL
|
||||||
|
INNER JOIN dbo.FAKTURA F ON F.ID = PL.FAKTURA
|
||||||
|
WHERE PL.FAKTURA = ?
|
||||||
|
ORDER BY PL.DATUM, PL.ID
|
||||||
|
', $fakturaId)->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sumaPlateb(int $fakturaId): float
|
||||||
|
{
|
||||||
|
$row = $this->kufrikExplorer->query('
|
||||||
|
SELECT ISNULL(SUM(PL.CASTKA), 0) AS ZAPLACENO
|
||||||
|
FROM dbo.PLATBA PL
|
||||||
|
INNER JOIN dbo.FAKTURA F ON F.ID = PL.FAKTURA
|
||||||
|
WHERE PL.FAKTURA = ?
|
||||||
|
', $fakturaId)->fetch();
|
||||||
|
|
||||||
|
return (float) ($row->ZAPLACENO ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string Nový stav faktury
|
||||||
|
*/
|
||||||
|
public function addPlatba(int $fakturaId, float $castka, \DateTimeInterface $datum, ?string $poznamka): string
|
||||||
|
{
|
||||||
|
if (!$this->getById($fakturaId)) {
|
||||||
|
throw new \InvalidArgumentException('Faktura nebyla nalezena.');
|
||||||
|
}
|
||||||
|
if ($castka <= 0) {
|
||||||
|
throw new \InvalidArgumentException('Částka platby musí být větší než 0.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$poznamka = trim((string) $poznamka);
|
||||||
|
if (mb_strlen($poznamka) > 2000) {
|
||||||
|
$poznamka = mb_substr($poznamka, 0, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->kufrikExplorer->table('PLATBA')->insert([
|
||||||
|
'FAKTURA' => $fakturaId,
|
||||||
|
'CASTKA' => round($castka, 2),
|
||||||
|
'DATUM' => $datum->format('Y-m-d'),
|
||||||
|
'POZNAMKA' => $poznamka !== '' ? $poznamka : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->syncStav($fakturaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatePlatba(int $id, float $castka, \DateTimeInterface $datum, ?string $poznamka): ?int
|
||||||
|
{
|
||||||
|
$row = $this->kufrikExplorer->query('
|
||||||
|
SELECT PL.ID, PL.FAKTURA
|
||||||
|
FROM dbo.PLATBA PL
|
||||||
|
INNER JOIN dbo.FAKTURA F ON F.ID = PL.FAKTURA
|
||||||
|
WHERE PL.ID = ?
|
||||||
|
', $id)->fetch();
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($castka <= 0) {
|
||||||
|
throw new \InvalidArgumentException('Částka platby musí být větší než 0.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$poznamka = trim((string) $poznamka);
|
||||||
|
if (mb_strlen($poznamka) > 2000) {
|
||||||
|
$poznamka = mb_substr($poznamka, 0, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->kufrikExplorer->table('PLATBA')->where('ID', (int) $row->ID)->update([
|
||||||
|
'CASTKA' => round($castka, 2),
|
||||||
|
'DATUM' => $datum->format('Y-m-d'),
|
||||||
|
'POZNAMKA' => $poznamka !== '' ? $poznamka : null,
|
||||||
|
]);
|
||||||
|
$this->syncStav((int) $row->FAKTURA);
|
||||||
|
|
||||||
|
return (int) $row->FAKTURA;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deletePlatba(int $id): ?int
|
||||||
|
{
|
||||||
|
$row = $this->kufrikExplorer->query('
|
||||||
|
SELECT PL.ID, PL.FAKTURA
|
||||||
|
FROM dbo.PLATBA PL
|
||||||
|
INNER JOIN dbo.FAKTURA F ON F.ID = PL.FAKTURA
|
||||||
|
WHERE PL.ID = ?
|
||||||
|
', $id)->fetch();
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->kufrikExplorer->table('PLATBA')->where('ID', (int) $row->ID)->delete();
|
||||||
|
$this->syncStav((int) $row->FAKTURA);
|
||||||
|
|
||||||
|
return (int) $row->FAKTURA;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncStav(int $fakturaId): string
|
||||||
|
{
|
||||||
|
$faktura = $this->getById($fakturaId);
|
||||||
if (!$faktura) {
|
if (!$faktura) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$novyStav = ($faktura->STAV === 'PAID') ? 'ISSUED' : 'PAID';
|
$celkem = 0.0;
|
||||||
|
foreach ($this->getPolozky($fakturaId) as $polozka) {
|
||||||
|
$celkem += (float) $polozka->CENA;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stav = self::stavZCastek($celkem, $this->sumaPlateb($fakturaId));
|
||||||
$this->kufrikExplorer->table('FAKTURA')
|
$this->kufrikExplorer->table('FAKTURA')
|
||||||
->where('ID', $id)
|
->where('ID', $fakturaId)
|
||||||
->update(['STAV' => $novyStav]);
|
->update(['STAV' => $stav]);
|
||||||
|
|
||||||
return $novyStav;
|
return $stav;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(int $id): void
|
public function delete(int $id): void
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class SdfCompactExporter
|
||||||
/**
|
/**
|
||||||
* Přečte SQL Compact .sdf a vrátí tabulky jako pole.
|
* Přečte SQL Compact .sdf a vrátí tabulky jako pole.
|
||||||
*
|
*
|
||||||
* @return array{uzivatel: array, zakaznik: array, faktura: array, polozka: array, platba: array}
|
* @return array{uzivatel: array, zakaznik: array, faktura: array, polozka: array, platba: array, konfigurace: array}
|
||||||
*/
|
*/
|
||||||
public function export(string $sdfPath): array
|
public function export(string $sdfPath): array
|
||||||
{
|
{
|
||||||
|
|
@ -45,6 +45,7 @@ class SdfCompactExporter
|
||||||
'faktura' => $data['faktura'] ?? [],
|
'faktura' => $data['faktura'] ?? [],
|
||||||
'polozka' => $data['polozka'] ?? [],
|
'polozka' => $data['polozka'] ?? [],
|
||||||
'platba' => $data['platba'] ?? [],
|
'platba' => $data['platba'] ?? [],
|
||||||
|
'konfigurace' => $data['konfigurace'] ?? [],
|
||||||
];
|
];
|
||||||
} finally {
|
} finally {
|
||||||
FileSystem::delete($workDir);
|
FileSystem::delete($workDir);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ namespace App\Model\Sdf;
|
||||||
|
|
||||||
use App\Model\Database\KufrikConnectionFactory;
|
use App\Model\Database\KufrikConnectionFactory;
|
||||||
use App\Model\Facade\DatabazeFacade;
|
use App\Model\Facade\DatabazeFacade;
|
||||||
|
use App\Model\Facade\FakturaFacade;
|
||||||
|
use App\Model\Faktura\FakturaCislovani;
|
||||||
use Nette\Database\Explorer;
|
use Nette\Database\Explorer;
|
||||||
|
|
||||||
class SdfKufrikImporter
|
class SdfKufrikImporter
|
||||||
|
|
@ -24,7 +26,8 @@ class SdfKufrikImporter
|
||||||
* fakturyInserted: int,
|
* fakturyInserted: int,
|
||||||
* fakturySkipped: int,
|
* fakturySkipped: int,
|
||||||
* polozkyInserted: int,
|
* polozkyInserted: int,
|
||||||
* platbyInserted: int
|
* platbyInserted: int,
|
||||||
|
* nastaveniImported: bool
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public function import(int $loginId): array
|
public function import(int $loginId): array
|
||||||
|
|
@ -46,6 +49,7 @@ class SdfKufrikImporter
|
||||||
'faktura' => $this->rows($exported['faktura']),
|
'faktura' => $this->rows($exported['faktura']),
|
||||||
'polozka' => $this->rows($exported['polozka']),
|
'polozka' => $this->rows($exported['polozka']),
|
||||||
'platba' => $this->rows($exported['platba']),
|
'platba' => $this->rows($exported['platba']),
|
||||||
|
'konfigurace' => $this->rows($exported['konfigurace'] ?? []),
|
||||||
];
|
];
|
||||||
$firmaId = (int) $login->FIRMA_ID;
|
$firmaId = (int) $login->FIRMA_ID;
|
||||||
$explorer = $this->kufrikConnectionFactory->createTenantExplorer(
|
$explorer = $this->kufrikConnectionFactory->createTenantExplorer(
|
||||||
|
|
@ -58,6 +62,7 @@ class SdfKufrikImporter
|
||||||
try {
|
try {
|
||||||
$this->wipeTenantData($explorer, $firmaId);
|
$this->wipeTenantData($explorer, $firmaId);
|
||||||
$firmaState = $this->importFirma($explorer, $data['uzivatel'][0] ?? null, $firmaId);
|
$firmaState = $this->importFirma($explorer, $data['uzivatel'][0] ?? null, $firmaId);
|
||||||
|
$nastaveniImported = $this->importNastaveni($explorer, $data['konfigurace'], $firmaId);
|
||||||
$zakaznikMap = $this->importZakaznici($explorer, $data['zakaznik'], $firmaId);
|
$zakaznikMap = $this->importZakaznici($explorer, $data['zakaznik'], $firmaId);
|
||||||
$fakturaResult = $this->importFaktury($explorer, $data, $firmaId, $zakaznikMap);
|
$fakturaResult = $this->importFaktury($explorer, $data, $firmaId, $zakaznikMap);
|
||||||
$explorer->getConnection()->commit();
|
$explorer->getConnection()->commit();
|
||||||
|
|
@ -75,6 +80,7 @@ class SdfKufrikImporter
|
||||||
'fakturySkipped' => $fakturaResult['skipped'],
|
'fakturySkipped' => $fakturaResult['skipped'],
|
||||||
'polozkyInserted' => $fakturaResult['polozky'],
|
'polozkyInserted' => $fakturaResult['polozky'],
|
||||||
'platbyInserted' => $fakturaResult['platby'],
|
'platbyInserted' => $fakturaResult['platby'],
|
||||||
|
'nastaveniImported' => $nastaveniImported,
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->databazeFacade->markSdfImported($loginId, $stats);
|
$this->databazeFacade->markSdfImported($loginId, $stats);
|
||||||
|
|
@ -139,6 +145,79 @@ class SdfKufrikImporter
|
||||||
return 'inserted';
|
return 'inserted';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maska, období číslování a splatnost ze staré tabulky KONFIGURACE.
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $rows
|
||||||
|
*/
|
||||||
|
private function importNastaveni(Explorer $explorer, array $rows, int $firmaId): bool
|
||||||
|
{
|
||||||
|
$xml = null;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$data = $row['DATA'] ?? null;
|
||||||
|
if (is_string($data) && trim($data) !== '') {
|
||||||
|
$xml = $data;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($xml === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nastaveni = $this->parseKonfigurace($xml);
|
||||||
|
if ($nastaveni === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = $explorer->table('FIRMA')->where('ID', $firmaId)->update([
|
||||||
|
'FAKTURA_MASKA' => $nastaveni['maska'],
|
||||||
|
'FAKTURA_CISLOVAT' => $nastaveni['obdobi'],
|
||||||
|
'FAKTURA_SPLATNOST' => $nastaveni['splatnost'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $updated > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{maska: string, obdobi: string, splatnost: int}|null
|
||||||
|
*/
|
||||||
|
private function parseKonfigurace(string $xml): ?array
|
||||||
|
{
|
||||||
|
$xml = preg_replace('/encoding="utf-16"/i', 'encoding="utf-8"', $xml) ?? $xml;
|
||||||
|
$doc = @simplexml_load_string($xml);
|
||||||
|
if ($doc === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maska = $this->maskaZeSdf((string) ($doc->FAKTURA_CISLOVANI_MASKA ?? ''));
|
||||||
|
$zpusob = (int) ($doc->FAKTURA_CISLOVANI_ZPUSOB ?? 1);
|
||||||
|
$splatnost = (int) ($doc->FAKTURA_SPLATNOST ?? FakturaCislovani::SPLATNOST_VYCHOZI);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'maska' => $maska,
|
||||||
|
'obdobi' => $zpusob === 2 ? 'MESIC' : 'ROK',
|
||||||
|
'splatnost' => FakturaCislovani::normalizeSplatnost($splatnost),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stará maska používá (ROK), (MESIC), (CISLO2), (CISLO4) a (CISLO6).
|
||||||
|
*/
|
||||||
|
private function maskaZeSdf(string $maska): string
|
||||||
|
{
|
||||||
|
$maska = trim($maska);
|
||||||
|
if ($maska === '') {
|
||||||
|
return FakturaCislovani::MASKA_VYCHOZI;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prevedena = preg_replace('/\((ROK|MESIC|CISLO[246])\)/', '{$1}', $maska) ?? $maska;
|
||||||
|
if (FakturaCislovani::validateMaska($prevedena) !== null) {
|
||||||
|
return FakturaCislovani::MASKA_VYCHOZI;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $prevedena;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>> $rows
|
* @param array<int, array<string, mixed>> $rows
|
||||||
* @return array{map: array<int, int>, inserted: int, matched: int}
|
* @return array{map: array<int, int>, inserted: int, matched: int}
|
||||||
|
|
@ -211,7 +290,14 @@ class SdfKufrikImporter
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasPlatba = !empty($platbyByFaktura[$oldFakturaId]);
|
$celkemPolozek = 0.0;
|
||||||
|
foreach ($polozkyByFaktura[$oldFakturaId] ?? [] as $polozkaSoucet) {
|
||||||
|
$celkemPolozek += (float) ($polozkaSoucet['CENA'] ?? 0);
|
||||||
|
}
|
||||||
|
$zaplaceno = 0.0;
|
||||||
|
foreach ($platbyByFaktura[$oldFakturaId] ?? [] as $platbaSoucet) {
|
||||||
|
$zaplaceno += (float) ($platbaSoucet['CASTKA'] ?? 0);
|
||||||
|
}
|
||||||
$dBanka = $this->clip($row['D_BANKA'] ?? null, 100);
|
$dBanka = $this->clip($row['D_BANKA'] ?? null, 100);
|
||||||
|
|
||||||
$fakturaId = (int) $explorer->table('FAKTURA')->insert([
|
$fakturaId = (int) $explorer->table('FAKTURA')->insert([
|
||||||
|
|
@ -240,7 +326,7 @@ class SdfKufrikImporter
|
||||||
'O_IC' => $this->clip($row['O_IC'] ?? null, 8),
|
'O_IC' => $this->clip($row['O_IC'] ?? null, 8),
|
||||||
'O_DIC' => $this->clip($row['O_DIC'] ?? null, 10),
|
'O_DIC' => $this->clip($row['O_DIC'] ?? null, 10),
|
||||||
'CINNOSTI' => $row['CINNOSTI'] ?: null,
|
'CINNOSTI' => $row['CINNOSTI'] ?: null,
|
||||||
'STAV' => $hasPlatba ? 'PAID' : 'ISSUED',
|
'STAV' => FakturaFacade::stavZCastek($celkemPolozek, $zaplaceno),
|
||||||
])->ID;
|
])->ID;
|
||||||
|
|
||||||
$poradi = 1;
|
$poradi = 1;
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,13 @@ class AdminPresenter extends BasePresenter
|
||||||
$stats = $this->sdfImporter->import($loginId);
|
$stats = $this->sdfImporter->import($loginId);
|
||||||
$this->flashMessage(
|
$this->flashMessage(
|
||||||
sprintf(
|
sprintf(
|
||||||
'Import ze SDF do databáze %s dokončen. Původní data byla smazána. Zákazníci: %d. Faktury: %d. Položky: %d. Platby: %d.',
|
'Import ze SDF do databáze %s dokončen. Původní data byla smazána. Zákazníci: %d. Faktury: %d. Položky: %d. Platby: %d. Nastavení: %s.',
|
||||||
$stats['database'],
|
$stats['database'],
|
||||||
$stats['zakazniciInserted'],
|
$stats['zakazniciInserted'],
|
||||||
$stats['fakturyInserted'],
|
$stats['fakturyInserted'],
|
||||||
$stats['polozkyInserted'],
|
$stats['polozkyInserted'],
|
||||||
$stats['platbyInserted']
|
$stats['platbyInserted'],
|
||||||
|
!empty($stats['nastaveniImported']) ? 'přeneseno' : 'bez změn'
|
||||||
),
|
),
|
||||||
'success'
|
'success'
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
namespace App\UI\Faktura;
|
namespace App\UI\Faktura;
|
||||||
|
|
||||||
|
use App\Model\Faktura\FakturaPdf;
|
||||||
use App\UI\BasePresenter;
|
use App\UI\BasePresenter;
|
||||||
use App\Model\Facade\FakturaFacade;
|
use App\Model\Facade\FakturaFacade;
|
||||||
use Nette\Application\Attributes\Persistent;
|
use Nette\Application\Attributes\Persistent;
|
||||||
|
use Nette\Application\Responses\CallbackResponse;
|
||||||
|
|
||||||
class FakturaPresenter extends BasePresenter
|
class FakturaPresenter extends BasePresenter
|
||||||
{
|
{
|
||||||
|
|
@ -18,11 +20,33 @@ class FakturaPresenter extends BasePresenter
|
||||||
public string $mesic = '';
|
public string $mesic = '';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private FakturaFacade $fakturaFacade
|
private FakturaFacade $fakturaFacade,
|
||||||
|
private FakturaPdf $fakturaPdf,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function actionPdf(int $id, bool $tisk = false): void
|
||||||
|
{
|
||||||
|
$faktura = $this->fakturaFacade->getById($id);
|
||||||
|
if (!$faktura) {
|
||||||
|
$this->error('Faktura nebyla nalezena.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf = $this->fakturaPdf->render($faktura, $this->fakturaFacade->getPolozky($id), $tisk);
|
||||||
|
$fileName = FakturaPdf::fileName((string) $faktura->CISLO);
|
||||||
|
$disposition = $tisk ? 'inline' : 'attachment';
|
||||||
|
|
||||||
|
$this->sendResponse(new CallbackResponse(function ($httpRequest, $httpResponse) use ($pdf, $fileName, $disposition): void {
|
||||||
|
$httpResponse->setContentType('application/pdf');
|
||||||
|
$httpResponse->setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
$disposition . '; filename="' . $fileName . '"'
|
||||||
|
);
|
||||||
|
echo $pdf;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
protected function startup(): void
|
protected function startup(): void
|
||||||
{
|
{
|
||||||
parent::startup();
|
parent::startup();
|
||||||
|
|
@ -56,21 +80,32 @@ class FakturaPresenter extends BasePresenter
|
||||||
$rok = $this->rok ?? (int) date('Y');
|
$rok = $this->rok ?? (int) date('Y');
|
||||||
$mesic = $this->normalizeMesic($this->mesic);
|
$mesic = $this->normalizeMesic($this->mesic);
|
||||||
|
|
||||||
|
$this->fakturaFacade->reconcileUhrady();
|
||||||
$faktury = $this->fakturaFacade->getFiltered($rok, $mesic);
|
$faktury = $this->fakturaFacade->getFiltered($rok, $mesic);
|
||||||
|
|
||||||
$celkemSum = 0;
|
$celkemSum = 0;
|
||||||
|
$zbyvaSum = 0;
|
||||||
$neuhrazenoCount = 0;
|
$neuhrazenoCount = 0;
|
||||||
|
|
||||||
foreach ($faktury as $f) {
|
foreach ($faktury as $f) {
|
||||||
$celkemSum += (float) ($f->CASTKA_CELKEM ?? 0);
|
$celkem = (float) ($f->CASTKA_CELKEM ?? 0);
|
||||||
if ($f->STAV !== 'PAID') {
|
$zaplaceno = (float) ($f->ZAPLACENO ?? 0);
|
||||||
|
$zbyva = max(0, round($celkem - $zaplaceno, 2));
|
||||||
|
$f->STAV_UHRADY = FakturaFacade::stavZCastek($celkem, $zaplaceno);
|
||||||
|
$f->ZBYVA = $zbyva;
|
||||||
|
|
||||||
|
$celkemSum += $celkem;
|
||||||
|
$zbyvaSum += $zbyva;
|
||||||
|
if ($f->STAV_UHRADY !== FakturaFacade::STAV_ZAPLACENO) {
|
||||||
$neuhrazenoCount++;
|
$neuhrazenoCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->template->faktury = $faktury;
|
$this->template->faktury = $faktury;
|
||||||
$this->template->celkemSum = $celkemSum;
|
$this->template->celkemSum = $celkemSum;
|
||||||
|
$this->template->zbyvaSum = $zbyvaSum;
|
||||||
$this->template->neuhrazenoCount = $neuhrazenoCount;
|
$this->template->neuhrazenoCount = $neuhrazenoCount;
|
||||||
|
$this->template->dnes = date('Y-m-d');
|
||||||
$this->template->selectedRok = $rok;
|
$this->template->selectedRok = $rok;
|
||||||
$this->template->selectedMesic = $mesic;
|
$this->template->selectedMesic = $mesic;
|
||||||
$this->template->roky = $this->fakturaFacade->getDostupneRoky($rok);
|
$this->template->roky = $this->fakturaFacade->getDostupneRoky($rok);
|
||||||
|
|
@ -146,23 +181,6 @@ class FakturaPresenter extends BasePresenter
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Přepnutí stavu zaplaceno / neuhrazeno
|
|
||||||
*/
|
|
||||||
public function handleUhradit(int $id): void
|
|
||||||
{
|
|
||||||
$novyStav = $this->fakturaFacade->toggleUhrada($id);
|
|
||||||
|
|
||||||
if ($novyStav !== '') {
|
|
||||||
$this->flashMessage(
|
|
||||||
$novyStav === 'PAID' ? 'Faktura byla označena jako zaplacená.' : 'Stav faktury byl změněn na neuhrazeno.',
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->redirect('this');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smazání faktury
|
* Smazání faktury
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@
|
||||||
<div class="fs-4 fw-bold text-warning">{$neuhrazenoCount} ks</div>
|
<div class="fs-4 fw-bold text-warning">{$neuhrazenoCount} ks</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm p-3 bg-white border-start border-danger border-4">
|
||||||
|
<div class="text-muted small">Zbývá uhradit</div>
|
||||||
|
<div class="fs-4 fw-bold text-danger">{$zbyvaSum|number:2, ',', ' '} CZK</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filtrační lišta (Rok / Měsíc) -->
|
<!-- Filtrační lišta (Rok / Měsíc) -->
|
||||||
|
|
@ -70,6 +76,7 @@
|
||||||
<th>Vystaveno</th>
|
<th>Vystaveno</th>
|
||||||
<th>Splatnost</th>
|
<th>Splatnost</th>
|
||||||
<th class="text-end">Částka</th>
|
<th class="text-end">Částka</th>
|
||||||
|
<th class="text-end">Zaplaceno</th>
|
||||||
<th class="text-center">Stav</th>
|
<th class="text-center">Stav</th>
|
||||||
<th class="text-end" data-orderable="false">Akce</th>
|
<th class="text-end" data-orderable="false">Akce</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -79,7 +86,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<!-- Číslo dokladu -->
|
<!-- Číslo dokladu -->
|
||||||
<td data-order="{$f->CISLO}">
|
<td data-order="{$f->CISLO}">
|
||||||
<a n:href="FakturaDetail:default $f->ID" class="fw-bold text-decoration-none">
|
<a n:href="FakturaEdit:edit $f->ID" class="fw-bold text-decoration-none">
|
||||||
{$f->CISLO}
|
{$f->CISLO}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -90,38 +97,38 @@
|
||||||
{$f->DATUM_VYSTAVENI|date:'d.m.Y'}
|
{$f->DATUM_VYSTAVENI|date:'d.m.Y'}
|
||||||
</td>
|
</td>
|
||||||
<!-- Datum splatnosti -->
|
<!-- Datum splatnosti -->
|
||||||
<td data-order="{$f->DATUM_SPLATNOSTI|date:'Ymd'}">
|
<td data-order="{$f->DATUM_SPLATNOSTI|date:'Ymd'}"
|
||||||
|
class="{if $f->STAV_UHRADY !== 'PAID' && ($f->DATUM_SPLATNOSTI|date:'Y-m-d') < $dnes}text-danger fw-bold{/if}">
|
||||||
{$f->DATUM_SPLATNOSTI|date:'d.m.Y'}
|
{$f->DATUM_SPLATNOSTI|date:'d.m.Y'}
|
||||||
</td>
|
</td>
|
||||||
<!-- Částka celkem -->
|
<!-- Částka celkem -->
|
||||||
<td class="text-end fw-bold" data-order="{$f->CASTKA_CELKEM}">
|
<td class="text-end fw-bold" data-order="{$f->CASTKA_CELKEM}">
|
||||||
{$f->CASTKA_CELKEM|number:2, ',', ' '} CZK
|
{$f->CASTKA_CELKEM|number:2, ',', ' '} CZK
|
||||||
</td>
|
</td>
|
||||||
|
<!-- Zaplaceno -->
|
||||||
|
<td class="text-end {if $f->STAV_UHRADY === 'PAID'}text-success{elseif $f->ZAPLACENO > 0}text-warning{else}text-danger{/if}"
|
||||||
|
data-order="{$f->ZAPLACENO}">
|
||||||
|
{$f->ZAPLACENO|number:2, ',', ' '} CZK
|
||||||
|
</td>
|
||||||
<!-- Stav -->
|
<!-- Stav -->
|
||||||
<td class="text-center" data-order="{$f->STAV}">
|
<td class="text-center" data-order="{$f->STAV_UHRADY}">
|
||||||
<span class="badge {if $f->STAV === 'PAID'}bg-success{else}bg-warning text-dark{/if}">
|
<span class="badge {if $f->STAV_UHRADY === 'PAID'}bg-success{elseif $f->STAV_UHRADY === 'PARTIAL'}bg-info text-dark{else}bg-warning text-dark{/if}">
|
||||||
{if $f->STAV === 'PAID'}Zaplaceno{else}Neuhrazeno{/if}
|
{if $f->STAV_UHRADY === 'PAID'}Zaplaceno{elseif $f->STAV_UHRADY === 'PARTIAL'}Částečně{else}Neuhrazeno{/if}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<!-- Sloupec AKCE -->
|
<!-- Sloupec AKCE -->
|
||||||
<td class="text-end text-nowrap">
|
<td class="text-end text-nowrap">
|
||||||
<!-- Přepnutí stavu úhrady -->
|
<a href="{link FakturaEdit:edit $f->ID}#uhrady"
|
||||||
<a n:href="uhradit! $f->ID"
|
class="btn btn-sm {if $f->STAV_UHRADY === 'PAID'}btn-outline-secondary{else}btn-outline-success{/if} me-1"
|
||||||
class="btn btn-sm {if $f->STAV === 'PAID'}btn-outline-warning{else}btn-outline-success{/if} me-1"
|
title="Upravit úhrady">
|
||||||
title="{if $f->STAV === 'PAID'}Označit jako neuhrazeno{else}Označit jako zaplaceno{/if}">
|
<i class="bi bi-cash-coin"></i>
|
||||||
<i class="bi {if $f->STAV === 'PAID'}bi-x-circle{else}bi-check-circle{/if}"></i>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Detail faktury -->
|
<a n:href="pdf $f->ID" class="btn btn-sm btn-outline-secondary me-1" title="Stáhnout PDF">
|
||||||
<a n:href="FakturaDetail:default $f->ID" class="btn btn-sm btn-outline-info me-1" title="Zobrazit detail">
|
|
||||||
<i class="bi bi-eye"></i>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a n:href="FakturaDetail:pdf $f->ID" class="btn btn-sm btn-outline-secondary me-1" title="Stáhnout PDF">
|
|
||||||
<i class="bi bi-file-earmark-pdf"></i>
|
<i class="bi bi-file-earmark-pdf"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a n:href="FakturaDetail:pdf $f->ID, tisk => true" target="_blank" class="btn btn-sm btn-outline-dark me-1" title="Tisk">
|
<a n:href="pdf $f->ID, tisk => true" target="_blank" class="btn btn-sm btn-outline-dark me-1" title="Tisk">
|
||||||
<i class="bi bi-printer"></i>
|
<i class="bi bi-printer"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\UI\FakturaDetail;
|
|
||||||
|
|
||||||
use App\Model\Faktura\FakturaPdf;
|
|
||||||
use App\UI\BasePresenter;
|
|
||||||
use App\Model\Facade\FakturaFacade;
|
|
||||||
use Nette\Application\Responses\CallbackResponse;
|
|
||||||
|
|
||||||
class FakturaDetailPresenter extends BasePresenter
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private FakturaFacade $fakturaFacade,
|
|
||||||
private FakturaPdf $fakturaPdf,
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function actionPdf(int $id, bool $tisk = false): void
|
|
||||||
{
|
|
||||||
$faktura = $this->fakturaFacade->getById($id);
|
|
||||||
if (!$faktura) {
|
|
||||||
$this->error('Faktura nebyla nalezena.', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdf = $this->fakturaPdf->render($faktura, $this->fakturaFacade->getPolozky($id), $tisk);
|
|
||||||
$fileName = FakturaPdf::fileName((string) $faktura->CISLO);
|
|
||||||
$disposition = $tisk ? 'inline' : 'attachment';
|
|
||||||
|
|
||||||
$this->sendResponse(new CallbackResponse(function ($httpRequest, $httpResponse) use ($pdf, $fileName, $disposition): void {
|
|
||||||
$httpResponse->setContentType('application/pdf');
|
|
||||||
$httpResponse->setHeader(
|
|
||||||
'Content-Disposition',
|
|
||||||
$disposition . '; filename="' . $fileName . '"'
|
|
||||||
);
|
|
||||||
echo $pdf;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function renderDefault(int $id): void
|
|
||||||
{
|
|
||||||
$faktura = $this->fakturaFacade->getById($id);
|
|
||||||
|
|
||||||
if (!$faktura) {
|
|
||||||
$this->flashMessage('Faktura nebyla nalezena.', 'danger');
|
|
||||||
$this->redirect('Faktura:default');
|
|
||||||
}
|
|
||||||
|
|
||||||
$polozky = $this->fakturaFacade->getPolozky($id);
|
|
||||||
|
|
||||||
// Výpočet celkové částky
|
|
||||||
$celkem = 0;
|
|
||||||
foreach ($polozky as $p) {
|
|
||||||
$celkem += (float) $p->CENA;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->template->faktura = $faktura;
|
|
||||||
$this->template->polozky = $polozky;
|
|
||||||
$this->template->celkem = $celkem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
{block content}
|
|
||||||
|
|
||||||
<div class="container-fluid py-3">
|
|
||||||
<!-- Ovládací lišta (při tisku se schová) -->
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
|
||||||
<div>
|
|
||||||
<a n:href="Faktura:default" class="btn btn-outline-secondary">
|
|
||||||
<i class="bi bi-arrow-left me-1"></i>Zpět na seznam
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a n:href="FakturaEdit:edit $faktura->ID" class="btn btn-outline-primary me-2">
|
|
||||||
<i class="bi bi-pencil me-1"></i>Upravit
|
|
||||||
</a>
|
|
||||||
<a n:href="pdf $faktura->ID" class="btn btn-outline-secondary me-2">
|
|
||||||
<i class="bi bi-file-earmark-pdf me-1"></i>PDF
|
|
||||||
</a>
|
|
||||||
<a n:href="pdf $faktura->ID, tisk => true" target="_blank" class="btn btn-success">
|
|
||||||
<i class="bi bi-printer me-1"></i>Tisk
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tělo faktury (Formát A4 náhled) -->
|
|
||||||
<div class="card shadow-sm border-0 p-4 p-md-5 bg-white mx-auto invoice-box" style="max-width: 900px;">
|
|
||||||
<!-- Hlavička faktury -->
|
|
||||||
<div class="row pb-4 mb-4 border-bottom">
|
|
||||||
<div class="col-6">
|
|
||||||
<h1 class="h3 fw-bold mb-1">FAKTURA - DAŇOVÝ DOKLAD</h1>
|
|
||||||
<div class="fs-5 text-muted">Číslo: <strong>{$faktura->CISLO}</strong></div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 text-end">
|
|
||||||
<span class="badge {if $faktura->STAV === 'PAID'}bg-success{else}bg-warning text-dark{/if} fs-6 p-2 d-print-none">
|
|
||||||
{if $faktura->STAV === 'PAID'}UHRATENO{else}NEUHRATENO{/if}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dodavatel a Odběratel (ze snapshotů D_* a O_*) -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<!-- DODAVATEL -->
|
|
||||||
<div class="col-6">
|
|
||||||
<h6 class="text-uppercase text-muted fw-bold mb-2">Dodavatel</h6>
|
|
||||||
<div class="fw-bold fs-5">{$faktura->D_PRAJM ?: '—'}</div>
|
|
||||||
<div>{$faktura->D_ULICE}</div>
|
|
||||||
<div>{$faktura->D_PSC} {$faktura->D_MESTO}</div>
|
|
||||||
|
|
||||||
<div class="mt-2">
|
|
||||||
{if $faktura->D_IC}<strong>IČ:</strong> {$faktura->D_IC}<br>{/if}
|
|
||||||
{if $faktura->D_DIC}<strong>DIČ:</strong> {$faktura->D_DIC}<br>{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{if $faktura->D_BANKA}
|
|
||||||
<div class="mt-2">
|
|
||||||
<strong>Bankovní účet:</strong><br>
|
|
||||||
<span class="fw-bold">{$faktura->D_BANKA}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ODBĚRATELE -->
|
|
||||||
<div class="col-6">
|
|
||||||
<h6 class="text-uppercase text-muted fw-bold mb-2">Odběratel</h6>
|
|
||||||
<div class="fw-bold fs-5">{$faktura->O_PRAJM ?: '—'}</div>
|
|
||||||
<div>{$faktura->O_ULICE}</div>
|
|
||||||
<div>{$faktura->O_PSC} {$faktura->O_MESTO}</div>
|
|
||||||
|
|
||||||
<div class="mt-2">
|
|
||||||
{if $faktura->O_IC}<strong>IČ:</strong> {$faktura->O_IC}<br>{/if}
|
|
||||||
{if $faktura->O_DIC}<strong>DIČ:</strong> {$faktura->O_DIC}<br>{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Datuma -->
|
|
||||||
<div class="row my-3 p-3 bg-light rounded text-center">
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="text-muted small">Datum vystavení:</div>
|
|
||||||
<div class="fw-bold">{$faktura->DATUM_VYSTAVENI|date:'d.m.Y'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="text-muted small">Datum DUZP:</div>
|
|
||||||
<div class="fw-bold">{if $faktura->DATUM_DUZP}{$faktura->DATUM_DUZP|date:'d.m.Y'}{else}—{/if}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="text-muted small">Datum splatnosti:</div>
|
|
||||||
<div class="fw-bold text-danger">{$faktura->DATUM_SPLATNOSTI|date:'d.m.Y'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Činnosti / Popis -->
|
|
||||||
<div n:if="$faktura->CINNOSTI" class="mb-4">
|
|
||||||
<h6 class="text-uppercase text-muted fw-bold mb-1">Popis plnění / Činnosti:</h6>
|
|
||||||
<p class="mb-0 text-break" style="white-space: pre-line;">{$faktura->CINNOSTI}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabulka položek -->
|
|
||||||
<div class="table-responsive my-4">
|
|
||||||
<table class="table table-striped align-middle">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th style="width: 40px;" class="text-center">#</th>
|
|
||||||
<th>Položka</th>
|
|
||||||
<th style="width: 90px;" class="text-end">Počet</th>
|
|
||||||
<th style="width: 70px;">Jedn.</th>
|
|
||||||
<th style="width: 120px;" class="text-end">Cena/J.</th>
|
|
||||||
<th style="width: 80px;" class="text-end">Sleva</th>
|
|
||||||
<th style="width: 130px;" class="text-end">Celkem</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{foreach $polozky as $p}
|
|
||||||
<tr>
|
|
||||||
<td class="text-center text-muted fw-bold">{$p->PORADI}</td>
|
|
||||||
<td>{$p->NAZEV}</td>
|
|
||||||
<td class="text-end">{$p->POCET}</td>
|
|
||||||
<td>{$p->JEDNOTKA}</td>
|
|
||||||
<td class="text-end">{$p->CENA_JEDNOTKA|number:2, ',', ' '} CZK</td>
|
|
||||||
<td class="text-end">{if $p->SLEVA > 0}{$p->SLEVA} %{else}—{/if}</td>
|
|
||||||
<td class="text-end fw-bold">{$p->CENA|number:2, ',', ' '} CZK</td>
|
|
||||||
</tr>
|
|
||||||
{/foreach}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Rekapitulace a součet -->
|
|
||||||
<div class="row justify-content-end my-3">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<div class="d-flex justify-content-between align-items-center p-3 bg-primary text-white rounded">
|
|
||||||
<span class="fs-5 fw-bold">Celkem k úhradě:</span>
|
|
||||||
<span class="fs-4 fw-bold">{$celkem|number:2, ',', ' '} CZK</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@media print {
|
|
||||||
body {
|
|
||||||
background: #fff !important;
|
|
||||||
}
|
|
||||||
.d-print-none {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.invoice-box {
|
|
||||||
box-shadow: none !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -89,8 +89,46 @@ class FakturaEditPresenter extends BasePresenter
|
||||||
{
|
{
|
||||||
$this->template->isEdit = (bool) $this->id;
|
$this->template->isEdit = (bool) $this->id;
|
||||||
|
|
||||||
// Pokud upravujeme, načteme stávající položky, jinak připravíme prázdné pole pro JS/Latte
|
$polozky = $this->id ? $this->fakturaFacade->getPolozky($this->id) : [];
|
||||||
$this->template->polozky = $this->id ? $this->fakturaFacade->getPolozky($this->id) : [];
|
$platby = $this->id ? $this->fakturaFacade->getPlatby($this->id) : [];
|
||||||
|
|
||||||
|
$celkem = 0.0;
|
||||||
|
foreach ($polozky as $polozka) {
|
||||||
|
$celkem += (float) $polozka->CENA;
|
||||||
|
}
|
||||||
|
$zaplaceno = 0.0;
|
||||||
|
foreach ($platby as $platba) {
|
||||||
|
$zaplaceno += (float) $platba->CASTKA;
|
||||||
|
}
|
||||||
|
|
||||||
|
$splatnostIso = date('Y-m-d');
|
||||||
|
if ($this->id) {
|
||||||
|
$faktura = $this->fakturaFacade->getById($this->id);
|
||||||
|
if ($faktura && $faktura->DATUM_SPLATNOSTI) {
|
||||||
|
$splatnostIso = $faktura->DATUM_SPLATNOSTI->format('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->template->polozky = $polozky;
|
||||||
|
$this->template->platby = $platby;
|
||||||
|
$this->template->celkemFaktury = $celkem;
|
||||||
|
$this->template->zaplaceno = $zaplaceno;
|
||||||
|
$zbyva = max(0, round($celkem - $zaplaceno, 2));
|
||||||
|
$this->template->zbyva = $zbyva;
|
||||||
|
$this->template->zbyvaInput = $zbyva > 0 ? number_format($zbyva, 2, '.', '') : '';
|
||||||
|
$this->template->stavUhrady = FakturaFacade::stavZCastek($celkem, $zaplaceno);
|
||||||
|
$this->template->splatnostIso = $splatnostIso;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleSmazatPlatbu(int $platbaId): void
|
||||||
|
{
|
||||||
|
if ($this->fakturaFacade->deletePlatba($platbaId) === null) {
|
||||||
|
$this->flashMessage('Platbu se nepodařilo smazat.', 'danger');
|
||||||
|
} else {
|
||||||
|
$this->flashMessage('Platba byla smazána.', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redirect('this');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createComponentFakturaForm(): Form
|
protected function createComponentFakturaForm(): Form
|
||||||
|
|
@ -136,11 +174,6 @@ class FakturaEditPresenter extends BasePresenter
|
||||||
$form->addText('DATUM_DUZP', 'Datum UZP:')
|
$form->addText('DATUM_DUZP', 'Datum UZP:')
|
||||||
->setHtmlType('date');
|
->setHtmlType('date');
|
||||||
|
|
||||||
$form->addSelect('STAV', 'Stav:', [
|
|
||||||
'ISSUED' => 'Vystaveno (Neuhrazeno)',
|
|
||||||
'PAID' => 'Zaplaceno',
|
|
||||||
])->setDefaultValue('ISSUED');
|
|
||||||
|
|
||||||
$form->addTextArea('CINNOSTI', 'Činnosti / Popis plnění:');
|
$form->addTextArea('CINNOSTI', 'Činnosti / Popis plnění:');
|
||||||
|
|
||||||
$form->addSubmit('save', 'Uložit fakturu')
|
$form->addSubmit('save', 'Uložit fakturu')
|
||||||
|
|
@ -164,6 +197,7 @@ class FakturaEditPresenter extends BasePresenter
|
||||||
|
|
||||||
// 3. Uložení položek s automatickým výpočtem PORADI a celkové ceny
|
// 3. Uložení položek s automatickým výpočtem PORADI a celkové ceny
|
||||||
$this->fakturaFacade->savePolozky($fakturaId, $polozkyRaw, $firmaId);
|
$this->fakturaFacade->savePolozky($fakturaId, $polozkyRaw, $firmaId);
|
||||||
|
$this->fakturaFacade->syncStav($fakturaId);
|
||||||
|
|
||||||
$this->flashMessage(
|
$this->flashMessage(
|
||||||
$this->id ? 'Faktura byla úspěšně upravena.' : 'Nová faktura byla vystavena.',
|
$this->id ? 'Faktura byla úspěšně upravena.' : 'Nová faktura byla vystavena.',
|
||||||
|
|
@ -172,4 +206,98 @@ class FakturaEditPresenter extends BasePresenter
|
||||||
|
|
||||||
$this->redirect('Faktura:default');
|
$this->redirect('Faktura:default');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function createComponentPlatbyForm(): Form
|
||||||
|
{
|
||||||
|
$form = new Form;
|
||||||
|
$platby = $form->addContainer('platby');
|
||||||
|
|
||||||
|
if ($this->id) {
|
||||||
|
foreach ($this->fakturaFacade->getPlatby($this->id) as $platba) {
|
||||||
|
$datum = $platba->DATUM instanceof \DateTimeInterface
|
||||||
|
? $platba->DATUM->format('Y-m-d')
|
||||||
|
: substr((string) $platba->DATUM, 0, 10);
|
||||||
|
|
||||||
|
$row = $platby->addContainer((string) $platba->ID);
|
||||||
|
$row->addText('castka')
|
||||||
|
->setHtmlType('number')
|
||||||
|
->setHtmlAttribute('step', '0.01')
|
||||||
|
->setHtmlAttribute('min', '0.01')
|
||||||
|
->setDefaultValue(number_format((float) $platba->CASTKA, 2, '.', ''))
|
||||||
|
->setRequired('Zadejte částku platby.')
|
||||||
|
->addRule(Form::FLOAT, 'Částka musí být číslo.')
|
||||||
|
->addRule(Form::MIN, 'Částka musí být větší než 0.', 0.01);
|
||||||
|
$row->addText('datum')
|
||||||
|
->setHtmlType('date')
|
||||||
|
->setDefaultValue($datum)
|
||||||
|
->setRequired('Zadejte datum platby.');
|
||||||
|
$row->addText('poznamka')
|
||||||
|
->setDefaultValue((string) ($platba->POZNAMKA ?? ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$nova = $form->addContainer('nova');
|
||||||
|
$nova->addText('castka')
|
||||||
|
->setHtmlType('number')
|
||||||
|
->setHtmlAttribute('step', '0.01')
|
||||||
|
->setHtmlAttribute('min', '0.01')
|
||||||
|
->setHtmlAttribute('id', 'edit-platba-castka')
|
||||||
|
->setHtmlAttribute('placeholder', 'Částka')
|
||||||
|
->addCondition(Form::FILLED)
|
||||||
|
->addRule(Form::FLOAT, 'Částka musí být číslo.')
|
||||||
|
->addRule(Form::MIN, 'Částka musí být větší než 0.', 0.01);
|
||||||
|
$nova->addText('datum')
|
||||||
|
->setHtmlType('date')
|
||||||
|
->setHtmlAttribute('id', 'edit-platba-datum')
|
||||||
|
->setDefaultValue(date('Y-m-d'));
|
||||||
|
$nova->addText('poznamka')
|
||||||
|
->setHtmlAttribute('id', 'edit-platba-poznamka');
|
||||||
|
|
||||||
|
$form->addSubmit('save', 'Uložit platby')
|
||||||
|
->setHtmlAttribute('class', 'btn btn-success');
|
||||||
|
$form->onSuccess[] = $this->platbyFormSucceeded(...);
|
||||||
|
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function platbyFormSucceeded(Form $form, array $data): void
|
||||||
|
{
|
||||||
|
if (!$this->id) {
|
||||||
|
$form->addError('Platby lze zadat až u uložené faktury.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($data['platby'] ?? [] as $platbaId => $platba) {
|
||||||
|
$this->fakturaFacade->updatePlatba(
|
||||||
|
(int) $platbaId,
|
||||||
|
(float) $platba['castka'],
|
||||||
|
new \DateTimeImmutable((string) $platba['datum']),
|
||||||
|
(string) ($platba['poznamka'] ?? '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$novaCastka = trim((string) ($data['nova']['castka'] ?? ''));
|
||||||
|
if ($novaCastka !== '') {
|
||||||
|
$datumText = trim((string) ($data['nova']['datum'] ?? ''));
|
||||||
|
$datum = $datumText !== ''
|
||||||
|
? new \DateTimeImmutable($datumText)
|
||||||
|
: new \DateTimeImmutable('today');
|
||||||
|
$this->fakturaFacade->addPlatba(
|
||||||
|
$this->id,
|
||||||
|
(float) $novaCastka,
|
||||||
|
$datum,
|
||||||
|
(string) ($data['nova']['poznamka'] ?? '')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fakturaFacade->syncStav($this->id);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$form->addError($e->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->flashMessage('Platby byly uloženy.', 'success');
|
||||||
|
$this->redirect('this');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,9 +43,14 @@
|
||||||
<label n:name="DATUM_SPLATNOSTI" class="form-label">Datum splatnosti</label>
|
<label n:name="DATUM_SPLATNOSTI" class="form-label">Datum splatnosti</label>
|
||||||
<input n:name="DATUM_SPLATNOSTI" class="form-control">
|
<input n:name="DATUM_SPLATNOSTI" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4" n:if="$isEdit">
|
||||||
<label n:name="STAV" class="form-label">Stav úhrady</label>
|
<label class="form-label">Stav úhrady</label>
|
||||||
<select n:name="STAV" class="form-select"></select>
|
<div class="form-control bg-light">
|
||||||
|
<span class="badge {if $stavUhrady === 'PAID'}bg-success{elseif $stavUhrady === 'PARTIAL'}bg-info text-dark{else}bg-warning text-dark{/if}">
|
||||||
|
{if $stavUhrady === 'PAID'}Zaplaceno{elseif $stavUhrady === 'PARTIAL'}Částečně uhrazeno{else}Neuhrazeno{/if}
|
||||||
|
</span>
|
||||||
|
<span class="ms-2 text-muted">zbývá {$zbyva|number:2, ',', ' '} CZK</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
|
|
@ -115,8 +120,94 @@
|
||||||
<a n:href="Faktura:default" class="btn btn-outline-secondary">Zrušit</a>
|
<a n:href="Faktura:default" class="btn btn-outline-secondary">Zrušit</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div n:if="$isEdit" id="uhrady" class="mt-4 pt-3 border-top">
|
||||||
|
<h5 class="mb-3"><i class="bi bi-cash-coin me-2"></i>Úhrady</h5>
|
||||||
|
<p class="text-muted small">
|
||||||
|
Celkem {$celkemFaktury|number:2, ',', ' '} CZK,
|
||||||
|
uhrazeno {$zaplaceno|number:2, ',', ' '} CZK,
|
||||||
|
zbývá {$zbyva|number:2, ',', ' '} CZK.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form n:name="platbyForm">
|
||||||
|
<div n:if="$presenter['platbyForm']->hasErrors()" class="alert alert-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li n:foreach="$presenter['platbyForm']->errors as $error">{$error}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive mb-3">
|
||||||
|
<table class="table table-sm align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 160px;">Datum</th>
|
||||||
|
<th style="width: 160px;" class="text-end">Částka</th>
|
||||||
|
<th>Poznámka</th>
|
||||||
|
<th style="width: 50px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr n:if="count($platby) === 0">
|
||||||
|
<td colspan="4" class="text-muted">Zatím žádná platba.</td>
|
||||||
|
</tr>
|
||||||
|
<tr n:foreach="$form['platby']->getComponents() as $platbaId => $row">
|
||||||
|
<td>{input $row['datum'] class: 'form-control'}</td>
|
||||||
|
<td>{input $row['castka'] class: 'form-control text-end'}</td>
|
||||||
|
<td>{input $row['poznamka'] class: 'form-control'}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a n:href="smazatPlatbu! platbaId => $platbaId"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
title="Smazat platbu"
|
||||||
|
onclick="return confirm('Opravdu smazat tuto platbu?');">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{input $form['nova']['datum'] class: 'form-control'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{input $form['nova']['castka'] class: 'form-control text-end'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{input $form['nova']['poznamka'] class: 'form-control', placeholder: 'Nová platba'}
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary me-1" id="edit-platba-dnes" data-zbyva="{$zbyvaInput}">Dnes</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="edit-platba-splatnost" data-date="{$splatnostIso}" data-zbyva="{$zbyvaInput}">Den splatnosti</button>
|
||||||
|
</div>
|
||||||
|
<button n:name="save" class="btn btn-success">
|
||||||
|
<i class="bi bi-check-lg me-1"></i>Uložit platby
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script n:if="$isEdit">
|
||||||
|
function prefillPlatba(date, zbyva) {
|
||||||
|
if (date) {
|
||||||
|
document.getElementById('edit-platba-datum').value = date;
|
||||||
|
}
|
||||||
|
const castka = Number(zbyva);
|
||||||
|
document.getElementById('edit-platba-castka').value = castka > 0 ? castka.toFixed(2) : '';
|
||||||
|
}
|
||||||
|
document.getElementById('edit-platba-dnes').addEventListener('click', function () {
|
||||||
|
const d = new Date();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
prefillPlatba(d.getFullYear() + '-' + m + '-' + day, this.getAttribute('data-zbyva'));
|
||||||
|
});
|
||||||
|
document.getElementById('edit-platba-splatnost').addEventListener('click', function () {
|
||||||
|
prefillPlatba(this.getAttribute('data-date'), this.getAttribute('data-zbyva'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{syntax off}
|
{syntax off}
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,14 @@ function New-ConnectionString([string] $path, [string] $password) {
|
||||||
return $cs
|
return $cs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Read-TableOptional($connection, [string] $sql) {
|
||||||
|
try {
|
||||||
|
return @(Read-Table $connection $sql)
|
||||||
|
} catch {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Read-Table($connection, [string] $sql) {
|
function Read-Table($connection, [string] $sql) {
|
||||||
$cmd = $connection.CreateCommand()
|
$cmd = $connection.CreateCommand()
|
||||||
$cmd.CommandText = $sql
|
$cmd.CommandText = $sql
|
||||||
|
|
@ -123,7 +131,8 @@ try {
|
||||||
zakaznik = @(Read-Table $connection 'SELECT * FROM ZAKAZNIK ORDER BY ID')
|
zakaznik = @(Read-Table $connection 'SELECT * FROM ZAKAZNIK ORDER BY ID')
|
||||||
faktura = @(Read-Table $connection 'SELECT * FROM FAKTURA ORDER BY ID')
|
faktura = @(Read-Table $connection 'SELECT * FROM FAKTURA ORDER BY ID')
|
||||||
polozka = @(Read-Table $connection 'SELECT * FROM POLOZKA ORDER BY ID')
|
polozka = @(Read-Table $connection 'SELECT * FROM POLOZKA ORDER BY ID')
|
||||||
platba = @(Read-Table $connection 'SELECT * FROM PLATBA ORDER BY ID')
|
platba = @(Read-TableOptional $connection 'SELECT * FROM PLATBA ORDER BY ID')
|
||||||
|
konfigurace = @(Read-TableOptional $connection 'SELECT ID, DATA FROM KONFIGURACE ORDER BY ID')
|
||||||
}
|
}
|
||||||
|
|
||||||
$json = $payload | ConvertTo-Json -Depth 6 -Compress:$false
|
$json = $payload | ConvertTo-Json -Depth 6 -Compress:$false
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,7 @@ CREATE TABLE [dbo].[FAKTURA](
|
||||||
[O_DIC] [nvarchar](10) NULL,
|
[O_DIC] [nvarchar](10) NULL,
|
||||||
|
|
||||||
[CINNOSTI] [nvarchar](max) NULL, -- Nahrazeno ntext -> nvarchar(max)
|
[CINNOSTI] [nvarchar](max) NULL, -- Nahrazeno ntext -> nvarchar(max)
|
||||||
[STAV] [nvarchar](20) NOT NULL CONSTRAINT [DF_FAKTURA_STAV] DEFAULT ('ISSUED'),
|
[STAV] [nvarchar](20) NOT NULL CONSTRAINT [DF_FAKTURA_STAV] DEFAULT ('ISSUED'), -- ISSUED / PARTIAL / PAID, odvozeno ze součtu PLATBA
|
||||||
|
|
||||||
CONSTRAINT [PK_FAKTURA] PRIMARY KEY CLUSTERED ([ID] ASC),
|
CONSTRAINT [PK_FAKTURA] PRIMARY KEY CLUSTERED ([ID] ASC),
|
||||||
CONSTRAINT [FK_FAKTURA_FIRMA] FOREIGN KEY ([FIRMA_ID]) REFERENCES [dbo].[FIRMA] ([ID]),
|
CONSTRAINT [FK_FAKTURA_FIRMA] FOREIGN KEY ([FIRMA_ID]) REFERENCES [dbo].[FIRMA] ([ID]),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue