From 4b4e3acafe98b8feebea892dec9e20c80d97ff3a Mon Sep 17 00:00:00 2001 From: Kowalski Date: Sat, 17 Oct 2026 10:50:46 +0200 Subject: [PATCH] =?UTF-8?q?Platby=20+=20import=20plateb=20a=20nastaven?= =?UTF-8?q?=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Model/Facade/FakturaFacade.php | 195 +++++++++++++++++- app/Model/Sdf/SdfCompactExporter.php | 3 +- app/Model/Sdf/SdfKufrikImporter.php | 92 ++++++++- app/UI/Admin/AdminPresenter.php | 5 +- app/UI/Faktura/FakturaPresenter.php | 58 ++++-- app/UI/Faktura/default.latte | 41 ++-- .../FakturaDetail/FakturaDetailPresenter.php | 61 ------ app/UI/FakturaDetail/default.latte | 153 -------------- app/UI/FakturaEdit/FakturaEditPresenter.php | 142 ++++++++++++- app/UI/FakturaEdit/edit.latte | 97 ++++++++- bin/sdf-export.ps1 | 11 +- sql/KufrikDB.sql | 2 +- 12 files changed, 583 insertions(+), 277 deletions(-) delete mode 100644 app/UI/FakturaDetail/FakturaDetailPresenter.php delete mode 100644 app/UI/FakturaDetail/default.latte diff --git a/app/Model/Facade/FakturaFacade.php b/app/Model/Facade/FakturaFacade.php index 9096051..1fb7421 100644 --- a/app/Model/Facade/FakturaFacade.php +++ b/app/Model/Facade/FakturaFacade.php @@ -7,11 +7,39 @@ use Nette\Database\Explorer; class FakturaFacade { + public const STAV_VYSTAVENO = 'ISSUED'; + public const STAV_CASTECNE = 'PARTIAL'; + public const STAV_ZAPLACENO = 'PAID'; + public function __construct( 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. */ @@ -36,7 +64,8 @@ class FakturaFacade $sql = " SELECT 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 {$whereSql} ORDER BY F.DATUM_VYSTAVENI DESC, F.ID DESC @@ -181,7 +210,6 @@ class FakturaFacade $values = [ 'DATUM_SPLATNOSTI' => $data['DATUM_SPLATNOSTI'], 'DATUM_DUZP' => !empty($data['DATUM_DUZP']) ? $data['DATUM_DUZP'] : null, - 'STAV' => $data['STAV'] ?? 'ISSUED', 'CINNOSTI' => !empty($data['CINNOSTI']) ? $data['CINNOSTI'] : null, ]; @@ -296,20 +324,171 @@ class FakturaFacade 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) { 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') - ->where('ID', $id) - ->update(['STAV' => $novyStav]); + ->where('ID', $fakturaId) + ->update(['STAV' => $stav]); - return $novyStav; + return $stav; } public function delete(int $id): void diff --git a/app/Model/Sdf/SdfCompactExporter.php b/app/Model/Sdf/SdfCompactExporter.php index d33ae75..6f7accc 100644 --- a/app/Model/Sdf/SdfCompactExporter.php +++ b/app/Model/Sdf/SdfCompactExporter.php @@ -16,7 +16,7 @@ class SdfCompactExporter /** * 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 { @@ -45,6 +45,7 @@ class SdfCompactExporter 'faktura' => $data['faktura'] ?? [], 'polozka' => $data['polozka'] ?? [], 'platba' => $data['platba'] ?? [], + 'konfigurace' => $data['konfigurace'] ?? [], ]; } finally { FileSystem::delete($workDir); diff --git a/app/Model/Sdf/SdfKufrikImporter.php b/app/Model/Sdf/SdfKufrikImporter.php index cf7b56e..4c9bbb7 100644 --- a/app/Model/Sdf/SdfKufrikImporter.php +++ b/app/Model/Sdf/SdfKufrikImporter.php @@ -4,6 +4,8 @@ namespace App\Model\Sdf; use App\Model\Database\KufrikConnectionFactory; use App\Model\Facade\DatabazeFacade; +use App\Model\Facade\FakturaFacade; +use App\Model\Faktura\FakturaCislovani; use Nette\Database\Explorer; class SdfKufrikImporter @@ -24,7 +26,8 @@ class SdfKufrikImporter * fakturyInserted: int, * fakturySkipped: int, * polozkyInserted: int, - * platbyInserted: int + * platbyInserted: int, + * nastaveniImported: bool * } */ public function import(int $loginId): array @@ -46,6 +49,7 @@ class SdfKufrikImporter 'faktura' => $this->rows($exported['faktura']), 'polozka' => $this->rows($exported['polozka']), 'platba' => $this->rows($exported['platba']), + 'konfigurace' => $this->rows($exported['konfigurace'] ?? []), ]; $firmaId = (int) $login->FIRMA_ID; $explorer = $this->kufrikConnectionFactory->createTenantExplorer( @@ -58,6 +62,7 @@ class SdfKufrikImporter try { $this->wipeTenantData($explorer, $firmaId); $firmaState = $this->importFirma($explorer, $data['uzivatel'][0] ?? null, $firmaId); + $nastaveniImported = $this->importNastaveni($explorer, $data['konfigurace'], $firmaId); $zakaznikMap = $this->importZakaznici($explorer, $data['zakaznik'], $firmaId); $fakturaResult = $this->importFaktury($explorer, $data, $firmaId, $zakaznikMap); $explorer->getConnection()->commit(); @@ -75,6 +80,7 @@ class SdfKufrikImporter 'fakturySkipped' => $fakturaResult['skipped'], 'polozkyInserted' => $fakturaResult['polozky'], 'platbyInserted' => $fakturaResult['platby'], + 'nastaveniImported' => $nastaveniImported, ]; $this->databazeFacade->markSdfImported($loginId, $stats); @@ -139,6 +145,79 @@ class SdfKufrikImporter return 'inserted'; } + /** + * Maska, období číslování a splatnost ze staré tabulky KONFIGURACE. + * + * @param array> $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> $rows * @return array{map: array, inserted: int, matched: int} @@ -211,7 +290,14 @@ class SdfKufrikImporter 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); $fakturaId = (int) $explorer->table('FAKTURA')->insert([ @@ -240,7 +326,7 @@ class SdfKufrikImporter 'O_IC' => $this->clip($row['O_IC'] ?? null, 8), 'O_DIC' => $this->clip($row['O_DIC'] ?? null, 10), 'CINNOSTI' => $row['CINNOSTI'] ?: null, - 'STAV' => $hasPlatba ? 'PAID' : 'ISSUED', + 'STAV' => FakturaFacade::stavZCastek($celkemPolozek, $zaplaceno), ])->ID; $poradi = 1; diff --git a/app/UI/Admin/AdminPresenter.php b/app/UI/Admin/AdminPresenter.php index 22e4053..fd19a70 100644 --- a/app/UI/Admin/AdminPresenter.php +++ b/app/UI/Admin/AdminPresenter.php @@ -102,12 +102,13 @@ class AdminPresenter extends BasePresenter $stats = $this->sdfImporter->import($loginId); $this->flashMessage( 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['zakazniciInserted'], $stats['fakturyInserted'], $stats['polozkyInserted'], - $stats['platbyInserted'] + $stats['platbyInserted'], + !empty($stats['nastaveniImported']) ? 'přeneseno' : 'bez změn' ), 'success' ); diff --git a/app/UI/Faktura/FakturaPresenter.php b/app/UI/Faktura/FakturaPresenter.php index 4ae9584..dd932b7 100644 --- a/app/UI/Faktura/FakturaPresenter.php +++ b/app/UI/Faktura/FakturaPresenter.php @@ -2,9 +2,11 @@ namespace App\UI\Faktura; +use App\Model\Faktura\FakturaPdf; use App\UI\BasePresenter; use App\Model\Facade\FakturaFacade; use Nette\Application\Attributes\Persistent; +use Nette\Application\Responses\CallbackResponse; class FakturaPresenter extends BasePresenter { @@ -18,11 +20,33 @@ class FakturaPresenter extends BasePresenter public string $mesic = ''; public function __construct( - private FakturaFacade $fakturaFacade + 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; + })); + } + protected function startup(): void { parent::startup(); @@ -56,21 +80,32 @@ class FakturaPresenter extends BasePresenter $rok = $this->rok ?? (int) date('Y'); $mesic = $this->normalizeMesic($this->mesic); + $this->fakturaFacade->reconcileUhrady(); $faktury = $this->fakturaFacade->getFiltered($rok, $mesic); $celkemSum = 0; + $zbyvaSum = 0; $neuhrazenoCount = 0; foreach ($faktury as $f) { - $celkemSum += (float) ($f->CASTKA_CELKEM ?? 0); - if ($f->STAV !== 'PAID') { + $celkem = (float) ($f->CASTKA_CELKEM ?? 0); + $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++; } } $this->template->faktury = $faktury; $this->template->celkemSum = $celkemSum; + $this->template->zbyvaSum = $zbyvaSum; $this->template->neuhrazenoCount = $neuhrazenoCount; + $this->template->dnes = date('Y-m-d'); $this->template->selectedRok = $rok; $this->template->selectedMesic = $mesic; $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 */ diff --git a/app/UI/Faktura/default.latte b/app/UI/Faktura/default.latte index 3ba5ad4..6684ef3 100644 --- a/app/UI/Faktura/default.latte +++ b/app/UI/Faktura/default.latte @@ -26,6 +26,12 @@
{$neuhrazenoCount} ks
+
+
+
Zbývá uhradit
+
{$zbyvaSum|number:2, ',', ' '} CZK
+
+
@@ -70,6 +76,7 @@ Vystaveno Splatnost Částka + Zaplaceno Stav Akce @@ -79,7 +86,7 @@ - + {$f->CISLO} @@ -90,38 +97,38 @@ {$f->DATUM_VYSTAVENI|date:'d.m.Y'} - + {$f->DATUM_SPLATNOSTI|date:'d.m.Y'} {$f->CASTKA_CELKEM|number:2, ',', ' '} CZK + + + {$f->ZAPLACENO|number:2, ',', ' '} CZK + - - - {if $f->STAV === 'PAID'}Zaplaceno{else}Neuhrazeno{/if} + + + {if $f->STAV_UHRADY === 'PAID'}Zaplaceno{elseif $f->STAV_UHRADY === 'PARTIAL'}Částečně{else}Neuhrazeno{/if} - - - + + - - - - - - + - + diff --git a/app/UI/FakturaDetail/FakturaDetailPresenter.php b/app/UI/FakturaDetail/FakturaDetailPresenter.php deleted file mode 100644 index 30a8693..0000000 --- a/app/UI/FakturaDetail/FakturaDetailPresenter.php +++ /dev/null @@ -1,61 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/app/UI/FakturaDetail/default.latte b/app/UI/FakturaDetail/default.latte deleted file mode 100644 index c5da3ec..0000000 --- a/app/UI/FakturaDetail/default.latte +++ /dev/null @@ -1,153 +0,0 @@ -{block content} - -
- - - - -
- -
-
-

FAKTURA - DAŇOVÝ DOKLAD

-
Číslo: {$faktura->CISLO}
-
-
- - {if $faktura->STAV === 'PAID'}UHRATENO{else}NEUHRATENO{/if} - -
-
- - -
- -
-
Dodavatel
-
{$faktura->D_PRAJM ?: '—'}
-
{$faktura->D_ULICE}
-
{$faktura->D_PSC} {$faktura->D_MESTO}
- -
- {if $faktura->D_IC}IČ: {$faktura->D_IC}
{/if} - {if $faktura->D_DIC}DIČ: {$faktura->D_DIC}
{/if} -
- - {if $faktura->D_BANKA} -
- Bankovní účet:
- {$faktura->D_BANKA} -
- {/if} -
- - -
-
Odběratel
-
{$faktura->O_PRAJM ?: '—'}
-
{$faktura->O_ULICE}
-
{$faktura->O_PSC} {$faktura->O_MESTO}
- -
- {if $faktura->O_IC}IČ: {$faktura->O_IC}
{/if} - {if $faktura->O_DIC}DIČ: {$faktura->O_DIC}
{/if} -
-
-
- - -
-
-
Datum vystavení:
-
{$faktura->DATUM_VYSTAVENI|date:'d.m.Y'}
-
-
-
Datum DUZP:
-
{if $faktura->DATUM_DUZP}{$faktura->DATUM_DUZP|date:'d.m.Y'}{else}—{/if}
-
-
-
Datum splatnosti:
-
{$faktura->DATUM_SPLATNOSTI|date:'d.m.Y'}
-
-
- - -
-
Popis plnění / Činnosti:
-

{$faktura->CINNOSTI}

-
- - -
- - - - - - - - - - - - - - {foreach $polozky as $p} - - - - - - - - - - {/foreach} - -
#PoložkaPočetJedn.Cena/J.SlevaCelkem
{$p->PORADI}{$p->NAZEV}{$p->POCET}{$p->JEDNOTKA}{$p->CENA_JEDNOTKA|number:2, ',', ' '} CZK{if $p->SLEVA > 0}{$p->SLEVA} %{else}—{/if}{$p->CENA|number:2, ',', ' '} CZK
-
- - -
-
-
- Celkem k úhradě: - {$celkem|number:2, ',', ' '} CZK -
-
-
-
-
- - \ No newline at end of file diff --git a/app/UI/FakturaEdit/FakturaEditPresenter.php b/app/UI/FakturaEdit/FakturaEditPresenter.php index 2a5e949..bdbbc2c 100644 --- a/app/UI/FakturaEdit/FakturaEditPresenter.php +++ b/app/UI/FakturaEdit/FakturaEditPresenter.php @@ -89,8 +89,46 @@ class FakturaEditPresenter extends BasePresenter { $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 - $this->template->polozky = $this->id ? $this->fakturaFacade->getPolozky($this->id) : []; + $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 @@ -136,11 +174,6 @@ class FakturaEditPresenter extends BasePresenter $form->addText('DATUM_DUZP', 'Datum UZP:') ->setHtmlType('date'); - $form->addSelect('STAV', 'Stav:', [ - 'ISSUED' => 'Vystaveno (Neuhrazeno)', - 'PAID' => 'Zaplaceno', - ])->setDefaultValue('ISSUED'); - $form->addTextArea('CINNOSTI', 'Činnosti / Popis plnění:'); $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 $this->fakturaFacade->savePolozky($fakturaId, $polozkyRaw, $firmaId); + $this->fakturaFacade->syncStav($fakturaId); $this->flashMessage( $this->id ? 'Faktura byla úspěšně upravena.' : 'Nová faktura byla vystavena.', @@ -172,4 +206,98 @@ class FakturaEditPresenter extends BasePresenter $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'); + } } \ No newline at end of file diff --git a/app/UI/FakturaEdit/edit.latte b/app/UI/FakturaEdit/edit.latte index 5cf22f3..4ea988f 100644 --- a/app/UI/FakturaEdit/edit.latte +++ b/app/UI/FakturaEdit/edit.latte @@ -43,9 +43,14 @@ -
- - +
+ +
+ + {if $stavUhrady === 'PAID'}Zaplaceno{elseif $stavUhrady === 'PARTIAL'}Částečně uhrazeno{else}Neuhrazeno{/if} + + zbývá {$zbyva|number:2, ',', ' '} CZK +

@@ -115,9 +120,95 @@ Zrušit
+ +
+
Úhrady
+

+ Celkem {$celkemFaktury|number:2, ',', ' '} CZK, + uhrazeno {$zaplaceno|number:2, ',', ' '} CZK, + zbývá {$zbyva|number:2, ',', ' '} CZK. +

+ +
+
+
    +
  • {$error}
  • +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
DatumČástkaPoznámka
Zatím žádná platba.
{input $row['datum'] class: 'form-control'}{input $row['castka'] class: 'form-control text-end'}{input $row['poznamka'] class: 'form-control'} + + + +
+ {input $form['nova']['datum'] class: 'form-control'} + + {input $form['nova']['castka'] class: 'form-control text-end'} + + {input $form['nova']['poznamka'] class: 'form-control', placeholder: 'Nová platba'} +
+
+
+ + +
+ +
+
+ + {syntax off}