Kufrik/app/Model/Sdf/SdfKufrikImporter.php

415 lines
15 KiB
PHP

<?php
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
{
public function __construct(
private DatabazeFacade $databazeFacade,
private SdfCompactExporter $sdfExporter,
private KufrikConnectionFactory $kufrikConnectionFactory
) {
}
/**
* @return array{
* database: string,
* firma: string,
* zakazniciInserted: int,
* zakazniciMatched: int,
* fakturyInserted: int,
* fakturySkipped: int,
* polozkyInserted: int,
* platbyInserted: int,
* nastaveniImported: bool
* }
*/
public function import(int $loginId): array
{
$login = $this->databazeFacade->getById($loginId);
if (!$login) {
throw new \InvalidArgumentException('Login / databáze nebyla nalezena.');
}
$sdfPath = $this->databazeFacade->getSdfFilePath($loginId);
if ($sdfPath === null) {
throw new \RuntimeException('K této databázi ještě není nahraný soubor SDF.');
}
$exported = $this->sdfExporter->export($sdfPath);
$data = [
'uzivatel' => $this->rows($exported['uzivatel']),
'zakaznik' => $this->rows($exported['zakaznik']),
'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(
(string) $login->SERVER,
(string) $login->DATABASE,
$firmaId
);
$explorer->getConnection()->beginTransaction();
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();
} catch (\Throwable $e) {
$explorer->getConnection()->rollBack();
throw $e;
}
$stats = [
'database' => (string) $login->DATABASE,
'firma' => $firmaState,
'zakazniciInserted' => $zakaznikMap['inserted'],
'zakazniciMatched' => $zakaznikMap['matched'],
'fakturyInserted' => $fakturaResult['inserted'],
'fakturySkipped' => $fakturaResult['skipped'],
'polozkyInserted' => $fakturaResult['polozky'],
'platbyInserted' => $fakturaResult['platby'],
'nastaveniImported' => $nastaveniImported,
];
$this->databazeFacade->markSdfImported($loginId, $stats);
return $stats;
}
private function wipeTenantData(Explorer $explorer, int $firmaId): void
{
$explorer->table('FAKTURA')->where('FIRMA_ID', $firmaId)->delete();
$explorer->table('POLOZKA')->where('FIRMA_ID', $firmaId)->delete();
$explorer->table('ZAKAZNIK')->where('FIRMA_ID', $firmaId)->delete();
}
private function importFirma(Explorer $explorer, ?array $uzivatel, int $firmaId): string
{
if (!$uzivatel) {
return 'skipped';
}
$banka = (string) ($uzivatel['BANKA'] ?? '');
$cisloUctu = $banka;
$bankaKod = null;
if (preg_match('#^(.*)/(\d{4})$#u', trim($banka), $m)) {
$cisloUctu = trim($m[1]);
$bankaKod = $m[2];
}
$nazev = trim(($uzivatel['JMENO'] ?? '') . ' ' . ($uzivatel['PRIJMENI'] ?? ''));
if ($nazev === '') {
$nazev = 'Importovaná firma';
}
$values = [
'NAZEV' => $this->clip($nazev, 255),
'IC' => $this->clip($uzivatel['IC'] ?? null, 8),
'DIC' => $this->clip($uzivatel['DIC'] ?? null, 12),
'PLATCE_DPH' => !empty($uzivatel['DIC']) ? 1 : 0,
'TELEFON' => $this->clip($uzivatel['TELEFON'] ?? null, 20),
'EMAIL' => $this->clip($uzivatel['EMAIL'] ?? null, 100),
'ULICE' => $this->clip($uzivatel['ULICE'] ?? null, 100),
'MESTO' => $this->clip($uzivatel['MESTO'] ?? null, 100),
'PSC' => $this->clip($uzivatel['PSC'] ?? null, 10),
'CISLO_UCTU' => $this->clip($cisloUctu, 30),
'BANKA_KOD' => $this->clip($bankaKod, 10),
];
$exists = $explorer->table('FIRMA')->get($firmaId);
if ($exists) {
$explorer->table('FIRMA')->where('ID', $firmaId)->update($values);
return 'updated';
}
$values['ID'] = $firmaId;
$explorer->query('SET IDENTITY_INSERT dbo.FIRMA ON');
try {
$explorer->table('FIRMA')->insert($values);
} finally {
$explorer->query('SET IDENTITY_INSERT dbo.FIRMA OFF');
}
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
* @return array{map: array<int, int>, inserted: int, matched: int}
*/
private function importZakaznici(Explorer $explorer, array $rows, int $firmaId): array
{
$map = [];
$inserted = 0;
foreach ($rows as $row) {
$oldId = (int) ($row['ID'] ?? 0);
$ic = trim((string) ($row['IC'] ?? ''));
$insertedRow = $explorer->table('ZAKAZNIK')->insert([
'FIRMA_ID' => $firmaId,
'JMENO' => $this->clip($row['JMENO'] ?? null, 255),
'PRIJMENI' => $this->clip($row['PRIJMENI'] ?? null, 255) ?? '—',
'ULICE' => $this->clip($row['ULICE'] ?? null, 50),
'MESTO' => $this->clip($row['MESTO'] ?? null, 50),
'PSC' => $this->clip($row['PSC'] ?? null, 5),
'IC' => $this->clip($ic !== '' ? $ic : null, 8),
'DIC' => $this->clip($row['DIC'] ?? null, 10),
'TELEFON' => $this->clip($row['TELEFON'] ?? null, 15),
'EMAIL' => $this->clip($row['EMAIL'] ?? null, 100),
'AKTIVNI' => 1,
]);
$inserted++;
if ($oldId) {
$map[$oldId] = (int) $insertedRow->ID;
}
}
return ['map' => $map, 'inserted' => $inserted, 'matched' => 0];
}
/**
* @param array{faktura: array, polozka: array, platba: array} $data
* @param array{map: array<int, int>, inserted: int, matched: int} $zakaznikMap
* @return array{inserted: int, skipped: int, polozky: int, platby: int}
*/
private function importFaktury(Explorer $explorer, array $data, int $firmaId, array $zakaznikMap): array
{
$polozkyByFaktura = [];
foreach ($data['polozka'] as $item) {
$polozkyByFaktura[(int) $item['FAKTURA']][] = $item;
}
$platbyByFaktura = [];
foreach ($data['platba'] as $item) {
$platbyByFaktura[(int) $item['FAKTURA']][] = $item;
}
$inserted = 0;
$skipped = 0;
$polozkyInserted = 0;
$platbyInserted = 0;
foreach ($data['faktura'] as $row) {
$cislo = $this->clip($row['CISLO'] ?? null, 20);
if ($cislo === null) {
$skipped++;
continue;
}
$oldFakturaId = (int) $row['ID'];
$oldZakaznikId = (int) ($row['ZAKAZNIK'] ?? 0);
$zakaznikId = $zakaznikMap['map'][$oldZakaznikId] ?? null;
$datumVystaveni = $this->toDate($row['DATUM_VYSTAVENI'] ?? null);
$datumSplatnosti = $this->toDate($row['DATUM_SPLATNOSTI'] ?? null) ?? $datumVystaveni;
if ($datumVystaveni === null) {
$skipped++;
continue;
}
$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([
'FIRMA_ID' => $firmaId,
'ZAKAZNIK' => $zakaznikId,
'CISLO' => $cislo,
'CISLO_ABS' => (int) ($row['CISLO_ABS'] ?? 0),
'FORMA_UHRADY' => (int) ($row['FORMA_UHRADY'] ?? 1),
'DATUM_VYSTAVENI' => $datumVystaveni,
'DATUM_SPLATNOSTI' => $datumSplatnosti,
'DATUM_DUZP' => $datumVystaveni,
'D_PRAJM' => $this->clip($row['D_PRAJM'] ?? null, 511),
'D_ULICE' => $this->clip($row['D_ULICE'] ?? null, 50),
'D_MESTO' => $this->clip($row['D_MESTO'] ?? null, 50),
'D_PSC' => $this->clip($row['D_PSC'] ?? null, 50),
'D_IC' => $this->clip($row['D_IC'] ?? null, 8),
'D_DIC' => $this->clip($row['D_DIC'] ?? null, 10),
'D_BANKA' => $dBanka,
'D_TELEFON' => $this->clip($row['D_TELEFON'] ?? null, 15),
'D_EMAIL' => $this->clip($row['D_EMAIL'] ?? null, 100),
'D_WEB' => $this->clip($row['D_WEB'] ?? null, 100),
'O_PRAJM' => $this->clip($row['O_PRAJM'] ?? null, 511),
'O_ULICE' => $this->clip($row['O_ULICE'] ?? null, 50),
'O_MESTO' => $this->clip($row['O_MESTO'] ?? null, 50),
'O_PSC' => $this->clip($row['O_PSC'] ?? null, 5),
'O_IC' => $this->clip($row['O_IC'] ?? null, 8),
'O_DIC' => $this->clip($row['O_DIC'] ?? null, 10),
'CINNOSTI' => $row['CINNOSTI'] ?: null,
'STAV' => FakturaFacade::stavZCastek($celkemPolozek, $zaplaceno),
])->ID;
$poradi = 1;
foreach ($polozkyByFaktura[$oldFakturaId] ?? [] as $polozka) {
$explorer->table('POLOZKA')->insert([
'FAKTURA' => $fakturaId,
'FIRMA_ID' => $firmaId,
'PORADI' => $poradi++,
'NAZEV' => $this->clip($polozka['NAZEV'] ?? null, 255) ?? 'Položka',
'POCET' => (float) ($polozka['POCET'] ?? 1),
'JEDNOTKA' => $this->clip($polozka['JEDNOTKA'] ?? null, 10) ?? 'ks',
'CENA_JEDNOTKA' => (float) ($polozka['CENA_JEDNOTKA'] ?? 0),
'SLEVA' => (float) ($polozka['SLEVA'] ?? 0),
'CENA' => (float) ($polozka['CENA'] ?? 0),
]);
$polozkyInserted++;
}
foreach ($platbyByFaktura[$oldFakturaId] ?? [] as $platba) {
$explorer->table('PLATBA')->insert([
'FAKTURA' => $fakturaId,
'CASTKA' => (float) ($platba['CASTKA'] ?? 0),
'DATUM' => $this->toDateTime($platba['DATUM'] ?? null) ?? $datumVystaveni,
'POZNAMKA' => $platba['POZNAMKA'] ?: null,
]);
$platbyInserted++;
}
$inserted++;
}
return [
'inserted' => $inserted,
'skipped' => $skipped,
'polozky' => $polozkyInserted,
'platby' => $platbyInserted,
];
}
private function rows(mixed $value): array
{
if (!is_array($value) || $value === []) {
return [];
}
return array_is_list($value) ? $value : [$value];
}
private function clip(mixed $value, int $max): ?string
{
if ($value === null || $value === '') {
return null;
}
$value = trim((string) $value);
if ($value === '') {
return null;
}
return mb_strlen($value) > $max ? mb_substr($value, 0, $max) : $value;
}
private function toDate(mixed $value): ?string
{
if ($value === null || $value === '') {
return null;
}
try {
return (new \DateTimeImmutable((string) $value))->format('Y-m-d');
} catch (\Throwable) {
return null;
}
}
private function toDateTime(mixed $value): ?string
{
if ($value === null || $value === '') {
return null;
}
try {
return (new \DateTimeImmutable((string) $value))->format('Y-m-d H:i:s');
} catch (\Throwable) {
return null;
}
}
}