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

@@ -166,6 +166,11 @@ module Permissao
'admin.planilha_simpli_route' => {
grupo: 'admin', label: 'Gerar planilha de carga',
descricao: 'Download do .xlsx de importação do SimpliRoute.'
},
'admin.romaneio' => {
grupo: 'admin', label: 'Gerar romaneio de entrega',
descricao: 'Importa o plano do dia, edita a tabela e imprime o "CONTROLE DE ' \
'ENTREGA" que o motorista assina ao receber a carga.'
}
}.freeze
@@ -189,6 +194,7 @@ module Permissao
consolidacao.gerir_motoristas consolidacao.exportar_pdf
notificacao.contatos notificacao.contatos_gerenciar notificacao.envios
admin.usuarios admin.configuracoes admin.auditoria admin.planilha_simpli_route
admin.romaneio
].freeze,
'operador' => %w[
dashboard.ver dashboard.financeiro dashboard.operacoes dashboard.exportar dashboard.metricas

71
app/models/romaneio.rb Normal file
View File

@@ -0,0 +1,71 @@
# app/models/romaneio.rb
#
# Um plano do SimpliRoute importado para virar o "CONTROLE DE ENTREGA" — a folha
# que o motorista assina ao retirar a carga, uma página A4 paisagem por veículo.
#
# O romaneio é NOSSO (tabela gravável). As fontes continuam somente leitura: o
# espelho de rastreio (Entrega) e as tabelas de operação (gade_entregas_*).
class Romaneio < ApplicationRecord
ORIGENS = %w[api espelho planilha].freeze
# Máximo de paradas que cabem numa folha. O número é da GEOMETRIA do PDF, então a
# fonte é Pdf::RomaneioPdf — duplicar aqui criaria duas verdades, e o dia em que
# alguém mexesse na margem a tela passaria a avisar "sai em 1 folha" para um
# romaneio que sai em 2. Lido em tempo de chamada (e não numa constante) para não
# amarrar a ordem de carga dos autoloads.
def self.linhas_por_folha
Pdf::RomaneioPdf::MAX_LINHAS
end
belongs_to :importado_por, class_name: 'User', optional: true
has_many :romaneio_veiculos, -> { order(:posicao, :veiculo) },
dependent: :destroy, inverse_of: :romaneio
has_many :romaneio_linhas, dependent: :destroy, inverse_of: :romaneio
validates :planned_date, presence: true
validates :origem, inclusion: { in: ORIGENS }
validates :planned_date, uniqueness: { scope: :operacao_tabela }
scope :recentes, -> { order(planned_date: :desc, id: :desc) }
# Linhas que entram no PDF: as que continuam no plano, na ordem do roteiro.
# `removido_em` marca a parada que saiu do plano mas tinha edição humana — ela
# fica visível na tela para o operador decidir, e NUNCA sai impressa.
def linhas_vigentes
romaneio_linhas.where(removido_em: nil).order(:veiculo, :ordem, :id)
end
def linhas_removidas
romaneio_linhas.where.not(removido_em: nil).order(:veiculo, :ordem, :id)
end
# { 'GADE_001' => 29, ... } — alimenta o seletor de veículo e o aviso de folhas.
def contagem_por_veiculo
@contagem_por_veiculo ||= linhas_vigentes.reorder(nil).group(:veiculo).count
end
def folhas_de(veiculo)
total = contagem_por_veiculo[veiculo].to_i
return 1 if total.zero?
(total.to_f / self.class.linhas_por_folha).ceil
end
# Veículos que estouram uma folha — a tela avisa antes de imprimir, porque
# descobrir isso só no papel é descobrir tarde.
def veiculos_multifolha
contagem_por_veiculo.select { |_v, n| n > self.class.linhas_por_folha }
end
def operacao_label
operacao_tabela.present? ? Operacao.label(operacao_tabela) : nil
end
# Rótulo do canto direito do PDF ("EMAD 09.2026"). Só uma SUGESTÃO: não existe
# fonte canônica para esse texto, então ele é editável na tela.
def self.rotulo_sugerido(tabela, data)
base = tabela.present? ? Operacao.label(tabela).split.first : nil
[base, data.to_date.strftime('%m.%Y')].compact.join(' ')
end
end

View File

@@ -0,0 +1,68 @@
# app/models/romaneio_linha.rb
#
# Uma PARADA do romaneio — vira uma linha da tabela do PDF.
#
# ⚠️ Nome de tabela fixado: "RomaneioLinha".tableize já dá `romaneio_linhas`, mas a
# declaração explícita é a trava contra alguém acrescentar um irregular ao
# config/initializers/inflections.rb e mudar isso sem perceber (ConsolidacaoMotorista
# já quebrou /motorista em produção exatamente assim). Coberto por
# spec/models/table_names_spec.rb.
class RomaneioLinha < ApplicationRecord
self.table_name = 'romaneio_linhas'
# Os únicos campos que o operador edita — e a whitelist que o controller usa
# para não deixar um PATCH escrever em `veiculo`, `ordem` ou `chave_origem`.
#
# `veiculo` e `ordem` NÃO são editáveis de propósito: mover uma parada de veículo
# aqui produziria um romaneio que contradiz a rota que o motorista recebe no
# celular — dois papéis, mesma entrega, respostas diferentes. Veículo errado se
# corrige no SimpliRoute e se reimporta.
CAMPOS_EDITAVEIS = %w[nota_fiscal nome endereco aparelho telefones].freeze
belongs_to :romaneio, inverse_of: :romaneio_linhas
validates :veiculo, :chave_origem, presence: true
scope :vigentes, -> { where(removido_em: nil) }
scope :removidas, -> { where.not(removido_em: nil) }
scope :do_veiculo, ->(veiculo) { where(veiculo: veiculo) }
# Identidade ESTÁVEL entre reimportações. A NF quando existe; senão um digest do
# título + endereço, porque nem toda linha do plano casa "NF 90507 - FULANO" e
# duas linhas sem NF colidiriam no índice único, derrubando a importação inteira.
#
# Veículo e ordem ficam FORA da chave: mudam quando o plano é reotimizado e a
# parada continua sendo a mesma entrega.
def self.chave_para(nota_fiscal:, titulo: nil, endereco: nil)
nf = nota_fiscal.to_s.strip
return nf if nf.present?
"T:#{Digest::MD5.hexdigest("#{titulo}|#{endereco}")}"
end
def editado?(campo)
campos_editados.include?(campo.to_s)
end
def editada?
campos_editados.present?
end
# Valor que o PLANO trouxe na última importação — é o que o botão "restaurar"
# devolve, e o que a reconciliação compara.
def valor_do_plano(campo)
origem[campo.to_s].to_s
end
# Marca o campo como tocado por humano. É ISTO que sobrevive à reimportação:
# o dirty tracking é POR CAMPO, não por linha — "corrigi o telefone" não pode
# significar "também respondo pelo endereço" e congelar um endereço velho quando
# o SimpliRoute regeocodificar.
def marcar_editado!(campo)
self.campos_editados = (campos_editados + [campo.to_s]).uniq
end
def desmarcar_editado!(campo)
self.campos_editados = campos_editados - [campo.to_s]
end
end

View File

@@ -0,0 +1,17 @@
# app/models/romaneio_veiculo.rb
#
# Uma FOLHA do romaneio: um veículo, uma página (ou mais, quando passa de
# Romaneio::LINHAS_POR_FOLHA paradas).
#
# ⚠️ `motorista` NÃO é impresso. O rodapé do documento traz "MOTORISTA: ______"
# para preenchimento à mão — é assim que a folha é assinada hoje, e o programa
# Python que originou esta tela também coletava o nome sem usá-lo. O campo existe
# para o operador saber, na tela, de quem é cada carro enquanto revisa.
class RomaneioVeiculo < ApplicationRecord
belongs_to :romaneio, inverse_of: :romaneio_veiculos
validates :veiculo, presence: true
validates :veiculo, uniqueness: { scope: :romaneio_id }
scope :ordenados, -> { order(:posicao, :veiculo) }
end

View File

@@ -129,6 +129,7 @@ class User < ApplicationRecord
['admin.usuarios', :admin_usuarios_path],
['admin.perfis', :admin_perfis_path],
['admin.configuracoes', :admin_configuracoes_path],
['admin.romaneio', :admin_romaneios_path],
['notificacao.contatos', :admin_contatos_path],
['notificacao.eventos', :admin_eventos_path],
['notificacao.envios', :admin_envios_path]