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

@@ -101,3 +101,36 @@ FactoryBot.define do
entregas_contadas { 10 }
end
end
FactoryBot.define do
factory :romaneio do
planned_date { Date.new(2026, 8, 31) }
operacao_tabela { '' }
rotulo_plano { 'EMAD 09.2026' }
origem { 'planilha' }
end
factory :romaneio_linha do
romaneio
veiculo { 'GADE_001' }
sequence(:ordem)
sequence(:nota_fiscal) { |n| (90_000 + n).to_s }
chave_origem { nota_fiscal }
nome { 'FULANO DE TAL' }
endereco { 'RUA X, 1, BAIRRO, SÃO PAULO, SP' }
aparelho { '' }
telefones { '(11) 90000-0000' }
origem do
{ 'nota_fiscal' => nota_fiscal, 'nome' => nome, 'endereco' => endereco,
'aparelho' => aparelho, 'telefones' => telefones }
end
campos_editados { [] }
end
factory :romaneio_veiculo do
romaneio
veiculo { 'GADE_001' }
motorista { '' }
posicao { 0 }
end
end

View File

@@ -9,7 +9,11 @@ RSpec.describe 'Nomes de tabela dos models' do
ConsolidacaoMotorista => 'consolidacao_motoristas',
ConsolidacaoEntrega => 'consolidacao_entregas',
Configuracao => 'configuracoes',
ConfiguracaoNotificacao => 'configuracao_notificacoes'
ConfiguracaoNotificacao => 'configuracao_notificacoes',
RomaneioVeiculo => 'romaneio_veiculos',
# "RomaneioItem" daria `romaneio_items` (o inflector pt-BR não conhece
# item/itens); por isso a tabela é `romaneio_linhas`.
RomaneioLinha => 'romaneio_linhas'
}.each do |model, tabela|
it "#{model} aponta para #{tabela}" do
expect(model.table_name).to eq(tabela)

View File

@@ -0,0 +1,21 @@
require 'rails_helper'
RSpec.describe RomaneioPolicy do
subject(:policy) { described_class.new(usuario, Romaneio) }
context 'com a permissão admin.romaneio' do
let(:usuario) { build(:gerente) }
it { is_expected.to be_index }
it { is_expected.to be_pdf }
it { is_expected.to be_atualizar_linha }
end
context 'sem a permissão' do
let(:usuario) { build(:operador) }
it { is_expected.not_to be_index }
it { is_expected.not_to be_pdf }
it { is_expected.not_to be_destroy }
end
end

View File

@@ -0,0 +1,82 @@
require 'rails_helper'
RSpec.describe 'Admin::Romaneios', type: :request do
let(:romaneio) { create(:romaneio) }
describe 'permissão' do
it 'exige login' do
get admin_romaneios_path
expect(response).to redirect_to(new_user_session_path)
end
it 'bloqueia operador (não tem admin.romaneio por padrão)' do
sign_in create(:operador)
get admin_romaneios_path
expect(response).to have_http_status(:redirect)
expect(flash[:alert]).to match(/permissão/i)
end
it 'libera gerente' do
sign_in create(:gerente)
get admin_romaneios_path
expect(response).to have_http_status(:ok)
end
end
describe 'GET #pdf' do
before do
create(:romaneio_linha, romaneio: romaneio, veiculo: 'GADE_001')
sign_in create(:admin)
end
it 'sai INLINE quando um veículo é pedido — é a prévia do editor' do
get pdf_admin_romaneio_path(romaneio, veiculo: 'GADE_001')
expect(response.media_type).to eq('application/pdf')
expect(response.headers['Content-Disposition']).to match(/inline/)
end
it 'sai como download quando nenhum veículo é pedido' do
get pdf_admin_romaneio_path(romaneio)
expect(response.headers['Content-Disposition']).to match(/attachment/)
end
end
describe 'PATCH #atualizar_linha' do
let(:linha) { create(:romaneio_linha, romaneio: romaneio, telefones: '(11) 1111-1111') }
before { sign_in create(:admin) }
it 'grava o campo e o marca como editado' do
patch atualizar_linha_admin_romaneio_path(romaneio),
params: { linha_id: linha.id, campo: 'telefones', valor: '(11) 99999-0000' },
as: :json
expect(response.parsed_body['valor']).to eq('(11) 99999-0000')
expect(response.parsed_body['editado']).to be(true)
expect(linha.reload.campos_editados).to include('telefones')
end
it 'com valor null restaura o que o plano trouxe e destrava o campo' do
patch atualizar_linha_admin_romaneio_path(romaneio),
params: { linha_id: linha.id, campo: 'telefones', valor: '(11) 99999-0000' }, as: :json
patch atualizar_linha_admin_romaneio_path(romaneio),
params: { linha_id: linha.id, campo: 'telefones', valor: nil }, as: :json
expect(linha.reload.telefones).to eq('(11) 1111-1111')
expect(linha.campos_editados).to be_empty
end
it 'recusa campo fora da whitelist — veiculo e ordem são a verdade do roteiro' do
patch atualizar_linha_admin_romaneio_path(romaneio),
params: { linha_id: linha.id, campo: 'veiculo', valor: 'GADE_999' }, as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(linha.reload.veiculo).not_to eq('GADE_999')
end
end
end

