441 lines
16 KiB
PHP
441 lines
16 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 ani JSON export.');
|
|
}
|
|
|
|
$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);
|
|
$this->doplnFirmaWebZFaktur($explorer, $firmaId);
|
|
$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' => 0,
|
|
'TELEFON' => $this->clip($uzivatel['TELEFON'] ?? null, 20),
|
|
'EMAIL' => $this->clip($uzivatel['EMAIL'] ?? null, 100),
|
|
'WEB' => $this->clip($uzivatel['WEB'] ?? $uzivatel['D_WEB'] ?? 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';
|
|
}
|
|
|
|
/**
|
|
* Pokud firma nemá WEB, vezme ho z první faktury s vyplněným D_WEB.
|
|
*/
|
|
private function doplnFirmaWebZFaktur(Explorer $explorer, int $firmaId): void
|
|
{
|
|
$firma = $explorer->table('FIRMA')->get($firmaId);
|
|
if (!$firma || trim((string) ($firma->WEB ?? '')) !== '') {
|
|
return;
|
|
}
|
|
|
|
$web = $explorer->table('FAKTURA')
|
|
->where('FIRMA_ID', $firmaId)
|
|
->where('D_WEB NOT', null)
|
|
->where('D_WEB NOT', '')
|
|
->order('ID ASC')
|
|
->limit(1)
|
|
->fetchField('D_WEB');
|
|
|
|
$web = $this->clip($web, 100);
|
|
if ($web !== null) {
|
|
$explorer->table('FIRMA')->where('ID', $firmaId)->update(['WEB' => $web]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
}
|