correção de bugs
This commit is contained in:
252
bin/entregas_do_motorista
Executable file
252
bin/entregas_do_motorista
Executable file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# Lista, DIRETO NA API do SimpliRoute, todas as visitas de um motorista dentro
|
||||
# de um período.
|
||||
#
|
||||
# ⚠️ POR QUE É DIA A DIA (e não uma chamada só):
|
||||
# A API NÃO tem filtro de intervalo. Testado contra a API real em 21/07/2026
|
||||
# (bin/sondar_busca_nf): `planned_date_from/to`, `planned_date_after/before`,
|
||||
# `__gte/__lte`, `since/until`, `start_date/end_date`, `date_from/to` — TODOS
|
||||
# ignorados em silêncio (200 + o conjunto padrão inteiro). E SEM `planned_date`
|
||||
# a API devolve um conjunto padrão (~2469 visitas) que NÃO cobre o histórico.
|
||||
# Logo: `planned_date` é obrigatório e o período vira N requisições, uma por dia,
|
||||
# disparadas em paralelo.
|
||||
#
|
||||
# ⚠️ Não filtramos motorista no servidor: `search=` só foi comprovado para NF, e
|
||||
# parâmetro que a API não registra é IGNORADO em silêncio (devolve o dia inteiro
|
||||
# com HTTP 200). Então baixamos o dia inteiro e filtramos AQUI — é lento
|
||||
# (~3,8 MB / ~9 s por dia) mas é a única forma correta.
|
||||
#
|
||||
# SÓ LEITURA: o script faz apenas GET. Não altera nada.
|
||||
#
|
||||
# Rode NO SERVIDOR (é lá que vive o SIMPLIROUTE_TOKEN):
|
||||
#
|
||||
# SIMPLIROUTE_TOKEN=xxx bin/entregas_do_motorista \
|
||||
# --motorista "JOSE DA SILVA" --de 2026-08-01 --ate 2026-08-14
|
||||
#
|
||||
# ... --csv /tmp/entregas.csv grava o resultado em CSV
|
||||
# ... --listar-motoristas só mostra quem aparece no período (sem filtrar)
|
||||
# ... --campos mostra as chaves cruas de uma visita (diagnóstico)
|
||||
#
|
||||
# Alternativa MUITO mais barata quando não precisa do dado ao vivo: o espelho
|
||||
# local (model Entrega, tabela db_reem_simplerout_2026) já tem a coluna `driver`
|
||||
# e responde isso numa query só:
|
||||
#
|
||||
# Entrega.no_periodo('2026-08-01', '2026-08-14').do_motorista('JOSE DA SILVA')
|
||||
#
|
||||
# O espelho sincroniza depois da API, então só use este script quando precisar
|
||||
# do estado ATUAL ou desconfiar da sync.
|
||||
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
require 'uri'
|
||||
require 'date'
|
||||
require 'csv'
|
||||
|
||||
BASE = ENV.fetch('SIMPLIROUTE_BASE_URL', 'https://api.simpliroute.com')
|
||||
TOKEN = ENV['SIMPLIROUTE_TOKEN'].to_s
|
||||
THREADS = 6 # mesmo lote usado no sondar_busca_nf; acima disso a API estrangula
|
||||
|
||||
def sair(msg)
|
||||
warn msg
|
||||
exit 1
|
||||
end
|
||||
|
||||
sair('Defina SIMPLIROUTE_TOKEN no ambiente.') if TOKEN.empty?
|
||||
|
||||
motorista = nil
|
||||
de = nil
|
||||
ate = nil
|
||||
csv_path = nil
|
||||
listar = false
|
||||
campos = false
|
||||
ARGV.each_with_index do |a, i|
|
||||
case a
|
||||
when '--motorista' then motorista = ARGV[i + 1]
|
||||
when '--de' then de = ARGV[i + 1]
|
||||
when '--ate', '--até' then ate = ARGV[i + 1]
|
||||
when '--csv' then csv_path = ARGV[i + 1]
|
||||
when '--listar-motoristas' then listar = true
|
||||
when '--campos' then campos = true
|
||||
end
|
||||
end
|
||||
|
||||
sair('Uso: bin/entregas_do_motorista --motorista "NOME" --de YYYY-MM-DD --ate YYYY-MM-DD') if de.to_s.empty? || ate.to_s.empty?
|
||||
sair('Informe --motorista, ou use --listar-motoristas.') if motorista.to_s.empty? && !listar && !campos
|
||||
|
||||
inicio = begin
|
||||
Date.parse(de)
|
||||
rescue ArgumentError
|
||||
sair("Data inválida em --de: #{de}")
|
||||
end
|
||||
fim = begin
|
||||
Date.parse(ate)
|
||||
rescue ArgumentError
|
||||
sair("Data inválida em --ate: #{ate}")
|
||||
end
|
||||
sair('--de é depois de --ate.') if inicio > fim
|
||||
|
||||
DIAS = (inicio..fim).to_a
|
||||
|
||||
# Normaliza nome para comparar: sem acento, sem espaço duplo, maiúsculo.
|
||||
# O nome no SimpliRoute é digitado por gente — "José da Silva" e "JOSE DA SILVA"
|
||||
# são o mesmo motorista.
|
||||
def normalizar(nome)
|
||||
nome.to_s
|
||||
.unicode_normalize(:nfd).gsub(/\p{Mn}/, '')
|
||||
.upcase.gsub(/\s+/, ' ').strip
|
||||
end
|
||||
|
||||
def requisicao(caminho)
|
||||
uri = URI.join(BASE, caminho)
|
||||
req = Net::HTTP::Get.new(uri)
|
||||
req['Authorization'] = "Token #{TOKEN}"
|
||||
req['Accept'] = 'application/json'
|
||||
|
||||
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
||||
open_timeout: 10, read_timeout: 180) { |h| h.request(req) }
|
||||
return [res.code.to_i, nil] unless res.code.to_i == 200
|
||||
|
||||
[200, (res.body.to_s.empty? ? nil : JSON.parse(res.body))]
|
||||
rescue StandardError => e
|
||||
[0, "ERRO: #{e.class}: #{e.message}"]
|
||||
end
|
||||
|
||||
# A resposta pode ser lista crua OU paginada ({count, next, results}).
|
||||
def itens(corpo)
|
||||
return corpo if corpo.is_a?(Array)
|
||||
return Array(corpo['results']) if corpo.is_a?(Hash) && corpo.key?('results')
|
||||
|
||||
[]
|
||||
end
|
||||
|
||||
# Onde está o nome do motorista numa visita. A lista-por-data às vezes traz o
|
||||
# campo achatado ('driver'), às vezes só o id, às vezes um hash aninhado — por
|
||||
# isso procuramos em várias formas em vez de fixar uma. Se nada casar, o script
|
||||
# avisa e o --campos mostra o formato real.
|
||||
CHAVES_MOTORISTA = %w[driver driver_name route_driver driver_full_name].freeze
|
||||
|
||||
def nome_do_motorista(visita)
|
||||
CHAVES_MOTORISTA.each do |k|
|
||||
v = visita[k]
|
||||
next if v.nil? || v == '' || v.is_a?(Numeric)
|
||||
|
||||
return v if v.is_a?(String)
|
||||
return (v['name'] || v['full_name'] ||
|
||||
[v['first_name'], v['last_name']].compact.join(' ')) if v.is_a?(Hash)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
puts "Base: #{BASE}"
|
||||
puts "Período: #{inicio.strftime('%d/%m/%Y')} a #{fim.strftime('%d/%m/%Y')} (#{DIAS.size} dia(s), #{THREADS} em paralelo)"
|
||||
puts "Motorista: #{motorista}" if motorista
|
||||
puts '(a API não filtra por intervalo nem por motorista — baixando dia a dia; pode levar ~1 min)'
|
||||
puts
|
||||
|
||||
# ── Varredura dia a dia, em lotes ────────────────────────────────────────────
|
||||
t0 = Time.now
|
||||
visitas = []
|
||||
falhas = []
|
||||
mutex = Mutex.new
|
||||
|
||||
DIAS.each_slice(THREADS) do |lote|
|
||||
lote.map do |dia|
|
||||
Thread.new do
|
||||
cod, corpo = requisicao("/v1/routes/visits/?planned_date=#{dia.iso8601}")
|
||||
lista = cod == 200 ? itens(corpo) : []
|
||||
mutex.synchronize do
|
||||
if cod == 200
|
||||
# planned_date às vezes não volta na visita — garantimos o dia pedido.
|
||||
lista.each { |v| v['_dia'] = dia.iso8601 }
|
||||
visitas.concat(lista)
|
||||
print '.'
|
||||
else
|
||||
falhas << [dia, cod, corpo]
|
||||
print 'x'
|
||||
end
|
||||
end
|
||||
end
|
||||
end.each(&:join)
|
||||
end
|
||||
puts "\nBaixadas #{visitas.size} visita(s) em #{(Time.now - t0).round(1)}s"
|
||||
falhas.each { |d, c, e| warn " ⚠️ #{d}: HTTP #{c} #{e.inspect[0, 120]}" }
|
||||
sair('Nenhuma visita voltou — período sem operação ou token sem acesso.') if visitas.empty?
|
||||
|
||||
# ── Diagnóstico do formato ───────────────────────────────────────────────────
|
||||
if campos
|
||||
puts "\nChaves de uma visita crua:"
|
||||
puts visitas.first.keys.sort.inspect
|
||||
puts "\nExemplo (recortado):"
|
||||
puts JSON.pretty_generate(visitas.first)[0, 2000]
|
||||
exit 0
|
||||
end
|
||||
|
||||
com_nome = visitas.count { |v| nome_do_motorista(v) }
|
||||
if com_nome.zero?
|
||||
warn "\n⚠️ NENHUMA visita trouxe nome de motorista nas chaves #{CHAVES_MOTORISTA.inspect}."
|
||||
warn ' A lista-por-data pode estar devolvendo só o id da rota. Rode com --campos'
|
||||
warn ' para ver o formato real, ou use o espelho local (Entrega#driver).'
|
||||
exit 1
|
||||
end
|
||||
warn "\n⚠️ #{visitas.size - com_nome} visita(s) sem motorista preenchido (ficam de fora do filtro)." if com_nome < visitas.size
|
||||
|
||||
# ── Só listar quem apareceu ──────────────────────────────────────────────────
|
||||
if listar
|
||||
puts "\nMotoristas no período:"
|
||||
visitas.group_by { |v| nome_do_motorista(v) }
|
||||
.reject { |k, _| k.nil? }
|
||||
.sort_by { |_, vs| -vs.size }
|
||||
.each { |nome, vs| puts " #{vs.size.to_s.rjust(5)} #{nome}" }
|
||||
exit 0
|
||||
end
|
||||
|
||||
# ── Filtro pelo motorista ────────────────────────────────────────────────────
|
||||
alvo = normalizar(motorista)
|
||||
dele = visitas.select { |v| normalizar(nome_do_motorista(v)) == alvo }
|
||||
|
||||
if dele.empty?
|
||||
puts "\nNenhuma visita de #{motorista.inspect} no período."
|
||||
parecidos = visitas.map { |v| nome_do_motorista(v) }.compact.uniq
|
||||
.select { |n| normalizar(n).include?(alvo) || alvo.include?(normalizar(n)) }
|
||||
puts "Nomes parecidos que existem: #{parecidos.inspect}" if parecidos.any?
|
||||
puts 'Use --listar-motoristas para ver a grafia exata.'
|
||||
exit 0
|
||||
end
|
||||
|
||||
dele.sort_by! { |v| [v['_dia'].to_s, v['checkout_time'].to_s] }
|
||||
|
||||
puts "\n#{'─' * 100}"
|
||||
puts "#{dele.size} visita(s) de #{motorista}"
|
||||
puts '─' * 100
|
||||
printf("%-11s %-10s %-9s %-19s %-40s\n", 'DATA', 'NF', 'STATUS', 'CHECKOUT', 'DESTINO')
|
||||
dele.each do |v|
|
||||
printf("%-11s %-10s %-9s %-19s %-40s\n",
|
||||
Date.parse(v['_dia']).strftime('%d/%m/%Y'),
|
||||
v['reference'].to_s[0, 10],
|
||||
v['status'].to_s[0, 9],
|
||||
v['checkout_time'].to_s[0, 19].tr('T', ' '),
|
||||
v['title'].to_s[0, 40])
|
||||
end
|
||||
|
||||
puts '─' * 100
|
||||
por_status = dele.group_by { |v| v['status'].to_s }.transform_values(&:size)
|
||||
puts "Total: #{dele.size} " + por_status.sort_by { |_, n| -n }.map { |s, n| "#{s}=#{n}" }.join(' ')
|
||||
atendidas = dele.count { |v| %w[completed failed].include?(v['status'].to_s) && v['checkout_time'].to_s != '' }
|
||||
puts "Atendidas (foi ao local, com checkout): #{atendidas}"
|
||||
puts "Dias com entrega: #{dele.map { |v| v['_dia'] }.uniq.size}"
|
||||
|
||||
# ── CSV ──────────────────────────────────────────────────────────────────────
|
||||
if csv_path
|
||||
CSV.open(csv_path, 'w') do |csv|
|
||||
csv << ['Data', 'NF', 'Tracking', 'ID visita', 'Motorista', 'Status',
|
||||
'Checkout', 'Título', 'Endereço', 'Motivo (UUID)', 'Comentário']
|
||||
dele.each do |v|
|
||||
csv << [Date.parse(v['_dia']).strftime('%d/%m/%Y'), v['reference'], v['tracking_id'], v['id'],
|
||||
nome_do_motorista(v), v['status'], v['checkout_time'], v['title'], v['address'],
|
||||
v['checkout_observation'], v['checkout_comment']]
|
||||
end
|
||||
end
|
||||
puts "\nCSV gravado em #{csv_path}"
|
||||
end
|
||||
Reference in New Issue
Block a user