View File

@@ -0,0 +1,107 @@
require 'rails_helper'
# A gem entra com `require: false` no Gemfile (é ferramenta de teste, não de runtime).
require 'pdf/inspector'
# As constantes derivadas SÃO o contrato do documento: elas foram conferidas pixel a
# pixel contra Imagens para implantação/Romaneiro_EMAD_SETEMBRO_2026.pdf (render a
# 110 dpi). Se alguém mexer numa margem "só um pouquinho", é aqui que aparece.
RSpec.describe Pdf::RomaneioPdf do
let(:romaneio) { create(:romaneio) }
def semear(veiculo, quantidade)
quantidade.times do |i|
create(:romaneio_linha, romaneio: romaneio, veiculo: veiculo, ordem: i + 1)
end
end
def paginas(pdf)
PDF::Inspector::Page.analyze(pdf).pages.size
end
describe 'geometria' do
subject(:documento) { described_class.new(romaneio: romaneio) }
it 'é A4 paisagem' do
medidas = documento.instance_variable_get(:@pdf).bounds
expect([medidas.width.round(2), medidas.height.round(2)]).to eq([785.2, 487.56])
end
it 'mantém a altura de linha e os corpos de fonte do documento original' do
expect(documento.instance_variable_get(:@linha_h)).to be_within(0.001).of(11.1811)
expect(documento.instance_variable_get(:@fonte_dados)).to eq(7.0)
expect(documento.instance_variable_get(:@fonte_hdr)).to eq(7.5)
end
it 'usa a largura útil inteira nas seis colunas' do
larguras = documento.instance_variable_get(:@larguras)
expect(larguras.size).to eq(6)
expect(larguras.sum).to be_within(0.01).of(785.2)
end
it 'reserva espaço para o rodapé caber na folha cheia' do
# 33 paradas + as 2 linhas em branco estouravam o orçamento e o
# "ASSINATURA DO MOTORISTA" sumia do papel, sem erro nenhum.
expect(documento.instance_variable_get(:@max_linhas_folha))
.to be >= described_class::MAX_LINHAS
end
end
it 'gera uma página por veículo' do
semear('GADE_001', 5)
semear('GADE_002', 3)
expect(paginas(described_class.new(romaneio: romaneio).render)).to eq(2)
end
it 'imprime só o veículo pedido (é o que a prévia do editor usa)' do
semear('GADE_001', 5)
semear('GADE_002', 3)
expect(paginas(described_class.new(romaneio: romaneio, veiculos: ['GADE_002']).render)).to eq(1)
end
it 'quebra em folhas de continuação acima de MAX_LINHAS' do
semear('GADE_008', described_class::MAX_LINHAS + 1)
pdf = described_class.new(romaneio: romaneio).render
expect(paginas(pdf)).to eq(2)
texto = PDF::Inspector::Text.analyze(pdf).strings.join(' ')
expect(texto).to include('FOLHA 1/2').and include('FOLHA 2/2')
end
it 'assina TODAS as folhas — inclusive a de continuação' do
semear('GADE_008', described_class::MAX_LINHAS + 1)
texto = PDF::Inspector::Text.analyze(described_class.new(romaneio: romaneio).render).strings
expect(texto.count { |t| t.include?('ASSINATURA DO MOTORISTA') }).to eq(2)
end
it 'numera as paradas continuando entre folhas (bate com a ordem no celular)' do
semear('GADE_008', described_class::MAX_LINHAS + 1)
texto = PDF::Inspector::Text.analyze(described_class.new(romaneio: romaneio).render).strings
expect(texto).to include((described_class::MAX_LINHAS + 1).to_s)
end
it 'não imprime a parada que saiu do plano' do
semear('GADE_001', 3)
romaneio.romaneio_linhas.first.update!(removido_em: Time.current, nome: 'NAO DEVE SAIR')
texto = PDF::Inspector::Text.analyze(described_class.new(romaneio: romaneio).render).strings.join(' ')
expect(texto).not_to include('NAO DEVE SAIR')
end
it 'escreve o rótulo do plano no cabeçalho' do
semear('GADE_001', 2)
texto = PDF::Inspector::Text.analyze(described_class.new(romaneio: romaneio).render).strings.join(' ')
expect(texto).to include('PLANO:').and include('EMAD 09.2026')
end
it 'aguenta acento nos endereços (a fonte AFM do Prawn estouraria aqui)' do
create(:romaneio_linha, romaneio: romaneio, veiculo: 'GADE_001',
endereco: 'RUA INÁCIO DE OLIVEIRA CAMPOS, 182, SÃO PAULO')
expect { described_class.new(romaneio: romaneio).render }.not_to raise_error
end
end

