Import ze souboru SDF
parent
3688c0312d
commit
28f30fa887
|
|
@ -6,6 +6,8 @@
|
||||||
# KOW
|
# KOW
|
||||||
temp/
|
temp/
|
||||||
log/
|
log/
|
||||||
|
data/sdf/*
|
||||||
|
!data/sdf/.gitkeep
|
||||||
|
|
||||||
|
|
||||||
#ZAZA
|
#ZAZA
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ class KufrikConnectionFactory
|
||||||
|
|
||||||
$identity = $this->user->getIdentity();
|
$identity = $this->user->getIdentity();
|
||||||
|
|
||||||
// Server, Databáze i FirmaId se načtou z Identity přihlášeného uživatele
|
|
||||||
$host = $identity->server ?? null;
|
$host = $identity->server ?? null;
|
||||||
$databaseName = $identity->database ?? null;
|
$databaseName = $identity->database ?? null;
|
||||||
$firmaId = $identity->firma_id ?? null;
|
$firmaId = $identity->firma_id ?? null;
|
||||||
|
|
@ -38,30 +37,44 @@ class KufrikConnectionFactory
|
||||||
throw new \LogicException('Přihlášený uživatel nemá v tabulce LOGIN definovaný SERVER, DATABASE nebo FIRMA_ID.');
|
throw new \LogicException('Přihlášený uživatel nemá v tabulce LOGIN definovaný SERVER, DATABASE nebo FIRMA_ID.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamický DSN řetězec
|
return $this->createTenantConnection((string) $host, (string) $databaseName, (int) $firmaId);
|
||||||
$dsn = "sqlsrv:Server={$host};Database={$databaseName};LoginTimeout=3;";
|
}
|
||||||
|
|
||||||
|
public function createExplorer(): Explorer
|
||||||
|
{
|
||||||
|
return $this->createExplorerFromConnection($this->createConnection());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createTenantExplorer(string $host, string $databaseName, int $firmaId): Explorer
|
||||||
|
{
|
||||||
|
return $this->createExplorerFromConnection(
|
||||||
|
$this->createTenantConnection($host, $databaseName, $firmaId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTenantConnection(string $host, string $databaseName, int $firmaId): Connection
|
||||||
|
{
|
||||||
|
$dsn = "sqlsrv:Server={$host};Database={$databaseName};LoginTimeout=10;";
|
||||||
|
|
||||||
$connection = new Connection(
|
$connection = new Connection(
|
||||||
$dsn,
|
$dsn,
|
||||||
$this->dbUser,
|
$this->dbUser,
|
||||||
$this->dbPassword,
|
$this->dbPassword,
|
||||||
[
|
[
|
||||||
'lazy' => false, // Spojení musí vzniknout ihned
|
'lazy' => false,
|
||||||
'driverOptions' => [
|
'driverOptions' => [
|
||||||
\PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_UTF8,
|
\PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_UTF8,
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// NASTAVENÍ RLS KONTEXTU PRO MS SQL SERVER
|
$connection->query('EXEC sp_set_session_context @key = N\'FirmaId\', @value = ?', $firmaId);
|
||||||
$connection->query('EXEC sp_set_session_context @key = N\'FirmaId\', @value = ?', (int)$firmaId);
|
|
||||||
|
|
||||||
return $connection;
|
return $connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createExplorer(): Explorer
|
private function createExplorerFromConnection(Connection $connection): Explorer
|
||||||
{
|
{
|
||||||
$connection = $this->createConnection();
|
|
||||||
$storage = new MemoryStorage();
|
$storage = new MemoryStorage();
|
||||||
$structure = new Structure($connection, $storage);
|
$structure = new Structure($connection, $storage);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,201 @@
|
||||||
namespace App\Model\Facade;
|
namespace App\Model\Facade;
|
||||||
|
|
||||||
use Nette\Database\Connection;
|
use Nette\Database\Connection;
|
||||||
|
use Nette\Http\FileUpload;
|
||||||
|
use Nette\Utils\FileSystem;
|
||||||
|
use Nette\Utils\Json;
|
||||||
|
|
||||||
class DatabazeFacade
|
class DatabazeFacade
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Connection $masterDb
|
private Connection $masterDb,
|
||||||
|
private string $sdfStorageDir
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seznam databází z master tabulky LOGIN (jeden login = jedna databáze).
|
* Seznam databází z master tabulky LOGIN (jeden login = jedna databáze).
|
||||||
*
|
*
|
||||||
* @return \Nette\Database\Row[]
|
* @return \stdClass[]
|
||||||
*/
|
*/
|
||||||
public function getAll(): array
|
public function getAll(): array
|
||||||
{
|
{
|
||||||
return $this->masterDb->fetchAll(
|
$rows = $this->masterDb->fetchAll(
|
||||||
'SELECT ID, EMAIL, POSLEDNI_PRIHLASENI, AKTIVNI, [DATABASE], [SERVER], FIRMA_ID, IS_ADMIN
|
'SELECT ID, EMAIL, POSLEDNI_PRIHLASENI, AKTIVNI, [DATABASE], [SERVER], FIRMA_ID, IS_ADMIN
|
||||||
FROM dbo.LOGIN
|
FROM dbo.LOGIN
|
||||||
ORDER BY [DATABASE], EMAIL'
|
ORDER BY [DATABASE], EMAIL'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$result[] = $this->hydrate($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById(int $id): ?\stdClass
|
||||||
|
{
|
||||||
|
$row = $this->masterDb->fetch(
|
||||||
|
'SELECT ID, EMAIL, POSLEDNI_PRIHLASENI, AKTIVNI, [DATABASE], [SERVER], FIRMA_ID, IS_ADMIN
|
||||||
|
FROM dbo.LOGIN
|
||||||
|
WHERE ID = ?',
|
||||||
|
$id
|
||||||
|
);
|
||||||
|
|
||||||
|
return $row ? $this->hydrate($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSdfFilePath(int $loginId): ?string
|
||||||
|
{
|
||||||
|
$path = $this->sdfPath($loginId);
|
||||||
|
return is_file($path) ? $path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $stats
|
||||||
|
*/
|
||||||
|
public function markSdfImported(int $loginId, array $stats): void
|
||||||
|
{
|
||||||
|
$meta = $this->readSdfMeta($loginId);
|
||||||
|
$meta['importedAt'] = (new \DateTimeImmutable())->format('c');
|
||||||
|
$meta['importStats'] = $stats;
|
||||||
|
FileSystem::createDir($this->sdfStorageDir);
|
||||||
|
FileSystem::write($this->sdfMetaPath($loginId), Json::encode($meta, pretty: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Položky selectu: ID => „databáze — e-mail“.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function getSelectItems(): array
|
||||||
|
{
|
||||||
|
$items = [];
|
||||||
|
foreach ($this->getAll() as $login) {
|
||||||
|
$items[(int) $login->ID] = $login->DATABASE . ' — ' . $login->EMAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uloží nahraný .sdf k loginu.
|
||||||
|
*/
|
||||||
|
public function saveSdf(int $loginId, FileUpload $file): void
|
||||||
|
{
|
||||||
|
$exists = $this->masterDb->fetchField('SELECT ID FROM dbo.LOGIN WHERE ID = ?', $loginId);
|
||||||
|
if (!$exists) {
|
||||||
|
throw new \InvalidArgumentException('Vybraná databáze (login) neexistuje.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$file->isOk()) {
|
||||||
|
throw new \RuntimeException('Soubor se nepodařilo nahrát. Zkontrolujte velikost (limit PHP/upload).');
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = strtolower(pathinfo($file->getUntrustedName(), PATHINFO_EXTENSION));
|
||||||
|
if ($extension !== 'sdf') {
|
||||||
|
throw new \InvalidArgumentException('Soubor musí mít příponu .sdf.');
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSystem::createDir($this->sdfStorageDir);
|
||||||
|
|
||||||
|
$file->move($this->sdfPath($loginId));
|
||||||
|
$this->clearSdfImportMeta($loginId);
|
||||||
|
|
||||||
|
$meta = [
|
||||||
|
'originalName' => $this->safeOriginalName($file->getUntrustedName()),
|
||||||
|
'uploadedAt' => (new \DateTimeImmutable())->format('c'),
|
||||||
|
'size' => $file->getSize(),
|
||||||
|
'loginId' => $loginId,
|
||||||
|
];
|
||||||
|
|
||||||
|
FileSystem::write($this->sdfMetaPath($loginId), Json::encode($meta, pretty: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{originalName?: string, uploadedAt?: string, size?: int}|array{}
|
||||||
|
*/
|
||||||
|
private function readSdfMeta(int $loginId): array
|
||||||
|
{
|
||||||
|
$sdf = $this->sdfPath($loginId);
|
||||||
|
if (!is_file($sdf)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$metaFile = $this->sdfMetaPath($loginId);
|
||||||
|
$meta = [];
|
||||||
|
if (is_file($metaFile)) {
|
||||||
|
try {
|
||||||
|
$decoded = Json::decode(FileSystem::read($metaFile), forceArrays: true);
|
||||||
|
$meta = is_array($decoded) ? $decoded : [];
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$meta = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta['size'] = filesize($sdf) ?: ($meta['size'] ?? null);
|
||||||
|
$meta['uploadedAt'] ??= date('c', filemtime($sdf) ?: time());
|
||||||
|
$meta['originalName'] ??= $loginId . '.sdf';
|
||||||
|
|
||||||
|
return $meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hydrate(object $row): \stdClass
|
||||||
|
{
|
||||||
|
$item = (object) (array) $row;
|
||||||
|
$meta = $this->readSdfMeta((int) $item->ID);
|
||||||
|
$item->SDF_SOUBOR = $meta['originalName'] ?? null;
|
||||||
|
$item->SDF_NAHRANO = isset($meta['uploadedAt']) ? new \DateTimeImmutable($meta['uploadedAt']) : null;
|
||||||
|
$item->SDF_VELIKOST = $meta['size'] ?? null;
|
||||||
|
$item->SDF_VELIKOST_TEXT = isset($meta['size']) ? $this->formatBytes((int) $meta['size']) : null;
|
||||||
|
$item->SDF_IMPORTOVANO = isset($meta['importedAt']) ? new \DateTimeImmutable($meta['importedAt']) : null;
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clearSdfImportMeta(int $loginId): void
|
||||||
|
{
|
||||||
|
$metaFile = $this->sdfMetaPath($loginId);
|
||||||
|
if (!is_file($metaFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$decoded = Json::decode(FileSystem::read($metaFile), forceArrays: true);
|
||||||
|
$meta = is_array($decoded) ? $decoded : [];
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unset($meta['importedAt'], $meta['importStats']);
|
||||||
|
FileSystem::write($metaFile, Json::encode($meta, pretty: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sdfPath(int $loginId): string
|
||||||
|
{
|
||||||
|
return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.sdf';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sdfMetaPath(int $loginId): string
|
||||||
|
{
|
||||||
|
return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
if ($bytes < 1024) {
|
||||||
|
return $bytes . ' B';
|
||||||
|
}
|
||||||
|
if ($bytes < 1024 * 1024) {
|
||||||
|
return number_format($bytes / 1024, 1, ',', ' ') . ' KB';
|
||||||
|
}
|
||||||
|
|
||||||
|
return number_format($bytes / (1024 * 1024), 1, ',', ' ') . ' MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function safeOriginalName(string $name): string
|
||||||
|
{
|
||||||
|
$name = str_replace(["\0", "\r", "\n"], '', $name);
|
||||||
|
$name = basename($name);
|
||||||
|
|
||||||
|
return $name !== '' ? mb_substr($name, 0, 255) : 'databaze.sdf';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Model\Sdf;
|
||||||
|
|
||||||
|
use Nette\Utils\FileSystem;
|
||||||
|
use Nette\Utils\Json;
|
||||||
|
|
||||||
|
class SdfCompactExporter
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private string $scriptPath,
|
||||||
|
private string $sdfPassword
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Přečte SQL Compact .sdf a vrátí tabulky jako pole.
|
||||||
|
*
|
||||||
|
* @return array{uzivatel: array, zakaznik: array, faktura: array, polozka: array, platba: array}
|
||||||
|
*/
|
||||||
|
public function export(string $sdfPath): array
|
||||||
|
{
|
||||||
|
if (!is_file($sdfPath)) {
|
||||||
|
throw new \RuntimeException('SDF soubor nebyl nalezen.');
|
||||||
|
}
|
||||||
|
if (!is_file($this->scriptPath)) {
|
||||||
|
throw new \RuntimeException('Exportní skript bin/sdf-export.ps1 chybí.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$workDir = dirname($sdfPath) . DIRECTORY_SEPARATOR . 'export-' . bin2hex(random_bytes(4));
|
||||||
|
FileSystem::createDir($workDir);
|
||||||
|
$outFile = $workDir . DIRECTORY_SEPARATOR . 'export.json';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->runPowershell($sdfPath, $outFile);
|
||||||
|
$raw = FileSystem::read($outFile);
|
||||||
|
$data = Json::decode($raw, forceArrays: true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
throw new \RuntimeException('Export SDF nevrátil platná data.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uzivatel' => $data['uzivatel'] ?? [],
|
||||||
|
'zakaznik' => $data['zakaznik'] ?? [],
|
||||||
|
'faktura' => $data['faktura'] ?? [],
|
||||||
|
'polozka' => $data['polozka'] ?? [],
|
||||||
|
'platba' => $data['platba'] ?? [],
|
||||||
|
];
|
||||||
|
} finally {
|
||||||
|
FileSystem::delete($workDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function runPowershell(string $sdfPath, string $outFile): void
|
||||||
|
{
|
||||||
|
$powershell = $this->findPowershell();
|
||||||
|
$cmd = sprintf(
|
||||||
|
'%s -NoProfile -NonInteractive -ExecutionPolicy Bypass -File %s -SdfPath %s -OutFile %s -Password %s',
|
||||||
|
$this->winArg($powershell),
|
||||||
|
$this->winArg($this->scriptPath),
|
||||||
|
$this->winArg($sdfPath),
|
||||||
|
$this->winArg($outFile),
|
||||||
|
$this->winArg($this->sdfPassword)
|
||||||
|
);
|
||||||
|
|
||||||
|
$descriptor = [
|
||||||
|
1 => ['pipe', 'w'],
|
||||||
|
2 => ['pipe', 'w'],
|
||||||
|
];
|
||||||
|
$process = proc_open($cmd, $descriptor, $pipes, null, null, ['bypass_shell' => false]);
|
||||||
|
if (!is_resource($process)) {
|
||||||
|
throw new \RuntimeException('Nepodařilo se spustit PowerShell pro čtení SDF.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stdout = stream_get_contents($pipes[1]);
|
||||||
|
$stderr = stream_get_contents($pipes[2]);
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
$code = proc_close($process);
|
||||||
|
|
||||||
|
if ($code !== 0 || !is_file($outFile)) {
|
||||||
|
$detail = trim($stderr . "\n" . $stdout);
|
||||||
|
throw new \RuntimeException('Export SDF selhal' . ($detail !== '' ? ': ' . $detail : '.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findPowershell(): string
|
||||||
|
{
|
||||||
|
$candidates = [
|
||||||
|
getenv('SystemRoot') . '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
|
||||||
|
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
|
||||||
|
];
|
||||||
|
foreach ($candidates as $path) {
|
||||||
|
if ($path && is_file($path)) {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'powershell.exe';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function winArg(string $value): string
|
||||||
|
{
|
||||||
|
return '"' . str_replace('"', '\\"', $value) . '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,328 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Model\Sdf;
|
||||||
|
|
||||||
|
use App\Model\Database\KufrikConnectionFactory;
|
||||||
|
use App\Model\Facade\DatabazeFacade;
|
||||||
|
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
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
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']),
|
||||||
|
];
|
||||||
|
$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);
|
||||||
|
$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'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$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';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasPlatba = !empty($platbyByFaktura[$oldFakturaId]);
|
||||||
|
$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' => $hasPlatba ? 'PAID' : 'ISSUED',
|
||||||
|
])->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,21 @@
|
||||||
namespace App\UI\Admin;
|
namespace App\UI\Admin;
|
||||||
|
|
||||||
use App\Model\Facade\DatabazeFacade;
|
use App\Model\Facade\DatabazeFacade;
|
||||||
|
use App\Model\Sdf\SdfKufrikImporter;
|
||||||
use App\UI\BasePresenter;
|
use App\UI\BasePresenter;
|
||||||
|
use Nette\Application\AbortException;
|
||||||
|
use Nette\Application\UI\Form;
|
||||||
|
use Nette\Forms\Controls\UploadControl;
|
||||||
|
use Nette\Forms\Helpers;
|
||||||
|
use Nette\Http\FileUpload;
|
||||||
|
|
||||||
class AdminPresenter extends BasePresenter
|
class AdminPresenter extends BasePresenter
|
||||||
{
|
{
|
||||||
|
private const SDF_MAX_BYTES = 50 * 1024 * 1024;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private DatabazeFacade $databazeFacade
|
private DatabazeFacade $databazeFacade,
|
||||||
|
private SdfKufrikImporter $sdfImporter
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
@ -46,4 +55,68 @@ class AdminPresenter extends BasePresenter
|
||||||
public function renderStatistika(): void
|
public function renderStatistika(): void
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function createComponentSdfImportForm(): Form
|
||||||
|
{
|
||||||
|
$form = new Form();
|
||||||
|
|
||||||
|
$form->addHidden('loginId')
|
||||||
|
->addRule($form::Integer, 'Vyberte databázi.')
|
||||||
|
->setRequired('Vyberte databázi.');
|
||||||
|
|
||||||
|
$iniLimits = array_filter([
|
||||||
|
Helpers::iniGetSize('upload_max_filesize'),
|
||||||
|
Helpers::iniGetSize('post_max_size'),
|
||||||
|
]);
|
||||||
|
$maxBytes = min(self::SDF_MAX_BYTES, ...($iniLimits ?: [self::SDF_MAX_BYTES]));
|
||||||
|
$maxMb = max(1, (int) floor($maxBytes / (1024 * 1024)));
|
||||||
|
|
||||||
|
$form->addUpload('sdf', 'Soubor SDF:')
|
||||||
|
->setRequired('Vyberte soubor s příponou .sdf.')
|
||||||
|
->addRule($form::MaxFileSize, "Soubor může mít nejvýše {$maxMb} MB.", $maxBytes)
|
||||||
|
->addRule(
|
||||||
|
function (UploadControl $control): bool {
|
||||||
|
$file = $control->getValue();
|
||||||
|
if (!$file instanceof FileUpload || !$file->hasFile()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtolower(pathinfo($file->getUntrustedName(), PATHINFO_EXTENSION)) === 'sdf';
|
||||||
|
},
|
||||||
|
'Soubor musí mít příponu .sdf.'
|
||||||
|
);
|
||||||
|
|
||||||
|
$form->addSubmit('send', 'Import');
|
||||||
|
$form->onSuccess[] = [$this, 'sdfImportFormSucceeded'];
|
||||||
|
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sdfImportFormSucceeded(Form $form, \stdClass $data): void
|
||||||
|
{
|
||||||
|
set_time_limit(180);
|
||||||
|
$loginId = (int) $data->loginId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->databazeFacade->saveSdf($loginId, $data->sdf);
|
||||||
|
$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.',
|
||||||
|
$stats['database'],
|
||||||
|
$stats['zakazniciInserted'],
|
||||||
|
$stats['fakturyInserted'],
|
||||||
|
$stats['polozkyInserted'],
|
||||||
|
$stats['platbyInserted']
|
||||||
|
),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
} catch (AbortException $e) {
|
||||||
|
throw $e;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->flashMessage('Import ze SDF selhal: ' . $e->getMessage(), 'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redirect('this');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,18 @@
|
||||||
<h2 class="h3 mb-0">Přehled databází</h2>
|
<h2 class="h3 mb-0">Přehled databází</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form n:name="sdfImportForm" class="d-none">
|
||||||
|
<input n:name="loginId">
|
||||||
|
<input n:name="sdf" accept=".sdf">
|
||||||
|
<button n:name="send">Import</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="alert alert-danger" n:if="$presenter['sdfImportForm']->hasErrors()">
|
||||||
|
<ul class="mb-0 ps-3">
|
||||||
|
<li n:foreach="$presenter['sdfImportForm']->errors as $error">{$error}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-6 col-lg-4">
|
<div class="col-md-6 col-lg-4">
|
||||||
<div class="card border-0 shadow-sm p-3 bg-white border-start border-primary border-4">
|
<div class="card border-0 shadow-sm p-3 bg-white border-start border-primary border-4">
|
||||||
|
|
@ -35,6 +47,8 @@
|
||||||
<th>Poslední přihlášení</th>
|
<th>Poslední přihlášení</th>
|
||||||
<th class="text-center">Stav</th>
|
<th class="text-center">Stav</th>
|
||||||
<th class="text-center">Role</th>
|
<th class="text-center">Role</th>
|
||||||
|
<th>Poslední import SDF</th>
|
||||||
|
<th class="text-end" data-orderable="false">Akce</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -69,6 +83,31 @@
|
||||||
<span class="badge bg-light text-dark border">Uživatel</span>
|
<span class="badge bg-light text-dark border">Uživatel</span>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
|
<td data-order="{$login->SDF_SOUBOR}">
|
||||||
|
{if $login->SDF_SOUBOR}
|
||||||
|
<div class="fw-semibold">{$login->SDF_SOUBOR}</div>
|
||||||
|
<div class="small text-muted">
|
||||||
|
nahráno {$login->SDF_NAHRANO|date:'d.m.Y H:i'}
|
||||||
|
{if $login->SDF_VELIKOST_TEXT}
|
||||||
|
· {$login->SDF_VELIKOST_TEXT}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="small text-success" n:if="$login->SDF_IMPORTOVANO">
|
||||||
|
importováno {$login->SDF_IMPORTOVANO|date:'d.m.Y H:i'}
|
||||||
|
</div>
|
||||||
|
{else}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="text-end text-nowrap">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-success js-sdf-import"
|
||||||
|
data-login-id="{$login->ID}"
|
||||||
|
data-database="{$login->DATABASE}"
|
||||||
|
title="Smaže stávající zákazníky, faktury, položky a platby a nahradí je daty ze SDF">
|
||||||
|
<i class="bi bi-box-arrow-in-down me-1"></i>Import
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -90,6 +129,26 @@ $(document).ready(function() {
|
||||||
order: [[0, 'asc']],
|
order: [[0, 'asc']],
|
||||||
pageLength: 25
|
pageLength: 25
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $form = $('form[id$="-sdfImportForm"], #frm-sdfImportForm').first();
|
||||||
|
const $loginId = $form.find('[name="loginId"]');
|
||||||
|
const $file = $form.find('input[type="file"]');
|
||||||
|
|
||||||
|
$(document).on('click', '.js-sdf-import', function() {
|
||||||
|
const database = $(this).data('database');
|
||||||
|
if (!confirm('VAROVÁNÍ: Import ze SDF do databáze ' + database + ' SMAŽE všechna stávající data této firmy (zákazníky, faktury, položky a platby) a nahradí je obsahem souboru.\n\nOpravdu chcete pokračovat?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$loginId.val($(this).data('login-id'));
|
||||||
|
$file.val('');
|
||||||
|
$file.trigger('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
$file.on('change', function() {
|
||||||
|
if (this.files && this.files.length) {
|
||||||
|
$form.trigger('submit');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{/syntax}
|
{/syntax}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string] $SdfPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string] $OutFile,
|
||||||
|
|
||||||
|
[string] $Password = '',
|
||||||
|
|
||||||
|
[string] $SqlCeDll = ''
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Find-SqlCeDll([string] $preferred) {
|
||||||
|
if ($preferred -and (Test-Path -LiteralPath $preferred)) {
|
||||||
|
return $preferred
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = @(
|
||||||
|
'C:\Program Files\Microsoft SQL Server Compact Edition\v4.0\Desktop\System.Data.SqlServerCe.dll',
|
||||||
|
'C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v4.0\Desktop\System.Data.SqlServerCe.dll'
|
||||||
|
)
|
||||||
|
foreach ($path in $candidates) {
|
||||||
|
if (Test-Path -LiteralPath $path) {
|
||||||
|
return $path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'Nenalezena knihovna System.Data.SqlServerCe.dll (SQL Server Compact 4.0).'
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ConnectionString([string] $path, [string] $password) {
|
||||||
|
$cs = "Data Source=$path;Max Database Size=4091"
|
||||||
|
if ($password -ne '') {
|
||||||
|
$cs += ";Password=$password"
|
||||||
|
}
|
||||||
|
return $cs
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-Table($connection, [string] $sql) {
|
||||||
|
$cmd = $connection.CreateCommand()
|
||||||
|
$cmd.CommandText = $sql
|
||||||
|
$adapter = New-Object System.Data.SqlServerCe.SqlCeDataAdapter($cmd)
|
||||||
|
$table = New-Object System.Data.DataTable
|
||||||
|
[void] $adapter.Fill($table)
|
||||||
|
|
||||||
|
$rows = New-Object System.Collections.Generic.List[object]
|
||||||
|
foreach ($row in $table.Rows) {
|
||||||
|
$item = [ordered] @{}
|
||||||
|
foreach ($col in $table.Columns) {
|
||||||
|
$value = $row[$col]
|
||||||
|
if ($value -is [DBNull]) {
|
||||||
|
$item[$col.ColumnName] = $null
|
||||||
|
} elseif ($value -is [datetime]) {
|
||||||
|
$item[$col.ColumnName] = ([datetime] $value).ToString('o')
|
||||||
|
} else {
|
||||||
|
$item[$col.ColumnName] = $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$rows.Add([pscustomobject] $item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function Open-Sdf([string] $path, [string] $password) {
|
||||||
|
$cs = New-ConnectionString $path $password
|
||||||
|
try {
|
||||||
|
$conn = New-Object System.Data.SqlServerCe.SqlCeConnection($cs)
|
||||||
|
$conn.Open()
|
||||||
|
return $conn
|
||||||
|
} catch {
|
||||||
|
$message = $_.Exception.Message
|
||||||
|
if ($message -notmatch 'earlier version|Upgrade') {
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
$engine = New-Object System.Data.SqlServerCe.SqlCeEngine($cs)
|
||||||
|
$engine.Upgrade()
|
||||||
|
$conn = New-Object System.Data.SqlServerCe.SqlCeConnection($cs)
|
||||||
|
$conn.Open()
|
||||||
|
return $conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $SdfPath)) {
|
||||||
|
throw "SDF soubor neexistuje: $SdfPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
Add-Type -Path (Find-SqlCeDll $SqlCeDll)
|
||||||
|
|
||||||
|
$workDir = Split-Path -Parent $OutFile
|
||||||
|
if (-not (Test-Path -LiteralPath $workDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $workDir | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$workSdf = Join-Path $workDir 'source.sdf'
|
||||||
|
Copy-Item -LiteralPath $SdfPath -Destination $workSdf -Force
|
||||||
|
|
||||||
|
$passwords = @()
|
||||||
|
if ($Password -ne '') { $passwords += $Password }
|
||||||
|
$passwords += ''
|
||||||
|
|
||||||
|
$connection = $null
|
||||||
|
$lastError = $null
|
||||||
|
foreach ($pw in $passwords) {
|
||||||
|
try {
|
||||||
|
$connection = Open-Sdf $workSdf $pw
|
||||||
|
break
|
||||||
|
} catch {
|
||||||
|
$lastError = $_
|
||||||
|
$connection = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $connection) {
|
||||||
|
throw "Nepodařilo se otevřít SDF databázi. $($lastError.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$payload = [ordered] @{
|
||||||
|
uzivatel = @(Read-Table $connection 'SELECT * FROM UZIVATEL ORDER BY ID')
|
||||||
|
zakaznik = @(Read-Table $connection 'SELECT * FROM ZAKAZNIK ORDER BY ID')
|
||||||
|
faktura = @(Read-Table $connection 'SELECT * FROM FAKTURA ORDER BY ID')
|
||||||
|
polozka = @(Read-Table $connection 'SELECT * FROM POLOZKA ORDER BY ID')
|
||||||
|
platba = @(Read-Table $connection 'SELECT * FROM PLATBA ORDER BY ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = $payload | ConvertTo-Json -Depth 6 -Compress:$false
|
||||||
|
[System.IO.File]::WriteAllText($OutFile, $json, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
} finally {
|
||||||
|
if ($connection -and $connection.State -eq 'Open') {
|
||||||
|
$connection.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,11 @@ parameters:
|
||||||
user: kufrik
|
user: kufrik
|
||||||
password: 'giteaKufrik_2026!'
|
password: 'giteaKufrik_2026!'
|
||||||
|
|
||||||
|
# Nahrané SDF soubory (SQL Server Compact) a heslo staré databáze
|
||||||
|
sdfStorageDir: %appDir%/../data/sdf
|
||||||
|
sdfPassword: 'Kmetix2016_'
|
||||||
|
sdfExportScript: %appDir%/../bin/sdf-export.ps1
|
||||||
|
|
||||||
application:
|
application:
|
||||||
errorPresenter:
|
errorPresenter:
|
||||||
4xx: Error:Error4xx
|
4xx: Error:Error4xx
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ services:
|
||||||
- App\Model\Security\Authenticator(@database.default.connection)
|
- App\Model\Security\Authenticator(@database.default.connection)
|
||||||
|
|
||||||
# Seznam databází (LOGIN) z Master DB
|
# Seznam databází (LOGIN) z Master DB
|
||||||
- App\Model\Facade\DatabazeFacade(@database.default.connection)
|
- App\Model\Facade\DatabazeFacade(@database.default.connection, %sdfStorageDir%)
|
||||||
|
|
||||||
|
- App\Model\Sdf\SdfCompactExporter(%sdfExportScript%, %sdfPassword%)
|
||||||
|
- App\Model\Sdf\SdfKufrikImporter
|
||||||
|
|
||||||
# 1. Factory pro klientskou databázi
|
# 1. Factory pro klientskou databázi
|
||||||
kufrikConnectionFactory:
|
kufrikConnectionFactory:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue