176 lines
6.8 KiB
Ruby
176 lines
6.8 KiB
Ruby
# app/models/entrega.rb
|
|
#
|
|
# Model de LEITURA para a tabela existente da Reem Transporte.
|
|
# NUNCA criar migration para esta tabela.
|
|
# NUNCA executar INSERT, UPDATE, DELETE ou DROP nesta tabela.
|
|
#
|
|
class Entrega < ApplicationRecord
|
|
self.table_name = 'db_reem_simplerout_2026'
|
|
self.primary_key = 'tracking_id'
|
|
|
|
# Apenas leitura — segurança contra mutações acidentais
|
|
def readonly?
|
|
true
|
|
end
|
|
|
|
# Status que o SimpleRoute grava para entregas SEM SUCESSO (falha/insucesso).
|
|
# AJUSTE aqui se o valor real for diferente (ex.: 'cancelled', 'not_delivered').
|
|
STATUS_FALHA = %w[failed].freeze
|
|
|
|
# ── Scopes ──────────────────────────────────────────────────
|
|
scope :concluidas, -> { where(status: 'completed') }
|
|
scope :com_checkout, -> { where.not(checkout: nil) }
|
|
# Pagamento = entrega concluída com checkout registrado
|
|
scope :pagas, -> { concluidas.com_checkout }
|
|
# Falhadas = entregas que não tiveram sucesso (status de falha)
|
|
scope :falhadas, -> { where(status: STATUS_FALHA) }
|
|
# Pendentes = nem concluídas nem falhadas (ainda em aberto). Exclui as falhadas
|
|
# para que entregues/pendentes/falhadas não se sobreponham no dashboard.
|
|
scope :pendentes, -> { where.not(status: ['completed'] + STATUS_FALHA) }
|
|
|
|
# planned_date é timestamp: usamos o dia inteiro do limite superior para não
|
|
# descartar as entregas do último dia do período (causava dashboard zerado e
|
|
# subcontagem de elegíveis nas consolidações).
|
|
scope :no_periodo, ->(inicio, fim) {
|
|
where(planned_date: inicio.to_date.beginning_of_day..fim.to_date.end_of_day)
|
|
}
|
|
|
|
# Período pela DATA REAL da entrega (checkout) — usada nas análises financeiras
|
|
# ("quando a entrega foi concluída"). Comparação naïve com Date (intervalo
|
|
# meio-aberto: >= início e < fim+1), SEM conversão de fuso — bate com a análise
|
|
# feita direto no banco. Só faz sentido sobre entregas .pagas (checkout != nil).
|
|
scope :no_periodo_checkout, ->(inicio, fim) {
|
|
where('checkout >= ? AND checkout < ?', inicio.to_date, fim.to_date + 1)
|
|
}
|
|
|
|
scope :do_motorista, ->(nome) {
|
|
where(driver: nome)
|
|
}
|
|
|
|
# Busca pela NF (reference_id). A coluna é numérica, então comparamos como
|
|
# texto para aceitar a entrada do usuário de forma robusta. Usado no
|
|
# apontamento manual de entregas (NF que chegou fora do período).
|
|
scope :por_nf, ->(nf) {
|
|
where('reference_id::text = ?', nf.to_s.strip)
|
|
}
|
|
|
|
scope :da_veiculo, ->(vehicle) {
|
|
where(vehicle: vehicle)
|
|
}
|
|
|
|
# Restringe às entregas cujas NFs (reference_id) constam na(s) tabela(s) de
|
|
# operação informada(s). O JOIN é o mesmo do script de gestão do cliente:
|
|
# reference_id::text = <tabela_operacao>.nota_fiscal
|
|
#
|
|
# SEGURANÇA: o nome da tabela entra no SQL, então passa pela whitelist
|
|
# (Operacao.sanitizar) e por quote_table_name. Nomes inválidos são descartados;
|
|
# se nada sobrar, não filtra (all) — o filtro de operação é opcional.
|
|
scope :da_operacoes, ->(tabelas) {
|
|
validas = Operacao.sanitizar(tabelas)
|
|
next all if validas.empty?
|
|
|
|
selects = validas.map { |t| "SELECT nota_fiscal FROM #{connection.quote_table_name(t)}" }
|
|
where("reference_id::text IN (#{selects.join(' UNION ')})")
|
|
}
|
|
|
|
scope :do_mes, ->(data = Date.current) {
|
|
where(planned_date: data.beginning_of_month..data.end_of_month)
|
|
}
|
|
|
|
# Filtra pela(s) conta(s) configurada(s) em DB_EXISTING_ACCOUNT_ID.
|
|
# A coluna account_id é TEXTO e pode vir vazia/NULL, então comparamos como string.
|
|
# IMPORTANTE: o valor é uma LISTA separada por vírgula — NÃO um trecho de SQL.
|
|
# Aceita: "95907" (uma) | "95907,12345" (várias) | "all" ou vazio (sem filtro).
|
|
# Use "95907," (vírgula no fim) para incluir também registros de conta vazia/NULL.
|
|
scope :da_conta_gade, -> {
|
|
contas = ENV.fetch('DB_EXISTING_ACCOUNT_ID', '95907').to_s.strip
|
|
if contas.empty? || contas.casecmp?('all')
|
|
all
|
|
else
|
|
valores = contas.split(',', -1).map(&:strip)
|
|
# Token vazio (ex.: "95907,") → inclui também as linhas sem conta (NULL).
|
|
valores << nil if valores.any?(&:empty?)
|
|
where(account_id: valores)
|
|
end
|
|
}
|
|
|
|
# ── Métodos de classe ────────────────────────────────────────
|
|
|
|
# Lista motoristas únicos (para selects, consolidações)
|
|
def self.motoristas_ativos(inicio: nil, fim: nil)
|
|
base = da_conta_gade
|
|
base = base.no_periodo(inicio, fim) if inicio && fim
|
|
base.distinct.order(:driver).pluck(:driver).compact.reject(&:empty?)
|
|
end
|
|
|
|
# Lista veículos únicos (para selects/filtros de consolidação)
|
|
def self.veiculos_unicos(inicio: nil, fim: nil)
|
|
base = da_conta_gade
|
|
base = base.no_periodo(inicio, fim) if inicio && fim
|
|
base.distinct.order(:vehicle).pluck(:vehicle).compact.reject(&:empty?)
|
|
end
|
|
|
|
# Veículos usados pelo(s) motorista(s) selecionado(s) no período — alimenta o
|
|
# filtro dinâmico de veículos na criação de consolidação.
|
|
def self.veiculos_de_motoristas(motoristas, inicio: nil, fim: nil)
|
|
nomes = Array(motoristas).reject(&:blank?)
|
|
return [] if nomes.empty?
|
|
|
|
base = da_conta_gade.where(driver: nomes)
|
|
base = base.no_periodo(inicio, fim) if inicio.present? && fim.present?
|
|
base.distinct.order(:vehicle).pluck(:vehicle).compact.reject(&:empty?)
|
|
end
|
|
|
|
# Contagem de entregas pagas para cálculo estimado.
|
|
# Período pela DATA REAL da entrega (checkout) — ver no_periodo_checkout.
|
|
def self.contar_pagas(inicio:, fim:, motorista: nil, vehicle: nil, operacoes: nil)
|
|
base = pagas.da_conta_gade.no_periodo_checkout(inicio, fim)
|
|
base = base.do_motorista(motorista) if motorista.present?
|
|
base = base.da_veiculo(vehicle) if vehicle.present?
|
|
base = base.da_operacoes(operacoes) if operacoes.present?
|
|
base.count
|
|
end
|
|
|
|
# ── Helpers de instância ─────────────────────────────────────
|
|
|
|
def numero_nf
|
|
reference_id
|
|
end
|
|
|
|
# Resumo para o apontamento manual (modal de busca de NF). Fonte única usada
|
|
# pelos dois fluxos: apontar em consolidação existente e criar "nota avulsa".
|
|
def resumo_apontamento
|
|
{
|
|
tracking_id: tracking_id,
|
|
nf: numero_nf,
|
|
motorista: driver,
|
|
veiculo: vehicle,
|
|
local: local,
|
|
status: status,
|
|
pago: elegivel_pagamento?,
|
|
data: (checkout || planned_date)&.to_s
|
|
}
|
|
end
|
|
|
|
def local
|
|
contact_name.presence || address
|
|
end
|
|
|
|
def concluida?
|
|
status == 'completed'
|
|
end
|
|
|
|
def checkout_registrado?
|
|
checkout.present?
|
|
end
|
|
|
|
def elegivel_pagamento?
|
|
concluida? && checkout_registrado?
|
|
end
|
|
|
|
def atraso_minutos
|
|
return 0 unless delay.present?
|
|
delay.to_s.split(':').then { |h, m, _s| h.to_i * 60 + m.to_i }
|
|
end
|
|
end
|