View File

@@ -0,0 +1,47 @@
require 'rails_helper'
# É o que dispensa a SEGUNDA PLANILHA do programa Python: o STATUS (NOVO/RECORRENTE)
# já está na tabela da operação.
RSpec.describe Romaneios::EnriquecimentoGade do
include_context 'tabela de operação'
let(:tabela) { "#{Operacao::PREFIXO}teste_jan_2026" }
it 'marca APARELHO = SIM só para quem está como NOVO' do
criar_operacao!(tabela)
semear_operacao!(tabela, [
{ nota_fiscal: '1', nome_completo: 'Novo Paciente', telefones: '(11) 1',
status: 'NOVO' },
{ nota_fiscal: '2', nome_completo: 'Velho Paciente', telefones: '(11) 2',
status: 'RECORRENTE' }
])
dados = described_class.new(tabela).para_notas(%w[1 2])
expect(dados['1'][:aparelho]).to eq('SIM')
expect(dados['2'][:aparelho]).to eq('')
expect(dados['1'][:nome]).to eq('NOVO PACIENTE')
expect(dados['2'][:telefones]).to eq('(11) 2')
end
it 'não quebra quando a operação não tem a coluna status — e AVISA qual falta' do
criar_operacao!(tabela, colunas: %i[nome_completo telefones])
semear_operacao!(tabela, [{ nota_fiscal: '1', nome_completo: 'Fulano', telefones: '(11) 1' }])
servico = described_class.new(tabela)
expect(servico.colunas_ausentes).to eq(['status'])
expect(servico.para_notas(['1'])['1'][:aparelho]).to eq('')
end
it 'omite a NF que não está na operação (é o plano avulso / de inclusão)' do
criar_operacao!(tabela)
semear_operacao!(tabela, [{ nota_fiscal: '1', status: 'NOVO' }])
expect(described_class.new(tabela).para_notas(%w[1 999])).not_to have_key('999')
end
it 'recusa nome de tabela fora da whitelist de operações' do
expect { described_class.new('users; DROP TABLE users') }.to raise_error(ArgumentError)
end
end

View File

@@ -0,0 +1,115 @@
require 'rails_helper'
# As regras de reconciliação — é onde mora o risco da funcionalidade: errar aqui
# significa ou perder a correção que o operador fez à mão, ou imprimir um endereço
# velho que o SimpliRoute já corrigiu.
RSpec.describe Romaneios::Importador do
let(:romaneio) { create(:romaneio) }
def linha_do_plano(nf:, nome: 'FULANO', endereco: 'RUA A, 1', telefones: '(11) 1111-1111',
veiculo: 'GADE_001', ordem: 1)
{ veiculo: veiculo, ordem: ordem, motorista: '', nota_fiscal: nf, nome: nome,
endereco: endereco, telefones: telefones, aparelho: '', tracking_id: '',
titulo: "NF #{nf} - #{nome}" }
end
def importar(linhas)
described_class.new(romaneio: romaneio, linhas: linhas, origem: 'planilha').aplicar!
end
it 'insere as paradas novas e cria a folha de cada veículo' do
resultado = importar([linha_do_plano(nf: '1'),
linha_do_plano(nf: '2', veiculo: 'GADE_002', ordem: 1)])
expect(resultado.novos).to eq(2)
expect(romaneio.romaneio_linhas.count).to eq(2)
expect(romaneio.romaneio_veiculos.pluck(:veiculo)).to contain_exactly('GADE_001', 'GADE_002')
end
it 'PRESERVA o campo que o operador editou e atualiza os demais' do
importar([linha_do_plano(nf: '1', nome: 'FULANO', endereco: 'RUA A, 1')])
linha = romaneio.romaneio_linhas.first
linha.telefones = '(11) 99999-0000'
linha.marcar_editado!('telefones')
linha.save!
resultado = importar([linha_do_plano(nf: '1', nome: 'FULANO DE TAL',
endereco: 'RUA B, 2', telefones: '(11) 1111-1111')])
linha.reload
expect(linha.telefones).to eq('(11) 99999-0000') # intocado: tem edição humana
expect(linha.endereco).to eq('RUA B, 2') # o plano manda no resto
expect(resultado.preservados).to eq(1)
expect(resultado.atualizados).to eq(1)
end
it 'faz veículo e ordem seguirem o plano mesmo numa linha editada' do
importar([linha_do_plano(nf: '1', veiculo: 'GADE_001', ordem: 1)])
linha = romaneio.romaneio_linhas.first
linha.marcar_editado!('nome')
linha.save!
importar([linha_do_plano(nf: '1', veiculo: 'GADE_009', ordem: 7)])
expect(linha.reload.veiculo).to eq('GADE_009')
expect(linha.ordem).to eq(7)
end
it 'APAGA a parada que saiu do plano sem edição humana' do
importar([linha_do_plano(nf: '1'), linha_do_plano(nf: '2', ordem: 2)])
resultado = importar([linha_do_plano(nf: '1')])
expect(romaneio.romaneio_linhas.pluck(:nota_fiscal)).to eq(['1'])
expect(resultado.saidos).to eq(1)
end
it 'GUARDA (sem apagar) a parada que saiu do plano mas tinha edição' do
importar([linha_do_plano(nf: '1'), linha_do_plano(nf: '2', ordem: 2)])
editada = romaneio.romaneio_linhas.find_by(nota_fiscal: '2')
editada.marcar_editado!('endereco')
editada.save!
importar([linha_do_plano(nf: '1')])
expect(editada.reload.removido_em).to be_present
expect(romaneio.linhas_vigentes.pluck(:nota_fiscal)).to eq(['1'])
end
it 'devolve a parada guardada para a folha quando ela volta ao plano' do
importar([linha_do_plano(nf: '1'), linha_do_plano(nf: '2', ordem: 2)])
editada = romaneio.romaneio_linhas.find_by(nota_fiscal: '2')
editada.marcar_editado!('endereco')
editada.save!
importar([linha_do_plano(nf: '1')])
importar([linha_do_plano(nf: '1'), linha_do_plano(nf: '2', ordem: 2)])
expect(editada.reload.removido_em).to be_nil
end
it 'descarta a NF repetida no plano e reporta, em vez de duplicar a parada' do
resultado = importar([linha_do_plano(nf: '1', nome: 'PRIMEIRO'),
linha_do_plano(nf: '1', nome: 'SEGUNDO', ordem: 2)])
expect(romaneio.romaneio_linhas.count).to eq(1)
expect(romaneio.romaneio_linhas.first.nome).to eq('PRIMEIRO')
expect(resultado.duplicados).to eq(1)
end
it 'não colide duas paradas sem NF (a chave cai para o digest do título)' do
sem_nf = linha_do_plano(nf: '').merge(titulo: 'PONTO DE APOIO')
outra = linha_do_plano(nf: '', endereco: 'RUA Z, 9', ordem: 2).merge(titulo: 'OUTRO PONTO')
expect { importar([sem_nf, outra]) }.not_to raise_error
expect(romaneio.romaneio_linhas.count).to eq(2)
end
it 'apaga a folha do veículo que sumiu do plano' do
importar([linha_do_plano(nf: '1'), linha_do_plano(nf: '2', veiculo: 'GADE_002')])
importar([linha_do_plano(nf: '1')])
expect(romaneio.romaneio_veiculos.pluck(:veiculo)).to eq(['GADE_001'])
end
end

