Adicionado geração de movimentos fixos

This commit is contained in:
Diego Freitas 2026-06-02 10:05:23 -03:00
parent 0731328492
commit 66287cff29
2 changed files with 452 additions and 1 deletions

View File

@ -1,4 +1,5 @@
const movimentosService = require('../services/movimentos.service');
const movimentosFixosGeracaoService = require('../../movimentosFixos/services/movimentosFixosGeracao.service');
const MOVIMENTOS_VALIDOS = ['Entrada', 'Saida', 'Sangria', 'Estorno'];
const STATUS_VALIDOS = ['Pago', 'A pagar', 'Recebido', 'A receber'];
@ -120,6 +121,14 @@ function tratarErro(error, res, mensagemPadrao) {
async function listar(req, res) {
try {
const idUsuario = req.user.idusuarios || req.user.id;
const geracaoFixos = await movimentosFixosGeracaoService.gerarParaIntervalo({
dataInicio: req.query.dataInicio,
dataFim: req.query.dataFim,
idUsuario,
});
const resultado = await movimentosService.listarMovimentos({
limite: req.query.limite,
offset: req.query.offset,
@ -144,7 +153,7 @@ async function listar(req, res) {
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
idusuarios: req.user.idusuarios || req.user.id,
idusuarios: idUsuario,
});
return res.json({
@ -152,6 +161,7 @@ async function listar(req, res) {
data: resultado.data,
pagination: resultado.pagination,
summary: resultado.summary,
movimentosFixosGerados: geracaoFixos,
});
} catch (error) {
console.error('Erro ao listar movimentos:', error);

View File

@ -0,0 +1,441 @@
const pool = require('../../../database/mysql');
function normalizarDataOuNull(valor) {
if (!valor) return null;
const texto = String(valor).slice(0, 10);
if (!/^\d{4}-\d{2}-\d{2}$/.test(texto)) {
return null;
}
return texto;
}
function hojeDataString() {
return new Date().toISOString().slice(0, 10);
}
function competenciaAtual() {
return hojeDataString().slice(0, 7);
}
function primeiroDiaCompetencia(competencia) {
return `${competencia}-01`;
}
function ultimoDiaDoMes(ano, mes) {
return new Date(ano, mes, 0).getDate();
}
function montarDataVencimento(competencia, diaVencimento) {
const [anoStr, mesStr] = competencia.split('-');
const ano = Number(anoStr);
const mes = Number(mesStr);
const ultimoDia = ultimoDiaDoMes(ano, mes);
const dia = Math.min(Math.max(Number(diaVencimento || 1), 1), ultimoDia);
return `${competencia}-${String(dia).padStart(2, '0')}`;
}
function addMesesCompetencia(competencia, meses) {
const [anoStr, mesStr] = competencia.split('-');
const data = new Date(Number(anoStr), Number(mesStr) - 1, 1);
data.setMonth(data.getMonth() + meses);
const ano = data.getFullYear();
const mes = String(data.getMonth() + 1).padStart(2, '0');
return `${ano}-${mes}`;
}
function listarCompetenciasEntre(dataInicio, dataFim) {
const inicio = normalizarDataOuNull(dataInicio) || hojeDataString();
const fim = normalizarDataOuNull(dataFim) || inicio;
let competenciaInicio = inicio.slice(0, 7);
const competenciaFim = fim.slice(0, 7);
const atual = competenciaAtual();
// Regra anti-retroativo:
// se o filtro começa no passado, começa no mês atual.
if (competenciaInicio < atual) {
competenciaInicio = atual;
}
if (competenciaInicio > competenciaFim) {
return [];
}
const competencias = [];
let cursor = competenciaInicio;
while (cursor <= competenciaFim) {
competencias.push(cursor);
cursor = addMesesCompetencia(cursor, 1);
}
return competencias;
}
function statusInicialPorMovimento(movimento) {
if (movimento === 'Entrada' || movimento === 'Estorno') {
return 'A receber';
}
if (movimento === 'Saida') {
return 'A pagar';
}
if (movimento === 'Sangria') {
return 'Pago';
}
return 'A pagar';
}
function dataBaixaInicialPorMovimento(movimento, dataVencimento) {
if (movimento === 'Sangria') {
return dataVencimento;
}
return null;
}
async function buscarFixosDoUsuario(connection, idUsuario) {
const [rows] = await connection.query(
`
SELECT
mf.idmovimentosfixos,
mf.movimento,
mf.descricao,
mf.valor,
mf.dia_vencimento,
mf.idcentrodecustos,
mf.idbancos
FROM movimentosfixos mf
INNER JOIN bancos b ON b.idbancos = mf.idbancos
WHERE mf.habilitado = 1
AND mf.gerar_automatico = 1
AND b.idusuarios = ?
ORDER BY mf.idmovimentosfixos ASC
`,
[idUsuario]
);
return rows;
}
async function jaExisteMovimentoFixo(connection, fixo, competencia) {
const [rowsGeracao] = await connection.query(
`
SELECT idmovimentosfixosgeracoes, idcontasapagar, status
FROM movimentosfixosgeracoes
WHERE idmovimentosfixos = ?
AND competencia = ?
LIMIT 1
FOR UPDATE
`,
[fixo.idmovimentosfixos, competencia]
);
if (rowsGeracao.length > 0) {
return true;
}
const [rowsMovimentoVinculado] = await connection.query(
`
SELECT idcontasapagar
FROM contasapagar
WHERE idmovimentosfixos = ?
AND competencia = ?
AND deleted_at IS NULL
LIMIT 1
FOR UPDATE
`,
[fixo.idmovimentosfixos, competencia]
);
if (rowsMovimentoVinculado.length > 0) {
await connection.query(
`
INSERT INTO movimentosfixosgeracoes (
idmovimentosfixos,
competencia,
idcontasapagar,
status,
mensagem,
insert_date,
update_date
) VALUES (?, ?, ?, 'existente', 'Movimento fixo já existia vinculado em contasapagar.', NOW(), NOW())
ON DUPLICATE KEY UPDATE
idcontasapagar = VALUES(idcontasapagar),
status = VALUES(status),
mensagem = VALUES(mensagem),
update_date = NOW()
`,
[fixo.idmovimentosfixos, competencia, rowsMovimentoVinculado[0].idcontasapagar]
);
return true;
}
const [rowsMovimentoLegado] = await connection.query(
`
SELECT idcontasapagar
FROM contasapagar
WHERE deleted_at IS NULL
AND idmovimentosfixos IS NULL
AND competencia IS NULL
AND movimento = ?
AND descricao = ?
AND idbancos = ?
AND (
(? IS NULL AND idcentrodecustos IS NULL)
OR idcentrodecustos = ?
)
AND DATE(datavencimento) BETWEEN ? AND ?
ORDER BY idcontasapagar DESC
LIMIT 1
FOR UPDATE
`,
[
fixo.movimento,
fixo.descricao,
fixo.idbancos,
fixo.idcentrodecustos || null,
fixo.idcentrodecustos || null,
`${competencia}-01`,
`${competencia}-31`,
]
);
if (rowsMovimentoLegado.length > 0) {
const idContasPagar = rowsMovimentoLegado[0].idcontasapagar;
await connection.query(
`
UPDATE contasapagar
SET
idmovimentosfixos = ?,
competencia = ?,
origem = COALESCE(origem, 'movimento_fixo_legado'),
update_date = NOW()
WHERE idcontasapagar = ?
`,
[fixo.idmovimentosfixos, competencia, idContasPagar]
);
await connection.query(
`
INSERT INTO movimentosfixosgeracoes (
idmovimentosfixos,
competencia,
idcontasapagar,
status,
mensagem,
insert_date,
update_date
) VALUES (?, ?, ?, 'existente', 'Movimento legado vinculado automaticamente ao fixo.', NOW(), NOW())
ON DUPLICATE KEY UPDATE
idcontasapagar = VALUES(idcontasapagar),
status = VALUES(status),
mensagem = VALUES(mensagem),
update_date = NOW()
`,
[fixo.idmovimentosfixos, competencia, idContasPagar]
);
return true;
}
return false;
}
async function inserirMovimentoFixoGerado(connection, fixo, competencia, idUsuario) {
const dataEntrada = primeiroDiaCompetencia(competencia);
const dataVencimento = montarDataVencimento(competencia, fixo.dia_vencimento);
const status = statusInicialPorMovimento(fixo.movimento);
const dataBaixa = dataBaixaInicialPorMovimento(fixo.movimento, dataVencimento);
const [result] = await connection.query(
`
INSERT INTO contasapagar (
movimento,
descricao,
dataentrada,
datavencimento,
databaixa,
valor,
parcela,
parcelas,
status,
idcentrodecustos,
idbancos,
idusuarios_cad,
idusuarios_baixa,
idclientes,
idveiculosdetalhes,
idbancos_p,
idmovimentosfixos,
competencia,
grupo_parcelamento,
origem,
saldo_processado,
observacao,
insert_date,
update_date,
deleted_at
) VALUES (?, ?, ?, ?, ?, ?, 1, 1, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?, NULL, 'movimento_fixo', 0, NULL, NOW(), NOW(), NULL)
`,
[
fixo.movimento,
fixo.descricao,
dataEntrada,
dataVencimento,
dataBaixa,
Number(fixo.valor || 0),
status,
fixo.idcentrodecustos || null,
fixo.idbancos || null,
idUsuario,
dataBaixa ? idUsuario : null,
fixo.idmovimentosfixos,
competencia,
]
);
const idContasPagar = result.insertId;
await connection.query(
`
INSERT INTO movimentosfixos_geracoes (
idmovimentosfixos,
competencia,
idcontasapagar,
status,
mensagem,
insert_date,
update_date
) VALUES (?, ?, ?, 'gerado', 'Movimento gerado automaticamente.', NOW(), NOW())
ON DUPLICATE KEY UPDATE
idcontasapagar = VALUES(idcontasapagar),
status = VALUES(status),
mensagem = VALUES(mensagem),
update_date = NOW()
`,
[fixo.idmovimentosfixos, competencia, idContasPagar]
);
await connection.query(
`
UPDATE movimentosfixos
SET
ultimo_mes_gerado = CASE
WHEN ultimo_mes_gerado IS NULL OR ultimo_mes_gerado < ? THEN ?
ELSE ultimo_mes_gerado
END,
update_date = NOW()
WHERE idmovimentosfixos = ?
`,
[competencia, competencia, fixo.idmovimentosfixos]
);
return idContasPagar;
}
async function gerarParaIntervalo(opcoes = {}) {
const idUsuario = Number(opcoes.idUsuario || opcoes.idusuarios || 0);
if (!idUsuario) {
return {
gerados: 0,
ignorados: 0,
competencias: [],
detalhes: [],
};
}
const competencias = listarCompetenciasEntre(opcoes.dataInicio, opcoes.dataFim);
if (competencias.length === 0) {
return {
gerados: 0,
ignorados: 0,
competencias: [],
detalhes: [],
};
}
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const fixos = await buscarFixosDoUsuario(connection, idUsuario);
let gerados = 0;
let ignorados = 0;
const detalhes = [];
for (const competencia of competencias) {
for (const fixo of fixos) {
const existe = await jaExisteMovimentoFixo(
connection,
fixo,
competencia
);
if (existe) {
ignorados++;
detalhes.push({
idmovimentosfixos: fixo.idmovimentosfixos,
competencia,
status: 'ignorado',
mensagem: 'Já existia geração ou movimento vinculado.',
});
continue;
}
const idcontasapagar = await inserirMovimentoFixoGerado(
connection,
fixo,
competencia,
idUsuario
);
gerados++;
detalhes.push({
idmovimentosfixos: fixo.idmovimentosfixos,
competencia,
idcontasapagar,
status: 'gerado',
});
}
}
await connection.commit();
return {
gerados,
ignorados,
competencias,
detalhes,
};
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}
module.exports = {
gerarParaIntervalo,
};