Path: C:\
<?php
$pageTitle = 'Importar XMLs de NF-e';
require_once __DIR__ . '/includes/header.php';
$summary = null;
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['xmls'])) {
try {
$files = $_FILES['xmls'];
$totalSubmitted = is_array($files['name']) ? count($files['name']) : 0;
if ($totalSubmitted === 0 || empty($files['name'][0])) {
throw new Exception("Nenhum arquivo XML ou ZIP foi selecionado para envio.");
}
$movimentacoes = load_json('movimentacoes.json');
$produtos = load_json('produtos.json');
// Mapear chaves existentes para verificação rápida de duplicidade
$existingKeys = [];
foreach ($movimentacoes as $mov) {
if (!empty($mov['chave_nfe'])) {
$existingKeys[$mov['chave_nfe']] = true;
}
}
// Mapear produtos existentes por cliente_documento|codigo para auto-cadastro
$produtosMap = [];
foreach ($produtos as $idx => $p) {
if (!empty($p['codigo'])) {
$doc = !empty($p['cliente_documento']) ? $p['cliente_documento'] : 'GERAL';
$key = $doc . '|' . $p['codigo'];
$produtosMap[$key] = $idx;
}
}
$processedCount = 0;
$duplicateCount = 0;
$errorCount = 0;
$newProductsCount = 0;
$extractedFromZipCount = 0;
$errorMessages = [];
libxml_use_internal_errors(true);
$tempZipDir = __DIR__ . '/data/temp_zip_' . uniqid();
$xmlsToProcess = [];
for ($i = 0; $i < $totalSubmitted; $i++) {
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
$errorCount++;
$errorMessages[] = "Erro no upload do arquivo " . htmlspecialchars($files['name'][$i]);
continue;
}
$tmpPath = $files['tmp_name'][$i];
$fileName = $files['name'][$i];
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if ($ext === 'zip') {
if (!class_exists('ZipArchive')) {
$errorCount++;
$errorMessages[] = "A extensão PHP ZipArchive não está habilitada no servidor.";
continue;
}
$zip = new ZipArchive();
if ($zip->open($tmpPath) === true) {
if (!is_dir($tempZipDir)) {
@mkdir($tempZipDir, 0755, true);
}
for ($z = 0; $z < $zip->numFiles; $z++) {
$entryName = $zip->getNameIndex($z);
$entryExt = strtolower(pathinfo($entryName, PATHINFO_EXTENSION));
if ($entryExt === 'xml' && substr($entryName, -1) !== '/') {
$content = $zip->getFromIndex($z);
if ($content !== false && strlen($content) > 0) {
$extractedPath = $tempZipDir . '/zip_file_' . $z . '_' . basename($entryName);
file_put_contents($extractedPath, $content);
$xmlsToProcess[] = [
'path' => $extractedPath,
'name' => basename($entryName)
];
$extractedFromZipCount++;
}
}
}
$zip->close();
} else {
$errorCount++;
$errorMessages[] = "Não foi possível abrir o arquivo ZIP: {$fileName}";
}
} elseif ($ext === 'xml') {
$xmlsToProcess[] = [
'path' => $tmpPath,
'name' => $fileName
];
} else {
$errorCount++;
$errorMessages[] = "Formato de arquivo não suportado: {$fileName}. Envie apenas arquivos .XML ou .ZIP.";
}
}
// Processar cada arquivo XML da lista
foreach ($xmlsToProcess as $itemXml) {
$xmlPath = $itemXml['path'];
$xmlName = $itemXml['name'];
if (!file_exists($xmlPath) || filesize($xmlPath) === 0) {
$errorCount++;
$errorMessages[] = "Arquivo XML vazio ou inacessível: {$xmlName}";
continue;
}
$xmlContent = file_get_contents($xmlPath);
$xml = simplexml_load_string($xmlContent, 'SimpleXMLElement', LIBXML_NONET);
if ($xml === false) {
$libErrors = libxml_get_errors();
libxml_clear_errors();
$errStr = !empty($libErrors) ? trim($libErrors[0]->message) : "XML inválido";
$errorCount++;
$errorMessages[] = "Erro de leitura no XML {$xmlName}: {$errStr}";
continue;
}
// Namespaces NF-e
$namespaces = $xml->getNamespaces(true);
$nfeNs = $namespaces[''] ?? 'http://www.portalfiscal.inf.br/nfe';
$xml->registerXPathNamespace('nfe', $nfeNs);
$infNFeList = $xml->xpath('//nfe:infNFe') ?: $xml->xpath('//infNFe');
if (empty($infNFeList)) {
$errorCount++;
$errorMessages[] = "Estrutura incompatível com NF-e no arquivo {$xmlName}: tag <infNFe> não encontrada.";
continue;
}
$infNFe = $infNFeList[0];
$attributes = $infNFe->attributes();
$rawId = (string)($attributes['Id'] ?? '');
$chaveNfe = preg_replace('/[^0-9]/', '', $rawId);
if (strlen($chaveNfe) !== 44) {
$errorCount++;
$errorMessages[] = "Chave NF-e inválida no arquivo {$xmlName}.";
continue;
}
// Verificar duplicidade
if (isset($existingKeys[$chaveNfe])) {
$duplicateCount++;
continue;
}
$ide = $infNFe->ide ?? null;
$numeroNfe = (string)($ide->nNF ?? '0');
$dhEmi = (string)($ide->dhEmi ?? $ide->dEmi ?? date('Y-m-d'));
$dataEmissao = substr($dhEmi, 0, 10);
$tpNF = isset($ide->tpNF) ? (string)$ide->tpNF : '1';
// Extrair Emitente (<emit>)
$emit = $infNFe->emit ?? null;
$emitDoc = (string)($emit->CNPJ ?? $emit->CPF ?? '');
$emitDocClean = !empty($emitDoc) ? preg_replace('/[^0-9]/', '', $emitDoc) : '';
$emitNome = (string)($emit->xNome ?? 'Emitente Não Informado');
// Extrair Destinatário (<dest>)
$dest = $infNFe->dest ?? null;
$destDoc = (string)($dest->CNPJ ?? $dest->CPF ?? '');
$destDocClean = !empty($destDoc) ? preg_replace('/[^0-9]/', '', $destDoc) : '';
$destNome = (string)($dest->xNome ?? 'Destinatário Não Informado');
// Determinar Cliente Real e Tipo (Entrada vs Saída)
$clientOp = determine_operation_client($emitDocClean, $emitNome, $destDocClean, $destNome, $tpNF);
$tipoMovimento = $clientOp['tipo'];
$clienteDoc = $clientOp['cliente_documento'];
$clienteNome = $clientOp['cliente_nome'];
$vNF = (string)($infNFe->total->ICMSTot->vNF ?? '0.00');
$valorNfe = floatval(str_replace(',', '.', $vNF));
$itens = [];
if (isset($infNFe->det)) {
foreach ($infNFe->det as $det) {
$prod = $det->prod ?? null;
if ($prod) {
$codigo = trim((string)($prod->cProd ?? ''));
$nome = trim((string)($prod->xProd ?? ''));
$qCom = floatval(str_replace(',', '.', (string)($prod->qCom ?? '0')));
$vProd = floatval(str_replace(',', '.', (string)($prod->vProd ?? '0')));
if (!empty($codigo)) {
$itens[] = [
'codigo' => $codigo,
'nome' => $nome,
'quantidade' => $qCom,
'valor_total' => $vProd
];
// Auto-cadastro vinculado ao cliente_documento em produtos.json
$prodCompositeKey = $clienteDoc . '|' . $codigo;
if (!isset($produtosMap[$prodCompositeKey])) {
$produtos[] = [
'cliente_documento' => $clienteDoc,
'codigo' => $codigo,
'nome' => $nome,
'fator_palete' => null
];
$produtosMap[$prodCompositeKey] = count($produtos) - 1;
$newProductsCount++;
}
}
}
}
}
$movimentacoes[] = [
'chave_nfe' => $chaveNfe,
'numero_nfe' => $numeroNfe,
'data_emissao' => $dataEmissao,
'tipo' => $tipoMovimento,
'cliente_documento' => $clienteDoc,
'cliente_nome' => $clienteNome,
'emitente_documento' => $emitDocClean,
'emitente_nome' => $emitNome,
'destinatario_documento' => $destDocClean,
'destinatario_nome' => $destNome,
'valor_nfe' => $valorNfe,
'itens' => $itens
];
$existingKeys[$chaveNfe] = true;
$processedCount++;
}
// Limpar pasta temporária de extração ZIP
if (is_dir($tempZipDir)) {
$tmpFiles = glob($tempZipDir . '/*');
foreach ($tmpFiles as $f) {
if (is_file($f)) @unlink($f);
}
@rmdir($tempZipDir);
}
// Salvar JSONs com LOCK_EX
save_json('movimentacoes.json', $movimentacoes);
save_json('produtos.json', $produtos);
$summary = [
'total' => $totalSubmitted,
'zip_extracted' => $extractedFromZipCount,
'processed' => $processedCount,
'duplicates' => $duplicateCount,
'errors' => $errorCount,
'new_products' => $newProductsCount,
'error_messages' => $errorMessages
];
} catch (Exception $e) {
$error = $e->getMessage();
}
}
?>
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<div>
<h1 class="h3 mb-0 text-gray-800">Importação de XMLs de NF-e (Entradas e Saídas)</h1>
<p class="text-muted small mb-0">Envie arquivos XML individuais ou pacotes ZIP. O Cliente da Operação e o Tipo (Entrada/Saída) serão identificados automaticamente.</p>
</div>
<div>
<a href="estoque.php" class="btn btn-outline-success btn-sm me-2">
<i class="bi bi-box2"></i> Ver Saldo de Estoque
</a>
<a href="produtos.php" class="btn btn-outline-secondary btn-sm me-2">
<i class="bi bi-boxes"></i> Ver Produtos
</a>
<a href="index.php" class="btn btn-primary btn-sm">
<i class="bi bi-bar-chart-line"></i> Ver Fechamento
</a>
</div>
</div>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> <?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($summary): ?>
<div class="card mb-4 border-success">
<div class="card-header bg-success text-white d-flex align-items-center justify-content-between">
<span><i class="bi bi-check-circle-fill me-2"></i> Resultado da Importação</span>
<span class="badge bg-light text-success"><?= $summary['processed'] ?> NFs Importadas</span>
</div>
<div class="card-body">
<div class="row text-center g-3 mb-3">
<div class="col-md-3 col-6">
<div class="p-3 bg-light rounded border">
<div class="text-muted small">Arquivos Submetidos</div>
<div class="fs-4 fw-bold"><?= $summary['total'] ?></div>
<?php if ($summary['zip_extracted'] > 0): ?>
<div class="text-primary small font-semibold">(<?= $summary['zip_extracted'] ?> XMLs extraídos do ZIP)</div>
<?php endif; ?>
</div>
</div>
<div class="col-md-3 col-6">
<div class="p-3 bg-light rounded border">
<div class="text-success small">Processados</div>
<div class="fs-4 fw-bold text-success"><?= $summary['processed'] ?></div>
</div>
</div>
<div class="col-md-3 col-6">
<div class="p-3 bg-light rounded border">
<div class="text-secondary small">Duplicados (Ignorados)</div>
<div class="fs-4 fw-bold text-secondary"><?= $summary['duplicates'] ?></div>
</div>
</div>
<div class="col-md-3 col-6">
<div class="p-3 bg-light rounded border">
<div class="text-warning small">Novos Produtos Cadastrados</div>
<div class="fs-4 fw-bold text-warning"><?= $summary['new_products'] ?></div>
</div>
</div>
</div>
<?php if ($summary['new_products'] > 0): ?>
<div class="alert alert-warning mb-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Atenção:</strong> <?= $summary['new_products'] ?> novo(s) produto(s) foram adicionado(s) vinculados ao Cliente da Operação.
<a href="produtos.php" class="alert-link">Clique aqui para configurar o Fator Palete</a>.
</div>
<?php endif; ?>
<?php if (!empty($summary['error_messages'])): ?>
<div class="alert alert-danger mb-0">
<h6 class="alert-heading font-bold mb-2"><i class="bi bi-x-circle-fill me-1"></i> Falhas em arquivos:</h6>
<ul class="mb-0 ps-3 small">
<?php foreach ($summary['error_messages'] as $msg): ?>
<li><?= htmlspecialchars($msg) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<div class="card">
<div class="card-header">
<i class="bi bi-cloud-upload me-2 text-primary"></i> Selecione os arquivos XML ou pacotes ZIP
</div>
<div class="card-body p-4">
<form action="upload.php" method="POST" enctype="multipart/form-data">
<div class="mb-4">
<label for="xmls" class="form-label font-semibold">Arquivos de NF-e (.XML ou .ZIP)</label>
<input type="file" name="xmls[]" id="xmls" class="form-control form-control-lg" accept=".xml,.zip" multiple required>
<div class="form-text mt-2">
<i class="bi bi-info-circle me-1"></i>
<strong>Identificação Automática:</strong> NFs com Destinatário Trans War são atreladas ao Cliente Remetente (Entrada). NFs com Emitente Trans War são atreladas ao Cliente Destinatário (Saída).
</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary btn-lg px-4">
<i class="bi bi-upload me-2"></i> Processar Arquivos
</button>
</div>
</form>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>