673 lines
22 KiB
PHP
673 lines
22 KiB
PHP
<?php
|
|
|
|
namespace App\Model\Facade;
|
|
|
|
use App\Model\Faktura\FakturaCislovani;
|
|
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.
|
|
*/
|
|
public function getFiltered(?int $rok = null, string $mesic = ''): array
|
|
{
|
|
$where = [];
|
|
$params = [];
|
|
|
|
if ($rok) {
|
|
$where[] = 'YEAR(F.DATUM_VYSTAVENI) = ?';
|
|
$params[] = $rok;
|
|
}
|
|
|
|
$mesicCislo = (int) $mesic;
|
|
if ($mesic !== '' && $mesicCislo >= 1 && $mesicCislo <= 12) {
|
|
$where[] = 'MONTH(F.DATUM_VYSTAVENI) = ?';
|
|
$params[] = $mesicCislo;
|
|
}
|
|
|
|
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
|
|
|
$sql = "
|
|
SELECT
|
|
F.*,
|
|
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.CISLO DESC, F.CISLO_ABS DESC
|
|
";
|
|
|
|
return $this->kufrikExplorer->query($sql, ...$params)->fetchAll();
|
|
}
|
|
|
|
/**
|
|
* Roky, pro které existují faktury (plus aktuální / právě vybraný rok).
|
|
*
|
|
* @return int[]
|
|
*/
|
|
public function getDostupneRoky(?int $vybranyRok = null): array
|
|
{
|
|
$rows = $this->kufrikExplorer->query("
|
|
SELECT DISTINCT YEAR(DATUM_VYSTAVENI) AS ROK
|
|
FROM dbo.FAKTURA
|
|
WHERE DATUM_VYSTAVENI IS NOT NULL
|
|
ORDER BY ROK DESC
|
|
")->fetchAll();
|
|
|
|
$roky = [];
|
|
foreach ($rows as $row) {
|
|
$roky[] = (int) $row->ROK;
|
|
}
|
|
|
|
foreach ([(int) date('Y'), $vybranyRok] as $rok) {
|
|
if ($rok && !in_array($rok, $roky, true)) {
|
|
$roky[] = $rok;
|
|
}
|
|
}
|
|
|
|
rsort($roky, SORT_NUMERIC);
|
|
return $roky;
|
|
}
|
|
|
|
/**
|
|
* Detail jedné faktury podle ID
|
|
*/
|
|
public function getById(int $id)
|
|
{
|
|
return $this->kufrikExplorer->table('FAKTURA')->where('ID', $id)->fetch();
|
|
}
|
|
|
|
/**
|
|
* Položky faktury seřazené podle PORADI
|
|
*/
|
|
public function getPolozky(int $fakturaId): array
|
|
{
|
|
return $this->kufrikExplorer->query("
|
|
SELECT P.*
|
|
FROM dbo.POLOZKA P
|
|
WHERE P.FAKTURA = ?
|
|
ORDER BY P.PORADI ASC, P.ID ASC
|
|
", $fakturaId)->fetchAll();
|
|
}
|
|
|
|
/**
|
|
* Názvy položek pro našeptávač. Jednotka a cena jsou z posledního použití,
|
|
* pořadí je podle počtu výskytů na fakturách za poslední rok.
|
|
*
|
|
* @return list<array{nazev: string, jednotka: string, cena: float}>
|
|
*/
|
|
public function getKatalogPolozek(): array
|
|
{
|
|
$od = (new \DateTimeImmutable('today'))->modify('-1 year')->format('Y-m-d');
|
|
$rows = $this->kufrikExplorer->query("
|
|
SELECT katalog.NAZEV, katalog.JEDNOTKA, katalog.CENA_JEDNOTKA
|
|
FROM (
|
|
SELECT
|
|
P.NAZEV,
|
|
P.JEDNOTKA,
|
|
P.CENA_JEDNOTKA,
|
|
ROW_NUMBER() OVER (PARTITION BY P.NAZEV ORDER BY P.ID DESC) AS rn
|
|
FROM dbo.POLOZKA P
|
|
WHERE P.NAZEV IS NOT NULL AND LTRIM(RTRIM(P.NAZEV)) <> ''
|
|
) AS katalog
|
|
LEFT JOIN (
|
|
SELECT P.NAZEV, COUNT(*) AS POCET
|
|
FROM dbo.POLOZKA P
|
|
INNER JOIN dbo.FAKTURA F ON F.ID = P.FAKTURA
|
|
WHERE F.DATUM_VYSTAVENI >= ?
|
|
GROUP BY P.NAZEV
|
|
) AS pouziti ON pouziti.NAZEV = katalog.NAZEV
|
|
WHERE katalog.rn = 1
|
|
ORDER BY ISNULL(pouziti.POCET, 0) DESC, katalog.NAZEV ASC
|
|
", $od)->fetchAll();
|
|
|
|
$katalog = [];
|
|
foreach ($rows as $row) {
|
|
$katalog[] = [
|
|
'nazev' => (string) $row->NAZEV,
|
|
'jednotka' => trim((string) ($row->JEDNOTKA ?? '')),
|
|
'cena' => round((float) $row->CENA_JEDNOTKA, 2),
|
|
];
|
|
}
|
|
|
|
return $katalog;
|
|
}
|
|
|
|
/**
|
|
* Maska, období číslování a výchozí splatnost uložené u firmy.
|
|
*
|
|
* @return array{maska: string, obdobi: string, splatnostDny: int}
|
|
*/
|
|
public function getNastaveni(int $firmaId): array
|
|
{
|
|
$firma = $this->kufrikExplorer->table('FIRMA')->get($firmaId);
|
|
|
|
return [
|
|
'maska' => trim((string) ($firma->FAKTURA_MASKA ?? '')) ?: FakturaCislovani::MASKA_VYCHOZI,
|
|
'obdobi' => FakturaCislovani::normalizeObdobi((string) ($firma->FAKTURA_CISLOVAT ?? '')),
|
|
'splatnostDny' => FakturaCislovani::normalizeSplatnost((int) ($firma->FAKTURA_SPLATNOST ?? FakturaCislovani::SPLATNOST_VYCHOZI)),
|
|
];
|
|
}
|
|
|
|
public function saveNastaveni(int $firmaId, string $maska, string $obdobi, int $splatnostDny): void
|
|
{
|
|
$this->kufrikExplorer->table('FIRMA')->where('ID', $firmaId)->update([
|
|
'FAKTURA_MASKA' => trim($maska),
|
|
'FAKTURA_CISLOVAT' => FakturaCislovani::normalizeObdobi($obdobi),
|
|
'FAKTURA_SPLATNOST' => FakturaCislovani::normalizeSplatnost($splatnostDny),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Další číslo faktury podle masky a období číslování.
|
|
*
|
|
* @return array{cislo: string, cisloAbs: int, splatnostDny: int}
|
|
*/
|
|
public function getNextCislo(int $firmaId, ?\DateTimeInterface $datum = null): array
|
|
{
|
|
$nastaveni = $this->getNastaveni($firmaId);
|
|
$datum ??= new \DateTimeImmutable('today');
|
|
$rok = (int) $datum->format('Y');
|
|
$mesic = (int) $datum->format('n');
|
|
|
|
$where = ['FIRMA_ID = ?'];
|
|
$params = [$firmaId];
|
|
if ($nastaveni['obdobi'] === 'ROK') {
|
|
$where[] = 'YEAR(DATUM_VYSTAVENI) = ?';
|
|
$params[] = $rok;
|
|
} elseif ($nastaveni['obdobi'] === 'MESIC') {
|
|
$where[] = 'YEAR(DATUM_VYSTAVENI) = ?';
|
|
$where[] = 'MONTH(DATUM_VYSTAVENI) = ?';
|
|
$params[] = $rok;
|
|
$params[] = $mesic;
|
|
}
|
|
|
|
$maxAbs = $this->kufrikExplorer->query(
|
|
'SELECT MAX(CISLO_ABS) AS MAX_ABS FROM dbo.FAKTURA WHERE ' . implode(' AND ', $where),
|
|
...$params
|
|
)->fetch()?->MAX_ABS;
|
|
|
|
$dalsi = ($maxAbs !== null) ? ((int) $maxAbs + 1) : 1;
|
|
|
|
for ($i = 0; $i < 10000; $i++) {
|
|
$cislo = FakturaCislovani::format($nastaveni['maska'], $rok, $mesic, $dalsi);
|
|
$obsazeno = $this->kufrikExplorer->query(
|
|
'SELECT 1 AS X FROM dbo.FAKTURA WHERE FIRMA_ID = ? AND CISLO = ?',
|
|
$firmaId,
|
|
$cislo
|
|
)->fetch();
|
|
if (!$obsazeno) {
|
|
return [
|
|
'cislo' => $cislo,
|
|
'cisloAbs' => $dalsi,
|
|
'splatnostDny' => $nastaveni['splatnostDny'],
|
|
];
|
|
}
|
|
$dalsi++;
|
|
}
|
|
|
|
throw new \RuntimeException('Nepodařilo se najít volné číslo faktury.');
|
|
}
|
|
|
|
/**
|
|
* Snapshot dodavatele z aktuálních údajů firmy (výchozí hodnoty nové faktury).
|
|
*
|
|
* @return array<string, string|null>
|
|
*/
|
|
public function getDodavatelDefaults(int $firmaId): array
|
|
{
|
|
$firma = $this->kufrikExplorer->table('FIRMA')->get($firmaId);
|
|
if (!$firma) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
'D_PRAJM' => $firma->NAZEV ?? null,
|
|
'D_ULICE' => $firma->ULICE ?? null,
|
|
'D_MESTO' => $firma->MESTO ?? null,
|
|
'D_PSC' => $firma->PSC ?? null,
|
|
'D_IC' => $firma->IC ?? null,
|
|
'D_DIC' => $firma->DIC ?? null,
|
|
'D_BANKA' => self::formatFirmaUcet($firma),
|
|
'D_TELEFON' => $firma->TELEFON ?? null,
|
|
'D_EMAIL' => $firma->EMAIL ?? null,
|
|
'D_WEB' => self::firmaWeb($firma),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Uložení / Aktualizace hlavičky faktury + snapshotů D_* a O_*
|
|
*/
|
|
public function saveFaktura(array $data, ?int $id = null, int $firmaId): int
|
|
{
|
|
$snapshot = $this->resolveSnapshot($data, $firmaId, $id === null);
|
|
|
|
if ($id) {
|
|
$values = [
|
|
'DATUM_SPLATNOSTI' => $data['DATUM_SPLATNOSTI'],
|
|
'DATUM_DUZP' => !empty($data['DATUM_DUZP']) ? $data['DATUM_DUZP'] : null,
|
|
'FORMA_UHRADY' => (int) ($data['FORMA_UHRADY'] ?? 1),
|
|
'CINNOSTI' => !empty($data['CINNOSTI']) ? $data['CINNOSTI'] : null,
|
|
] + $snapshot;
|
|
|
|
$this->kufrikExplorer->table('FAKTURA')->where('ID', $id)->update($values);
|
|
return $id;
|
|
}
|
|
|
|
$zakaznikId = !empty($data['ZAKAZNIK_ID']) ? (int) $data['ZAKAZNIK_ID'] : null;
|
|
if (!$zakaznikId) {
|
|
throw new \InvalidArgumentException('Faktura musí mít odběratele.');
|
|
}
|
|
|
|
$values = [
|
|
'FIRMA_ID' => $firmaId,
|
|
'ZAKAZNIK' => $zakaznikId,
|
|
'CISLO' => $data['CISLO'],
|
|
'CISLO_ABS' => $this->resolveCisloAbs($data, $firmaId),
|
|
'FORMA_UHRADY' => (int) ($data['FORMA_UHRADY'] ?? 1),
|
|
'DATUM_VYSTAVENI' => $data['DATUM_VYSTAVENI'],
|
|
'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,
|
|
] + $snapshot;
|
|
|
|
$row = $this->kufrikExplorer->table('FAKTURA')->insert($values);
|
|
return $row->ID;
|
|
}
|
|
|
|
/**
|
|
* Snapshot z formuláře; u nové faktury doplní chybějící údaje z firmy / zákazníka.
|
|
*
|
|
* @return array<string, string|null>
|
|
*/
|
|
private function resolveSnapshot(array $data, int $firmaId, bool $isNew): array
|
|
{
|
|
$fromForm = [
|
|
'D_PRAJM' => self::nullIfEmpty($data['D_PRAJM'] ?? null),
|
|
'D_ULICE' => self::nullIfEmpty($data['D_ULICE'] ?? null),
|
|
'D_MESTO' => self::nullIfEmpty($data['D_MESTO'] ?? null),
|
|
'D_PSC' => self::nullIfEmpty($data['D_PSC'] ?? null),
|
|
'D_IC' => self::nullIfEmpty($data['D_IC'] ?? null),
|
|
'D_DIC' => self::nullIfEmpty($data['D_DIC'] ?? null),
|
|
'D_BANKA' => self::nullIfEmpty($data['D_BANKA'] ?? null),
|
|
'D_TELEFON' => self::nullIfEmpty($data['D_TELEFON'] ?? null),
|
|
'D_EMAIL' => self::nullIfEmpty($data['D_EMAIL'] ?? null),
|
|
'D_WEB' => self::nullIfEmpty($data['D_WEB'] ?? null),
|
|
'O_PRAJM' => self::nullIfEmpty($data['O_PRAJM'] ?? null),
|
|
'O_ULICE' => self::nullIfEmpty($data['O_ULICE'] ?? null),
|
|
'O_MESTO' => self::nullIfEmpty($data['O_MESTO'] ?? null),
|
|
'O_PSC' => self::nullIfEmpty($data['O_PSC'] ?? null),
|
|
'O_IC' => self::nullIfEmpty($data['O_IC'] ?? null),
|
|
'O_DIC' => self::nullIfEmpty($data['O_DIC'] ?? null),
|
|
];
|
|
|
|
if ($isNew) {
|
|
$defaults = $this->getDodavatelDefaults($firmaId);
|
|
foreach ($defaults as $key => $value) {
|
|
if ($fromForm[$key] === null || $fromForm[$key] === '') {
|
|
$fromForm[$key] = $value;
|
|
}
|
|
}
|
|
|
|
$zakaznikId = !empty($data['ZAKAZNIK_ID']) ? (int) $data['ZAKAZNIK_ID'] : null;
|
|
$zakaznik = $zakaznikId
|
|
? $this->kufrikExplorer->table('ZAKAZNIK')->where('ID', $zakaznikId)->fetch()
|
|
: null;
|
|
if ($zakaznik) {
|
|
$oDefaults = [
|
|
'O_PRAJM' => trim(($zakaznik->PRIJMENI ?? '') . ' ' . ($zakaznik->JMENO ?? '')) ?: null,
|
|
'O_ULICE' => $zakaznik->ULICE ?? null,
|
|
'O_MESTO' => $zakaznik->MESTO ?? null,
|
|
'O_PSC' => $zakaznik->PSC ?? null,
|
|
'O_IC' => $zakaznik->IC ?? null,
|
|
'O_DIC' => $zakaznik->DIC ?? null,
|
|
];
|
|
foreach ($oDefaults as $key => $value) {
|
|
if ($fromForm[$key] === null || $fromForm[$key] === '') {
|
|
$fromForm[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return self::clampSnapshot($fromForm);
|
|
}
|
|
|
|
/**
|
|
* Ořízne snapshot na délky sloupců FAKTURA (ochrana před truncací SQL Serveru).
|
|
*
|
|
* @param array<string, string|null> $snapshot
|
|
* @return array<string, string|null>
|
|
*/
|
|
private static function clampSnapshot(array $snapshot): array
|
|
{
|
|
$limits = [
|
|
'D_PRAJM' => 511,
|
|
'D_ULICE' => 50,
|
|
'D_MESTO' => 50,
|
|
'D_PSC' => 50,
|
|
'D_IC' => 8,
|
|
'D_DIC' => 12,
|
|
'D_BANKA' => 100,
|
|
'D_TELEFON' => 20,
|
|
'D_EMAIL' => 100,
|
|
'D_WEB' => 100,
|
|
'O_PRAJM' => 511,
|
|
'O_ULICE' => 50,
|
|
'O_MESTO' => 50,
|
|
'O_PSC' => 5,
|
|
'O_IC' => 8,
|
|
'O_DIC' => 10,
|
|
];
|
|
|
|
foreach ($limits as $key => $max) {
|
|
if (!isset($snapshot[$key]) || $snapshot[$key] === null) {
|
|
continue;
|
|
}
|
|
$text = (string) $snapshot[$key];
|
|
if (mb_strlen($text) > $max) {
|
|
$snapshot[$key] = mb_substr($text, 0, $max);
|
|
}
|
|
}
|
|
|
|
return $snapshot;
|
|
}
|
|
|
|
private static function nullIfEmpty(mixed $value): ?string
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
$text = trim((string) $value);
|
|
|
|
return $text !== '' ? $text : null;
|
|
}
|
|
|
|
/**
|
|
* Uložení položek s automatickým výpočtem PORADI
|
|
*/
|
|
public function savePolozky(int $fakturaId, array $polozkyData, int $firmaId): void
|
|
{
|
|
$this->kufrikExplorer->table('POLOZKA')->where('FAKTURA', $fakturaId)->delete();
|
|
|
|
$poradi = 1;
|
|
foreach ($polozkyData as $item) {
|
|
if (empty($item['NAZEV']) && empty($item['CENA_JEDNOTKA'])) {
|
|
continue;
|
|
}
|
|
|
|
$pocet = (float) ($item['POCET'] ?? 1);
|
|
$cenaJednotka = (float) ($item['CENA_JEDNOTKA'] ?? 0);
|
|
$sleva = (float) ($item['SLEVA'] ?? 0);
|
|
$cenaCelkem = ($pocet * $cenaJednotka) * (1 - ($sleva / 100));
|
|
|
|
$this->kufrikExplorer->table('POLOZKA')->insert([
|
|
'FAKTURA' => $fakturaId,
|
|
'FIRMA_ID' => $firmaId,
|
|
'PORADI' => $poradi++,
|
|
'NAZEV' => $item['NAZEV'],
|
|
'POCET' => $pocet,
|
|
'JEDNOTKA' => $item['JEDNOTKA'] ?: 'ks',
|
|
'CENA_JEDNOTKA' => $cenaJednotka,
|
|
'SLEVA' => $sleva,
|
|
'CENA' => $cenaCelkem,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pořadové číslo řady. Když formulář nepřinesl platné CISLO_ABS, dopočítá se z nastavení.
|
|
*/
|
|
private function resolveCisloAbs(array $data, int $firmaId): int
|
|
{
|
|
$abs = (int) ($data['CISLO_ABS'] ?? 0);
|
|
if ($abs > 0) {
|
|
return $abs;
|
|
}
|
|
|
|
$datum = !empty($data['DATUM_VYSTAVENI'])
|
|
? new \DateTimeImmutable((string) $data['DATUM_VYSTAVENI'])
|
|
: new \DateTimeImmutable('today');
|
|
|
|
return $this->getNextCislo($firmaId, $datum)['cisloAbs'];
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
$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 '';
|
|
}
|
|
|
|
$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', $fakturaId)
|
|
->update(['STAV' => $stav]);
|
|
|
|
return $stav;
|
|
}
|
|
|
|
public function delete(int $id): void
|
|
{
|
|
$this->kufrikExplorer->table('FAKTURA')->where('ID', $id)->delete();
|
|
}
|
|
|
|
/**
|
|
* Číslo účtu dodavatele ve formátu „účet/kód banky“.
|
|
*/
|
|
private static function formatFirmaUcet(?object $firma): ?string
|
|
{
|
|
if (!$firma) {
|
|
return null;
|
|
}
|
|
|
|
$ucet = trim((string) ($firma->CISLO_UCTU ?? ''));
|
|
$kod = trim((string) ($firma->BANKA_KOD ?? ''));
|
|
if ($ucet !== '') {
|
|
return $kod !== '' ? $ucet . '/' . $kod : $ucet;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static function firmaWeb(?object $firma): ?string
|
|
{
|
|
if (!$firma) {
|
|
return null;
|
|
}
|
|
|
|
// toArray() — bezpečné i když sloupec WEB v DB ještě není (starší tenant)
|
|
$row = method_exists($firma, 'toArray') ? $firma->toArray() : (array) $firma;
|
|
$web = trim((string) ($row['WEB'] ?? ''));
|
|
|
|
return $web !== '' ? $web : null;
|
|
}
|
|
|
|
} |