Correção de horario e implantação da geração de Romaneio

This commit is contained in:
2026-08-27 16:21:56 -03:00
parent a0aafe62fc
commit ad1a071e4b
51 changed files with 3450 additions and 46 deletions

View File

@@ -0,0 +1,101 @@
# app/services/romaneios/enriquecimento_gade.rb
#
# Preenche APARELHO, TELEFONES e NOME do romaneio a partir da tabela da operação
# (gade_entregas_*). É o que dispensa a SEGUNDA PLANILHA do programa Python: lá o
# operador subia um .xlsx só com NOTA FISCAL + STATUS para marcar quem é NOVO; aqui
# o STATUS já está no banco, na própria operação.
#
# A tabela é SOMENTE LEITURA (apenas SELECT), o nome passa por Operacao.valida? +
# quote_table_name e as NFs por conn.quote.
module Romaneios
class EnriquecimentoGade
# chave usada no SELECT => coluna real na gade.
#
# ⚠️ `status` PRECISA sair aliasado: o espelho de rastreio também tem uma coluna
# `status` (o estado da visita), e qualquer query que junte as duas trocaria uma
# pela outra em silêncio. Analytics::OperacaoMetricas já faz esse mesmo alias
# (GADE_OPCIONAIS) pelo mesmo motivo.
COLUNAS = {
'nota_fiscal' => 'nota_fiscal',
'nome_completo' => 'nome_completo',
'telefones' => 'telefones',
'status' => 'status_gade'
}.freeze
# Sem essas, não há o que enriquecer.
ESSENCIAIS = %w[nota_fiscal].freeze
attr_reader :tabela
def initialize(tabela)
@tabela = tabela.to_s
raise ArgumentError, "Operação inválida: #{@tabela}" unless Operacao.valida?(@tabela)
end
# { '90507' => { aparelho: 'SIM', telefones: '(11) ...', nome: 'CICERA ...' } }
# NF que não estiver na operação simplesmente não vem no Hash — é assim que a
# tela marca "fora da operação" (plano avulso / de inclusão), mesma semântica de
# Analytics::NotasForaOperacao.
def para_notas(notas)
lista = Array(notas).map { |n| n.to_s.strip }.reject(&:empty?).uniq
return {} if lista.empty?
registros(lista).each_with_object({}) do |r, memo|
nf = r['nota_fiscal'].to_s.strip
next if nf.empty?
memo[nf] = {
aparelho: Normalizador.aparelho_por_status(r['status_gade']),
telefones: r['telefones'].to_s.strip,
nome: Normalizador.nome(r['nome_completo'])
}
end
end
# Colunas que esta operação NÃO tem — a tela precisa DIZER isso.
#
# O esquema das gade_entregas_* varia por operação (só `nota_fiscal` é
# garantida). Quando falta `status`, o APARELHO sai vazio para TODO MUNDO — e um
# romaneio em que ninguém recebe aparelho, entregue calado, é pior do que um
# aviso feio na tela.
def colunas_ausentes
@colunas_ausentes ||= COLUNAS.keys - existentes
end
def utilizavel?
(ESSENCIAIS - existentes).empty?
end
def label
Operacao.label(@tabela)
end
private
def conn
ActiveRecord::Base.connection
end
def existentes
@existentes ||= conn.columns(@tabela).map(&:name)
end
def registros(lista)
return [] unless utilizavel?
valores = lista.map { |n| conn.quote(n) }.join(', ')
sql = "SELECT #{selects} FROM #{conn.quote_table_name(@tabela)} g " \
"WHERE g.nota_fiscal IN (#{valores})"
conn.select_all(sql).to_a
end
# Coluna que não existir vira NULL com o mesmo alias, preservando a forma do
# resultado — mesmo padrão de SimpliRoute::PlanilhaCarga#selects. As chaves de
# COLUNAS são whitelist fixa (não há entrada de usuário no SQL).
def selects
COLUNAS.map do |coluna, apelido|
existentes.include?(coluna) ? "g.#{coluna} AS #{apelido}" : "CAST(NULL AS text) AS #{apelido}"
end.join(', ')
end
end
end