View File

@@ -0,0 +1,102 @@
require 'rails_helper'
require 'caxlsx'
# A planilha do plano é o caminho B do romaneio — e é o que o cliente usa quando o
# plano do dia ainda não está publicado. Os casos aqui são os que o export REAL
# apresenta (medido em Imagens para implantação/SimpliRoute_Plan_ 2026-08-31.xlsx):
# coluna "Motorista" vazia, sem "Aparelho", e a NF numa coluna com typo do próprio
# SimpliRoute ("ID de referêncua").
RSpec.describe Romaneios::PlanoPlanilha do
def planilha(cabecalho, linhas)
pacote = Axlsx::Package.new
pacote.workbook.add_worksheet(name: 'Plan') do |aba|
aba.add_row(cabecalho)
linhas.each { |l| aba.add_row(l) }
end
arquivo = Tempfile.new(['plano', '.xlsx'])
arquivo.binmode
arquivo.write(pacote.to_stream.read)
arquivo.flush
arquivo
end
it 'lê o formato real do export (sem Motorista, sem Aparelho, NF com o typo)' do
arquivo = planilha(
['Motorista', 'Veículo', 'Índice de rota', 'Ordem de entrega', 'Título', 'Endereço',
'ID de referêncua', 'Anotações'],
[[nil, 'GADE_038', 1, 1, 'NF 90507 - Cicera Beserra dos Anjos',
'Rua Amapola, 206, Jardim Vista Linda, São Paulo, SP, CEP: 05159-100',
'90507', 'TEL: (11) 99262-0594']]
)
linha = described_class.new(arquivo).linhas.first
expect(linha[:veiculo]).to eq('GADE_038')
expect(linha[:nota_fiscal]).to eq('90507')
expect(linha[:nome]).to eq('CICERA BESERRA DOS ANJOS')
expect(linha[:endereco]).to eq('RUA AMAPOLA, 206, JARDIM VISTA LINDA, SÃO PAULO, SP, CEP: 05159-100')
expect(linha[:telefones]).to eq('(11) 99262-0594')
expect(linha[:motorista]).to eq('')
end
it 'aceita a grafia corrigida da coluna de referência' do
arquivo = planilha(['Veículo', 'Título', 'Endereço', 'ID de referência'],
[['GADE_001', 'NF 1 - Fulano', 'Rua A, 1', '1']])
expect(described_class.new(arquivo).linhas.first[:nota_fiscal]).to eq('1')
end
it 'aceita título fora do padrão "NF n - NOME" sem perder a parada' do
arquivo = planilha(['Veículo', 'Título', 'Endereço'],
[['GADE_001', 'PONTO DE APOIO', 'Rua A, 1']])
linha = described_class.new(arquivo).linhas.first
expect(linha[:nota_fiscal]).to eq('')
expect(linha[:nome]).to eq('PONTO DE APOIO')
end
it 'tira o sufixo ", Brazil" do endereço e junta vários telefones' do
arquivo = planilha(['Veículo', 'Título', 'Endereço', 'Anotações'],
[['GADE_001', 'NF 1 - Fulano', 'Rua A, 1, Bairro, Brazil',
'TEL: (11) 95440-5780 / (11) 99824-8099']])
linha = described_class.new(arquivo).linhas.first
expect(linha[:endereco]).to eq('RUA A, 1, BAIRRO')
expect(linha[:telefones]).to eq('(11) 95440-5780 / (11) 99824-8099')
end
it 'renumera a ordem de 1..N dentro de cada veículo, na ordem de entrega do plano' do
arquivo = planilha(['Veículo', 'Ordem de entrega', 'Título', 'Endereço'],
[['GADE_001', 3, 'NF 3 - C', 'Rua C'],
['GADE_002', 9, 'NF 9 - I', 'Rua I'],
['GADE_001', 1, 'NF 1 - A', 'Rua A']])
linhas = described_class.new(arquivo).linhas
primeiro = linhas.select { |l| l[:veiculo] == 'GADE_001' }
expect(primeiro.map { |l| [l[:ordem], l[:nota_fiscal]] }).to eq([[1, '1'], [2, '3']])
expect(linhas.find { |l| l[:veiculo] == 'GADE_002' }[:ordem]).to eq(1)
end
it 'pula linha sem veículo (rodapé do próprio export) sem levantar erro' do
arquivo = planilha(['Veículo', 'Título', 'Endereço'],
[['GADE_001', 'NF 1 - A', 'Rua A'], [nil, nil, nil]])
expect(described_class.new(arquivo).linhas.size).to eq(1)
end
it 'recusa arquivo que não é .xlsx com uma mensagem acionável' do
arquivo = Tempfile.new(['plano', '.xlsx'])
arquivo.write('isto não é um zip')
arquivo.flush
expect { described_class.new(arquivo) }
.to raise_error(described_class::Erro, /não é um \.xlsx/)
end
it 'recusa planilha sem a coluna Veículo — não dá para montar folha sem carro' do
arquivo = planilha(['Título', 'Endereço'], [['NF 1 - A', 'Rua A']])
expect { described_class.new(arquivo).linhas }
.to raise_error(described_class::Erro, /Veículo/)
end
end

