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

@@ -0,0 +1,33 @@
# Romaneio = UM plano do SimpliRoute importado para virar o "CONTROLE DE ENTREGA"
# que o motorista assina ao retirar a carga.
#
# Chave (planned_date, operacao_tabela): o eixo de busca da API é a data, e a fonte
# do enriquecimento (APARELHO/telefones) é exatamente UMA tabela gade_entregas_*.
# Duas operações podem dividir a mesma data com veículos distintos, daí a dupla.
#
# `rotulo_plano` ("EMAD 09.2026") é APRESENTAÇÃO — sai no canto direito do PDF e o
# operador reescreve à vontade. Por isso NÃO entra na chave: renomear o rótulo não
# pode criar um segundo romaneio do mesmo dia.
#
# `operacao_tabela` é NOT NULL com default '' de propósito: no Postgres dois NULLs
# são distintos entre si, e um índice único sobre coluna anulável deixaria passar
# duplicata justamente no caso "romaneio sem operação escolhida".
class CreateRomaneios < ActiveRecord::Migration[7.1]
def change
create_table :romaneios do |t|
t.date :planned_date, null: false
t.string :operacao_tabela, null: false, default: ''
t.string :rotulo_plano, null: false, default: ''
# 'api' (rotas do SimpliRoute) | 'espelho' (db_reem_simplerout_2026) |
# 'planilha' (upload do .xlsx do plano)
t.string :origem, null: false, default: 'api'
t.datetime :importado_em
t.references :importado_por, foreign_key: { to_table: :users }
t.timestamps
end
add_index :romaneios, %i[planned_date operacao_tabela],
unique: true, name: 'idx_romaneios_data_operacao_unico'
end
end

View File

@@ -0,0 +1,26 @@
# Uma FOLHA do romaneio: um veículo, uma página A4 paisagem.
#
# Motorista e ordem de página são atributos da FOLHA, não da linha de entrega.
# Guardá-los em cada linha significaria um UPDATE de 30 linhas a cada tecla e uma
# ambiguidade que o PDF não sabe resolver ("qual dos 30 nomes eu imprimo?").
# É a mesma lição de 20260703000001 (vehicle desnormalizado em consolidacao_entregas)
# → 20260703000002 (create_consolidacao_veiculos), no dia seguinte.
#
# ⚠️ O nome do motorista NÃO é impresso: o rodapé traz a linha "MOTORISTA: ____"
# para preenchimento à mão (é assim que o documento é assinado hoje). O campo existe
# para o operador saber, NA TELA, de quem é cada carro enquanto revisa.
class CreateRomaneioVeiculos < ActiveRecord::Migration[7.1]
def change
create_table :romaneio_veiculos do |t|
t.references :romaneio, null: false, foreign_key: true
t.string :veiculo, null: false
t.string :motorista, null: false, default: ''
t.integer :posicao, null: false, default: 0 # ordem das páginas no PDF
t.timestamps
end
add_index :romaneio_veiculos, %i[romaneio_id veiculo],
unique: true, name: 'idx_romaneio_veiculo_unico'
end
end

View File

@@ -0,0 +1,51 @@
# Uma PARADA do romaneio — vira uma linha da tabela do PDF.
#
# ⚠️ "linhas" e não "itens": "RomaneioItem".tableize dá `romaneio_items`, e o
# inflector pt-BR do projeto (config/initializers/inflections.rb) não conhece
# item/itens — ensiná-lo renomearia todo `item`/`items` do app. `RomaneioLinha`
# pluraliza sozinho, e `linhas` já é a palavra que os serviços usam
# (PlanilhaEntregas#linhas, PlanilhaCarga#linhas). Travado em
# spec/models/table_names_spec.rb, que existe porque ConsolidacaoMotorista já
# quebrou em produção por causa do inflector.
#
# `chave_origem` é a identidade ESTÁVEL entre reimportações: a NF quando existe,
# senão "T:<md5(titulo|endereco)>". Sem esse fallback, duas linhas cujo "Título" não
# casa o padrão "NF n - NOME" colidiriam no índice único e a importação inteira
# estouraria. Veículo e ordem NÃO entram na chave: eles mudam quando o plano é
# reotimizado, e a linha continua sendo a mesma entrega.
#
# `origem` = snapshot do que o PLANO trouxe na última importação (para o
# diff e para o botão "restaurar do plano").
# `campos_editados` = nomes dos campos que o operador sobrescreveu. É ISTO que
# sobrevive à reimportaçã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.
# `removido_em` = a linha saiu do plano mas tinha edição humana. Soft, porque
# apagar levaria junto o trabalho sem deixar rastro na tela.
class CreateRomaneioLinhas < ActiveRecord::Migration[7.1]
def change
create_table :romaneio_linhas do |t|
t.references :romaneio, null: false, foreign_key: true
t.string :veiculo, null: false
t.integer :ordem, null: false, default: 0
t.string :chave_origem, null: false
t.string :nota_fiscal, null: false, default: ''
t.string :nome, null: false, default: ''
t.text :endereco, null: false, default: ''
t.string :aparelho, null: false, default: '' # 'SIM' ou ''
t.string :telefones, null: false, default: ''
t.jsonb :origem, null: false, default: {}
t.jsonb :campos_editados, null: false, default: []
t.datetime :removido_em
t.timestamps
end
add_index :romaneio_linhas, %i[romaneio_id chave_origem],
unique: true, name: 'idx_romaneio_linha_chave_unico'
add_index :romaneio_linhas, %i[romaneio_id veiculo ordem],
name: 'idx_romaneio_linha_ordem'
end
end

View File

@@ -0,0 +1,43 @@
# Leva a chave nova `admin.romaneio` para os perfis de sistema JÁ SALVOS.
#
# Por que existe (mesma armadilha de 20260826000005): 20260826000003 é idempotente,
# então uma permissão acrescentada ao catálogo DEPOIS daquele deploy nunca chega às
# linhas de perfis_acesso. O sintoma é mudo — o item some do menu e a URL responde
# "sem permissão" sem nada explicando o porquê.
#
# Diferença deliberada para 20260826000005: aqui é ADITIVO (`permissoes || '[...]'`)
# em vez de reescrever a lista inteira. Reescrever é aceitável para o Administrador
# ("acesso total" por definição); para o Gerente desfaria qualquer ajuste que o ADM
# tenha feito na tela de Perfis.
#
# Gerente entra porque `admin.romaneio` é a mesma classe de ferramenta operacional
# que `admin.planilha_simpli_route`, que ele já tem. Operador NÃO entra por padrão —
# quem carrega o caminhão recebe a chave pela tela de Perfis, que existe para isso.
class AddPermissaoRomaneioAosPerfis < ActiveRecord::Migration[7.1]
CHAVE = 'admin.romaneio'.freeze
def up
return unless table_exists?(:perfis_acesso)
execute(<<~SQL.squish)
UPDATE perfis_acesso
SET permissoes = permissoes || #{quote([CHAVE].to_json)}::jsonb,
updated_at = NOW()
WHERE sistema = true
AND LOWER(nome) IN ('administrador', 'gerente')
AND NOT (permissoes ? #{quote(CHAVE)})
SQL
end
# Reversível: tirar a chave devolve os perfis ao estado anterior sem tocar em
# mais nada (o `-` de jsonb remove o elemento do array, se estiver lá).
def down
return unless table_exists?(:perfis_acesso)
execute(<<~SQL.squish)
UPDATE perfis_acesso
SET permissoes = permissoes - #{quote(CHAVE)}, updated_at = NOW()
WHERE permissoes ? #{quote(CHAVE)}
SQL
end
end