Grande correção do filtro de veiculos na area de consolidação

This commit is contained in:
2026-07-03 09:58:59 -03:00
parent 83310e9e66
commit eca6df39eb
17 changed files with 681 additions and 41 deletions

View File

@@ -8,6 +8,11 @@ class Consolidacao < ApplicationRecord
has_many :consolidacao_motoristas, dependent: :destroy
has_many :consolidacao_entregas, dependent: :destroy
has_many :consolidacao_veiculos, dependent: :destroy
# Bucket dos lançamentos sem veículo (termo/apontamento manual legado). Mesmo
# sentinel do controller e de ConsolidacaoVeiculo.
SEM_VEICULO = ConsolidacaoVeiculo::SEM_VEICULO
# ── Enums ───────────────────────────────────────────────────
enum status: { rascunho: 0, finalizada: 1, arquivada: 2 }
@@ -111,21 +116,34 @@ class Consolidacao < ApplicationRecord
def finalizar!(user)
return false unless todas_entregas_classificadas?
update!(
status: :finalizada,
finalizado_por: user.id,
finalizado_em: Time.current
)
transaction do
# Finalizar a consolidação fecha (consolida) os veículos que ainda estavam
# abertos — mantém o estado por veículo coerente com o todo finalizado.
consolidacao_veiculos.ativos.abertos.update_all(
fechado_em: Time.current, fechado_por: user.id, updated_at: Time.current
)
update!(
status: :finalizada,
finalizado_por: user.id,
finalizado_em: Time.current
)
end
end
# Só está completa quando CADA motorista tem todas as suas entregas elegíveis
# classificadas. (Antes era uma soma agregada com >=, que mascarava motoristas
# pendentes quando outro tinha entregas a mais.)
# Só está completa quando CADA motorista ativo está completo (nada por
# classificar, descontados os veículos arquivados). Fechar veículo é um passo
# independente; ao finalizar, os veículos abertos são consolidados juntos.
def todas_entregas_classificadas?
consolidacao_motoristas.ativos.all? do |cm|
total = entregas_elegiveis_count(cm.motorista_nome)
total.zero? || classificadas_count(cm.motorista_nome) >= total
end
consolidacao_motoristas.ativos.all? { |cm| motorista_completo?(cm.motorista_nome) }
end
# Completude de UM motorista: não sobra entrega elegível por classificar. Os
# veículos ARQUIVADOS saem da conta (suas entregas deixam de ser exigidas),
# permitindo finalizar deixando um carro problemático de fora. Fechar um veículo
# é um passo independente (lock) — a finalização em si fecha os que restarem.
def motorista_completo?(motorista)
total = entregas_elegiveis_count(motorista) - eligible_veiculos_arquivados(motorista)
total.zero? || classificadas_count(motorista) >= total
end
def percentual_classificado
@@ -153,6 +171,28 @@ class Consolidacao < ApplicationRecord
)
end
# Elegíveis de UM veículo específico do motorista (respeita filtros da
# consolidação). O bucket "(sem veículo)" casa as entregas com vehicle NULL/vazio.
def entregas_elegiveis_count_veiculo(motorista, vehicle)
rel = Entrega.atendidas.da_conta_gade
.no_periodo_checkout(data_inicio, data_fim)
.do_motorista(motorista)
rel = rel.da_veiculo(vehicle_ids) if vehicle_ids.present?
rel = rel.da_operacoes(operacoes) if operacoes.present?
rel = vehicle == SEM_VEICULO ? rel.where(vehicle: [nil, '']) : rel.where(vehicle: vehicle)
rel.count
end
# Total de elegíveis dos veículos ARQUIVADOS do motorista — descontado do gate
# para que arquivar um carro problemático libere a finalização. Só toca a base
# externa quando existe algum veículo arquivado (mantém os testes isolados).
def eligible_veiculos_arquivados(motorista)
arquivados = consolidacao_veiculos.arquivados.where(motorista_nome: motorista)
return 0 if arquivados.empty?
arquivados.sum { |cv| entregas_elegiveis_count_veiculo(motorista, cv.vehicle) }
end
# Rótulos amigáveis das operações vinculadas (para exibição).
def operacoes_label
Array(operacoes).map { |t| Operacao.label(t) }
@@ -190,6 +230,7 @@ class Consolidacao < ApplicationRecord
motorista_nome: motorista,
valor_aplicado: ConsolidacaoEntrega.valor_para(tipo),
manual: true,
vehicle: entrega.try(:vehicle).presence, # snapshot do veículo da rastreio
created_by: user&.id
)
ce.save!
@@ -208,7 +249,7 @@ class Consolidacao < ApplicationRecord
# O motorista vem digitado (não de uma Entrega) e é incluído se ainda não
# estiver. tracking_id é sintético (não há Entrega), e a NF/observação ficam
# guardadas em nf_manual/obs_manual.
def adicionar_apontamento_manual!(motorista:, tipos:, user:, nf: nil, obs: nil, valor_extra: nil)
def adicionar_apontamento_manual!(motorista:, tipos:, user:, nf: nil, obs: nil, valor_extra: nil, vehicle: nil)
tipos_validos = Array(tipos).map(&:to_s).select { |t| ConsolidacaoEntrega.tipos.key?(t) }.uniq
raise ArgumentError, 'Selecione ao menos uma classificação.' if tipos_validos.empty?
@@ -232,6 +273,7 @@ class Consolidacao < ApplicationRecord
motorista_nome: nome,
valor_aplicado: valor,
manual: true,
vehicle: vehicle.presence, # veículo escolhido no lançamento (Q3)
nf_manual: nf.presence,
obs_manual: obs.presence,
created_by: user&.id
@@ -249,7 +291,7 @@ class Consolidacao < ApplicationRecord
# para atrelar (só motorista + quantidade, a um preço fixo configurável). Fica
# numa única linha (manual: true): quantidade guarda o N e valor_aplicado guarda
# o total do lote, então recalcular_motorista! e demais somas seguem inalteradas.
def adicionar_termos!(motorista:, quantidade:, user:)
def adicionar_termos!(motorista:, quantidade:, user:, vehicle: nil)
nome = motorista.to_s.strip
raise ArgumentError, 'Informe o motorista do termo.' if nome.blank?
@@ -266,6 +308,7 @@ class Consolidacao < ApplicationRecord
quantidade: qtd,
valor_aplicado: qtd * ConsolidacaoEntrega.valor_para('termo'), # total do lote
manual: true,
vehicle: vehicle.presence, # veículo escolhido no lançamento (Q3)
created_by: user&.id
)
@@ -286,6 +329,27 @@ class Consolidacao < ApplicationRecord
end
cm.update_column(:valor_total, total)
update_column(:valor_total, consolidacao_motoristas.ativos.sum(:valor_total))
sincronizar_veiculos!(nome)
end
# Mantém as linhas de consolidacao_veiculos do motorista em dia a partir dos
# pilares já classificados (100% DB — não toca a base externa). Cria/atualiza uma
# linha por veículo presente nas classificações e remove as que ficaram sem
# pilares e ainda não foram fechadas/arquivadas. Veículos elegíveis mas 100% sem
# classificar não geram linha aqui — o gate já os pega por classificadas < total.
def sincronizar_veiculos!(nome)
entregas = consolidacao_entregas.where(motorista_nome: nome)
presentes = entregas.pluck(:vehicle).map { |v| v.presence || SEM_VEICULO }.uniq
presentes.each do |veic|
cv = consolidacao_veiculos.find_or_initialize_by(motorista_nome: nome, vehicle: veic)
cv.save! # before_save :recalcular_valor atualiza o valor_total do veículo
end
consolidacao_veiculos.where(motorista_nome: nome)
.where(fechado_em: nil, arquivado_em: nil)
.where.not(vehicle: presentes)
.destroy_all
end
private