View File

@@ -0,0 +1,84 @@
# Harness de uma tabela de operação (gade_entregas_*) para os specs.
#
# A tabela é EXTERNA: não tem migration (o cliente a carrega no banco) e não tem
# factory. Sem este harness, testar o enriquecimento do romaneio só daria para
# mockar o método sob teste — o que não pega regressão de SQL, que é onde moram os
# bugs de coluna ausente e de alias (`status` da gade x `status` do espelho).
#
# Guardas copiadas de spec/support/espelho_rastreio.rb, pelo mesmo motivo: se a
# tabela já existir COM DADOS, estamos apontados para dados de verdade e o spec tem
# que abortar em vez de destruí-los.
#
# Uso:
#
# RSpec.describe Romaneios::EnriquecimentoGade do
# include_context 'tabela de operação'
#
# it '...' do
# criar_operacao!('gade_entregas_teste_jan_2026', colunas: %i[nome_completo telefones status])
# semear_operacao!('gade_entregas_teste_jan_2026', [{ nota_fiscal: '1', status: 'NOVO' }])
# end
# end
module TabelaOperacao
COLUNAS_PADRAO = %i[nome_completo telefones status].freeze
class << self
def criar!(nome, colunas: COLUNAS_PADRAO)
raise "Tabela de operação de teste exige RAILS_ENV=test (está em #{Rails.env})" unless Rails.env.test?
if conexao.table_exists?(nome)
linhas = conexao.select_value("SELECT COUNT(*) FROM #{conexao.quote_table_name(nome)}").to_i
if linhas.positive?
raise "#{nome} já existe no banco de teste com #{linhas} linha(s) — o spec NÃO vai " \
'derrubá-la. Aponte RAILS_ENV=test para um banco descartável.'
end
conexao.drop_table(nome)
end
conexao.create_table(nome, id: false) do |t|
t.string :nota_fiscal
colunas.each { |c| t.string c }
end
criadas << nome
nome
end
def semear!(nome, linhas)
colunas = conexao.columns(nome).map(&:name)
linhas.each do |linha|
dados = linha.transform_keys(&:to_s).slice(*colunas)
conexao.execute(
"INSERT INTO #{conexao.quote_table_name(nome)} (#{dados.keys.join(', ')}) " \
"VALUES (#{dados.values.map { |v| conexao.quote(v) }.join(', ')})"
)
end
end
def derrubar_todas!
criadas.each { |nome| conexao.drop_table(nome, if_exists: true) }
criadas.clear
end
def criadas
@criadas ||= []
end
private
def conexao
ActiveRecord::Base.connection
end
end
end
RSpec.shared_context 'tabela de operação' do
after { TabelaOperacao.derrubar_todas! }
def criar_operacao!(nome, colunas: TabelaOperacao::COLUNAS_PADRAO)
TabelaOperacao.criar!(nome, colunas: colunas)
end
def semear_operacao!(nome, linhas)
TabelaOperacao.semear!(nome, linhas)
end
end