Correção de horario e implantação da geração de Romaneio
This commit is contained in:
215
app/services/romaneios/importador.rb
Normal file
215
app/services/romaneios/importador.rb
Normal file
@@ -0,0 +1,215 @@
|
||||
# app/services/romaneios/importador.rb
|
||||
#
|
||||
# O ÚNICO lugar que sabe reconciliar um plano recebido com o romaneio já editado.
|
||||
# Os dois caminhos de entrada (API do SimpliRoute e upload do .xlsx) entregam a
|
||||
# mesma forma de linha e caem aqui — senão a mesma parada importada por caminhos
|
||||
# diferentes viraria duas.
|
||||
#
|
||||
# ── A REGRA (dirty tracking POR CAMPO, não por linha) ───────────────────────
|
||||
# 1. Identidade = chave_origem (NF, ou digest do título+endereço). Veículo e ordem
|
||||
# NÃO entram: eles mudam quando o plano é reotimizado e a parada continua sendo
|
||||
# a mesma entrega.
|
||||
# 2. Linha que já existe: `origem` (o snapshot do plano) é SEMPRE atualizada; os
|
||||
# campos são reescritos SÓ se o operador não os tiver tocado.
|
||||
# 3. Linha nova: insere.
|
||||
# 4. Linha que sumiu do plano: sem edição humana, apaga; com edição, marca
|
||||
# `removido_em` — apagar levaria junto o trabalho sem deixar rastro na tela.
|
||||
# 5. NF repetida no plano: fica a primeira, as outras são reportadas.
|
||||
#
|
||||
# Por que POR CAMPO e não um "bloqueado" na linha inteira: a edição comum é "o
|
||||
# operador corrigiu um telefone". Travar a linha congelaria um endereço velho quando
|
||||
# o SimpliRoute regeocodificasse, e o papel sairia com um endereço que o motorista
|
||||
# não acha. Por campo é a única regra em que "corrigi o telefone" não significa
|
||||
# "também respondo pelo endereço".
|
||||
module Romaneios
|
||||
class Importador
|
||||
Resultado = Struct.new(:novos, :atualizados, :preservados, :saidos, :duplicados,
|
||||
:veiculos, :colunas_ausentes, keyword_init: true) do
|
||||
def to_h
|
||||
super.transform_values { |v| v.is_a?(Array) ? v : v.to_i }
|
||||
end
|
||||
|
||||
# Frase que a tela mostra depois de importar. Números soltos não dizem se
|
||||
# deu certo; esta linha diz.
|
||||
def resumo
|
||||
partes = ["#{novos} nova(s)", "#{atualizados} atualizada(s)"]
|
||||
partes << "#{preservados} mantida(s) por edição" if preservados.to_i.positive?
|
||||
partes << "#{saidos} saíram do plano" if saidos.to_i.positive?
|
||||
partes << "#{duplicados} NF repetida(s) no plano" if duplicados.to_i.positive?
|
||||
partes.join(' · ')
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(romaneio:, linhas:, usuario: nil, origem: nil)
|
||||
@romaneio = romaneio
|
||||
@recebidas = Array(linhas)
|
||||
@usuario = usuario
|
||||
@origem = origem
|
||||
end
|
||||
|
||||
def aplicar!
|
||||
novas, duplicados = deduplicar(@recebidas)
|
||||
dados = enriquecer(novas)
|
||||
|
||||
novos = atualizados = preservados = saidos = 0
|
||||
vistas = []
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
existentes = @romaneio.romaneio_linhas.index_by(&:chave_origem)
|
||||
|
||||
dados.each do |linha|
|
||||
vistas << linha[:chave_origem]
|
||||
registro = existentes[linha[:chave_origem]]
|
||||
|
||||
if registro.nil?
|
||||
criar(linha)
|
||||
novos += 1
|
||||
else
|
||||
preservados += atualizar(registro, linha)
|
||||
atualizados += 1
|
||||
end
|
||||
end
|
||||
|
||||
saidos = remover_ausentes(existentes, vistas)
|
||||
sincronizar_veiculos(dados)
|
||||
|
||||
@romaneio.update!(
|
||||
origem: @origem || @romaneio.origem,
|
||||
importado_em: Time.current,
|
||||
importado_por: @usuario || @romaneio.importado_por
|
||||
)
|
||||
end
|
||||
|
||||
# Fora do bloco de propósito: `return` dentro de uma transaction do Rails 7
|
||||
# COMMITA em silêncio, o que é uma pegadinha esperando um leitor distraído.
|
||||
Resultado.new(
|
||||
novos: novos, atualizados: atualizados, preservados: preservados,
|
||||
saidos: saidos, duplicados: duplicados,
|
||||
veiculos: dados.map { |l| l[:veiculo] }.uniq.size,
|
||||
colunas_ausentes: @colunas_ausentes || []
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# A primeira ocorrência vence. Plano duplicado no SimpliRoute gera visita nova
|
||||
# com a MESMA reference — repetir a parada no papel faria o motorista levar dois
|
||||
# pacotes para o mesmo endereço.
|
||||
def deduplicar(linhas)
|
||||
vistas = {}
|
||||
repetidas = 0
|
||||
|
||||
linhas.each do |linha|
|
||||
chave = RomaneioLinha.chave_para(nota_fiscal: linha[:nota_fiscal],
|
||||
titulo: linha[:titulo], endereco: linha[:endereco])
|
||||
if vistas.key?(chave)
|
||||
repetidas += 1
|
||||
next
|
||||
end
|
||||
vistas[chave] = linha.merge(chave_origem: chave)
|
||||
end
|
||||
|
||||
[vistas.values, repetidas]
|
||||
end
|
||||
|
||||
# Aplica o que a operação sabe por cima do que o plano trouxe.
|
||||
# Precedência (decidida com o usuário): operação > plano.
|
||||
def enriquecer(linhas)
|
||||
tabela = @romaneio.operacao_tabela.to_s
|
||||
return linhas if tabela.empty?
|
||||
|
||||
gade = EnriquecimentoGade.new(tabela)
|
||||
@colunas_ausentes = gade.colunas_ausentes
|
||||
dados = gade.para_notas(linhas.map { |l| l[:nota_fiscal] })
|
||||
|
||||
linhas.map do |linha|
|
||||
extra = dados[linha[:nota_fiscal].to_s] || {}
|
||||
linha.merge(
|
||||
aparelho: extra[:aparelho].to_s,
|
||||
# A coluna `telefones` da operação é a fonte completa; o "TEL:" das
|
||||
# anotações do plano só entra quando ela não tem o número.
|
||||
telefones: extra[:telefones].to_s.empty? ? linha[:telefones].to_s : extra[:telefones],
|
||||
nome: extra[:nome].to_s.empty? ? linha[:nome].to_s : extra[:nome]
|
||||
)
|
||||
end
|
||||
rescue ArgumentError
|
||||
# Operação removida do banco entre a criação do romaneio e a reimportação:
|
||||
# segue com o que o plano trouxe, e a tela avisa.
|
||||
@colunas_ausentes = EnriquecimentoGade::COLUNAS.keys
|
||||
linhas
|
||||
end
|
||||
|
||||
def campos(linha)
|
||||
{ 'nota_fiscal' => linha[:nota_fiscal].to_s, 'nome' => linha[:nome].to_s,
|
||||
'endereco' => linha[:endereco].to_s, 'aparelho' => linha[:aparelho].to_s,
|
||||
'telefones' => linha[:telefones].to_s }
|
||||
end
|
||||
|
||||
def criar(linha)
|
||||
valores = campos(linha)
|
||||
@romaneio.romaneio_linhas.create!(
|
||||
valores.merge(
|
||||
veiculo: linha[:veiculo], ordem: linha[:ordem],
|
||||
chave_origem: linha[:chave_origem], origem: valores, campos_editados: []
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# Devolve quantos campos foram PRESERVADOS por terem edição humana.
|
||||
def atualizar(registro, linha)
|
||||
valores = campos(linha)
|
||||
preservados = 0
|
||||
|
||||
valores.each do |campo, valor|
|
||||
if registro.editado?(campo)
|
||||
preservados += 1
|
||||
next
|
||||
end
|
||||
registro[campo] = valor
|
||||
end
|
||||
|
||||
# Veículo e ordem são a verdade do roteiro, não conteúdo do documento —
|
||||
# seguem o plano mesmo numa linha editada.
|
||||
registro.veiculo = linha[:veiculo]
|
||||
registro.ordem = linha[:ordem]
|
||||
registro.origem = valores
|
||||
registro.removido_em = nil
|
||||
registro.save!
|
||||
|
||||
preservados
|
||||
end
|
||||
|
||||
def remover_ausentes(existentes, vistas)
|
||||
ausentes = existentes.except(*vistas).values
|
||||
return 0 if ausentes.empty?
|
||||
|
||||
limpas, editadas = ausentes.partition { |l| l.campos_editados.blank? }
|
||||
RomaneioLinha.where(id: limpas.map(&:id)).delete_all if limpas.any?
|
||||
RomaneioLinha.where(id: editadas.map(&:id)).update_all(removido_em: Time.current) if editadas.any?
|
||||
|
||||
ausentes.size
|
||||
end
|
||||
|
||||
# Cria a folha de cada veículo novo e reordena as páginas. O `motorista` já
|
||||
# preenchido NÃO é tocado: ele é digitado na tela, não vem do plano (o export
|
||||
# real traz a coluna vazia em todas as linhas).
|
||||
def sincronizar_veiculos(linhas)
|
||||
ordem = linhas.map { |l| l[:veiculo] }.uniq.sort
|
||||
existentes = @romaneio.romaneio_veiculos.index_by(&:veiculo)
|
||||
|
||||
ordem.each_with_index do |veiculo, i|
|
||||
registro = existentes[veiculo] ||
|
||||
@romaneio.romaneio_veiculos.build(veiculo: veiculo)
|
||||
registro.posicao = i
|
||||
motorista = linhas.find { |l| l[:veiculo] == veiculo && l[:motorista].to_s != '' }
|
||||
registro.motorista = motorista[:motorista] if motorista && registro.motorista.to_s.empty?
|
||||
registro.save!
|
||||
end
|
||||
|
||||
# Veículo que saiu do plano e ficou sem nenhuma parada não tem folha para
|
||||
# imprimir.
|
||||
sobrando = existentes.keys - ordem
|
||||
@romaneio.romaneio_veiculos.where(veiculo: sobrando).destroy_all if sobrando.any?
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user