View File

@@ -0,0 +1,71 @@
# app/models/consolidacao_veiculo.rb
# Unidade de FECHAMENTO por veículo dentro de uma consolidação. Espelha os padrões
# de ConsolidacaoMotorista (arquivamento reversível + valor_total recalculado dos
# pilares), mas o que ela controla é o estado "fechado/consolidado" de cada carro.
# O PAGAMENTO continua no motorista (ConsolidacaoMotorista) — ver [[consolidacao]].
class ConsolidacaoVeiculo < ApplicationRecord
# Nome da tabela fixado (mesmo motivo dos outros: inflector pt-BR).
self.table_name = 'consolidacao_veiculos'
# Bucket dos lançamentos sem veículo (termo/apontamento manual legado). Igual ao
# sentinel usado no controller e na migração — NULL quebraria o índice único.
SEM_VEICULO = '(sem veículo)'
belongs_to :consolidacao
belongs_to :fechador, class_name: 'User', foreign_key: :fechado_por, optional: true
belongs_to :arquivador, class_name: 'User', foreign_key: :arquivado_por, optional: true
validates :motorista_nome, presence: true
validates :vehicle, presence: true
validates :vehicle, uniqueness: { scope: %i[consolidacao_id motorista_nome] }
before_save :recalcular_valor
# ── Fechamento (consolidação do veículo, reversível) ────────
scope :fechados, -> { where.not(fechado_em: nil) }
scope :abertos, -> { where(fechado_em: nil) }
# ── Arquivamento (reversível) — sai do gate, mantém os dados ─
scope :ativos, -> { where(arquivado_em: nil) }
scope :arquivados, -> { where.not(arquivado_em: nil) }
def fechado?
fechado_em.present?
end
def arquivado?
arquivado_em.present?
end
# update_columns: não dispara before_save (o valor já está correto no fechamento).
def fechar!(user)
update_columns(fechado_em: Time.current, fechado_por: user&.id, updated_at: Time.current)
end
def reabrir!(_user)
update_columns(fechado_em: nil, fechado_por: nil, updated_at: Time.current)
end
def arquivar!(user)
update_columns(arquivado_em: Time.current, arquivado_por: user&.id, updated_at: Time.current)
end
def desarquivar!(_user)
update_columns(arquivado_em: nil, arquivado_por: nil, updated_at: Time.current)
end
# Soma dos pilares deste (motorista + veículo). Mesma regra do ConsolidacaoMotorista:
# o valor_aplicado capturado na classificação é a fonte da verdade e o desconto
# subtrai. O bucket "(sem veículo)" casa os pilares com vehicle NULL/vazio.
def recalcular_valor
self.valor_total = escopo_entregas
.sum { |e| e.desconto? ? -e.valor_aplicado : e.valor_aplicado }
end
private
def escopo_entregas
rel = consolidacao.consolidacao_entregas.where(motorista_nome: motorista_nome)
vehicle == SEM_VEICULO ? rel.where(vehicle: [nil, '']) : rel.where(vehicle: vehicle)
end
end