ajuste na regra de criação do movimento estorno e quitação de crédito considerando estorno

This commit is contained in:
Diego Freitas 2026-08-08 14:36:33 -03:00
parent f085f2623d
commit 65634edcdf
5 changed files with 120 additions and 31 deletions

View File

@ -147,7 +147,13 @@ async function criar(req, res) {
return res.status(201).json({ return res.status(201).json({
ok: true, ok: true,
message: 'Crédito quitado com sucesso.', message: 'Crédito quitado com sucesso.',
data: resultado.quitacao, data: {
...(resultado.quitacao || {}),
contasQuitadas: resultado.contasQuitadas,
valorQuitado: resultado.valorQuitado,
impactoSaldo: resultado.impactoSaldo || [],
},
// Mantidos no topo por compatibilidade com consumidores antigos.
contasQuitadas: resultado.contasQuitadas, contasQuitadas: resultado.contasQuitadas,
valorQuitado: resultado.valorQuitado, valorQuitado: resultado.valorQuitado,
impactoSaldo: resultado.impactoSaldo || [], impactoSaldo: resultado.impactoSaldo || [],
@ -238,4 +244,4 @@ module.exports = {
criar, criar,
listarHistorico, listarHistorico,
buscarPorId, buscarPorId,
}; };

View File

@ -112,6 +112,19 @@ function idsUnicos(ids = []) {
)]; )];
} }
function contaEhSaidaAberta(conta) {
return conta?.movimento === 'Saida' && conta?.status === 'A pagar';
}
function contaEhEstornoRecebido(conta) {
return (conta?.movimento === 'Entrada' || conta?.movimento === 'Estorno') && conta?.status === 'Recebido';
}
function valorContaNaQuitacao(conta) {
const valor = Number(conta?.valor || 0);
return contaEhEstornoRecebido(conta) ? -valor : valor;
}
async function buscarBancoParaUpdate(connection, idbancos, idUsuario) { async function buscarBancoParaUpdate(connection, idbancos, idUsuario) {
const [rows] = await connection.query( const [rows] = await connection.query(
` `
@ -205,10 +218,18 @@ async function listarBancosPagamento(idUsuario) {
function montarWhereContasPreview(filtros = {}) { function montarWhereContasPreview(filtros = {}) {
const where = [ const where = [
'cp.deleted_at IS NULL', 'cp.deleted_at IS NULL',
"cp.movimento = 'Saida'", `(
"cp.status = 'A pagar'", (cp.movimento = 'Saida' AND cp.status = 'A pagar')
OR
(cp.movimento IN ('Entrada', 'Estorno') AND cp.status = 'Recebido')
)`,
'cp.idbancos = ?', 'cp.idbancos = ?',
'b.idusuarios = ?', 'b.idusuarios = ?',
`NOT EXISTS (
SELECT 1
FROM contasquitadasref cqr
WHERE cqr.idcontasapagar = cp.idcontasapagar
)`,
]; ];
const params = [ const params = [
@ -305,6 +326,11 @@ async function previewQuitacao(idUsuario, filtros = {}) {
[...params, limite, offset] [...params, limite, offset]
); );
const contas = rows.map((conta) => ({
...conta,
valor_quitacao: arredondarMoeda(valorContaNaQuitacao(conta)),
}));
const [countRows] = await pool.query( const [countRows] = await pool.query(
` `
SELECT COUNT(*) AS total SELECT COUNT(*) AS total
@ -318,7 +344,12 @@ async function previewQuitacao(idUsuario, filtros = {}) {
` `
SELECT SELECT
COUNT(*) AS quantidade, COUNT(*) AS quantidade,
COALESCE(SUM(cp.valor), 0) AS valorTotal, COALESCE(SUM(
CASE
WHEN cp.movimento IN ('Entrada', 'Estorno') AND cp.status = 'Recebido' THEN -cp.valor
ELSE cp.valor
END
), 0) AS valorTotal,
MIN(cp.datavencimento) AS primeiroVencimento, MIN(cp.datavencimento) AS primeiroVencimento,
MAX(cp.datavencimento) AS ultimoVencimento MAX(cp.datavencimento) AS ultimoVencimento
${FROM_CONTA_JOIN} ${FROM_CONTA_JOIN}
@ -333,7 +364,7 @@ async function previewQuitacao(idUsuario, filtros = {}) {
return { return {
bancoCredito, bancoCredito,
contas: rows, contas,
pagination: { pagination: {
total, total,
limite, limite,
@ -343,8 +374,11 @@ async function previewQuitacao(idUsuario, filtros = {}) {
}, },
summary: { summary: {
quantidade: Number(resumo.quantidade || 0), quantidade: Number(resumo.quantidade || 0),
totalAberto: arredondarMoeda(resumo.valorTotal || 0),
valorTotal: arredondarMoeda(resumo.valorTotal || 0), valorTotal: arredondarMoeda(resumo.valorTotal || 0),
saldoAtualCredito: arredondarMoeda(bancoCredito.saldo || 0),
saldoCreditoAtual: arredondarMoeda(bancoCredito.saldo || 0), saldoCreditoAtual: arredondarMoeda(bancoCredito.saldo || 0),
saldoAposQuitacaoTotal: arredondarMoeda(Number(bancoCredito.saldo || 0) - Number(resumo.valorTotal || 0)),
saldoCreditoAposQuitacaoTotal: arredondarMoeda(Number(bancoCredito.saldo || 0) - Number(resumo.valorTotal || 0)), saldoCreditoAposQuitacaoTotal: arredondarMoeda(Number(bancoCredito.saldo || 0) - Number(resumo.valorTotal || 0)),
primeiroVencimento: resumo.primeiroVencimento || null, primeiroVencimento: resumo.primeiroVencimento || null,
ultimoVencimento: resumo.ultimoVencimento || null, ultimoVencimento: resumo.ultimoVencimento || null,
@ -366,6 +400,11 @@ async function buscarContasSelecionadasParaUpdate(connection, ids, idBancoCredit
WHERE cp.idcontasapagar IN (${placeholders}) WHERE cp.idcontasapagar IN (${placeholders})
AND cp.idbancos = ? AND cp.idbancos = ?
AND b.idusuarios = ? AND b.idusuarios = ?
AND NOT EXISTS (
SELECT 1
FROM contasquitadasref cqr
WHERE cqr.idcontasapagar = cp.idcontasapagar
)
FOR UPDATE FOR UPDATE
`, `,
[ [
@ -475,16 +514,16 @@ async function criarQuitacaoCredito(idUsuario, dados = {}) {
throw criarErro(`A conta ${conta.idcontasapagar} não pertence ao banco de crédito selecionado.`); throw criarErro(`A conta ${conta.idcontasapagar} não pertence ao banco de crédito selecionado.`);
} }
if (conta.movimento !== 'Saida') { if (!contaEhSaidaAberta(conta) && !contaEhEstornoRecebido(conta)) {
throw criarErro(`A conta ${conta.idcontasapagar} não é uma saída de crédito.`); throw criarErro(
} `A conta ${conta.idcontasapagar} não é uma saída em aberto nem um estorno recebido.`
);
if (conta.status !== 'A pagar') {
throw criarErro(`A conta ${conta.idcontasapagar} não está em aberto para pagamento.`);
} }
} }
const valorTotal = arredondarMoeda(contasAntes.reduce((total, conta) => total + Number(conta.valor || 0), 0)); const valorTotal = arredondarMoeda(
contasAntes.reduce((total, conta) => total + valorContaNaQuitacao(conta), 0)
);
if (valorTotal <= 0) { if (valorTotal <= 0) {
throw criarErro('O valor total da quitação deve ser maior que zero.'); throw criarErro('O valor total da quitação deve ser maior que zero.');
@ -555,6 +594,8 @@ async function criarQuitacaoCredito(idUsuario, dados = {}) {
idbancos_p = ?, idbancos_p = ?,
update_date = NOW() update_date = NOW()
WHERE idcontasapagar IN (${placeholders}) WHERE idcontasapagar IN (${placeholders})
AND movimento = 'Saida'
AND status = 'A pagar'
`, `,
[ [
dataQuitacao, dataQuitacao,
@ -837,4 +878,4 @@ module.exports = {
criarQuitacaoCredito, criarQuitacaoCredito,
listarHistorico, listarHistorico,
buscarQuitacaoPorId, buscarQuitacaoPorId,
}; };

View File

@ -59,7 +59,12 @@ type FormSectionProps = {
}; };
function hojeISO() { function hojeISO() {
return new Date().toISOString().slice(0, 10); const hoje = new Date();
const ano = hoje.getFullYear();
const mes = String(hoje.getMonth() + 1).padStart(2, '0');
const dia = String(hoje.getDate()).padStart(2, '0');
return `${ano}-${mes}-${dia}`;
} }
function calcularVencimentoCreditoProximoMes(diaVencimento: number, dataBaseISO?: string) { function calcularVencimentoCreditoProximoMes(diaVencimento: number, dataBaseISO?: string) {
@ -79,13 +84,11 @@ function calcularVencimentoCreditoProximoMes(diaVencimento: number, dataBaseISO?
const diaSeguro = Math.min(Number(diaVencimento), ultimoDiaProximoMes); const diaSeguro = Math.min(Number(diaVencimento), ultimoDiaProximoMes);
return new Date( const anoVencimento = proximoMes.getFullYear();
proximoMes.getFullYear(), const mesVencimento = String(proximoMes.getMonth() + 1).padStart(2, '0');
proximoMes.getMonth(), const diaVencimentoFormatado = String(diaSeguro).padStart(2, '0');
diaSeguro
) return `${anoVencimento}-${mesVencimento}-${diaVencimentoFormatado}`;
.toISOString()
.slice(0, 10);
} }
function toNumberOrNull(value: string): number | null { function toNumberOrNull(value: string): number | null {
@ -287,6 +290,10 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
return ['Recebido', 'A receber']; return ['Recebido', 'A receber'];
} }
if (movimento === 'Estorno') {
return ['Recebido'];
}
if (movimento === 'Saida' || movimento === 'Sangria') { if (movimento === 'Saida' || movimento === 'Sangria') {
return ['Pago', 'A pagar']; return ['Pago', 'A pagar'];
} }
@ -393,7 +400,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
useEffect(() => { useEffect(() => {
if (isEdit) return; if (isEdit) return;
if (movimento !== 'Saida') return; if (movimento !== 'Saida' && movimento !== 'Estorno') return;
if (!bancoSelecionado) return; if (!bancoSelecionado) return;
if (Number(bancoSelecionado.debito) !== 0) return; if (Number(bancoSelecionado.debito) !== 0) return;
if (!bancoSelecionado.dia_vencimento) return; if (!bancoSelecionado.dia_vencimento) return;
@ -405,9 +412,15 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
setDataVencimento(novaDataVencimento); setDataVencimento(novaDataVencimento);
if (status === 'Pago') { if (movimento === 'Saida' && status === 'Pago') {
setStatus('A pagar'); setStatus('A pagar');
setDataBaixa(''); setDataBaixa('');
return;
}
if (movimento === 'Estorno') {
setStatus('Recebido');
setDataBaixa(dataEntrada || hojeISO());
} }
}, [ }, [
isEdit, isEdit,

View File

@ -102,6 +102,23 @@ function impactoColor(valorDelta: number) {
return valorDelta >= 0 ? 'success.main' : 'error.main'; return valorDelta >= 0 ? 'success.main' : 'error.main';
} }
function valorContaQuitacao(conta: ContaCreditoQuitacao) {
const ehEstornoRecebido =
(conta.movimento === 'Entrada' || conta.movimento === 'Estorno') &&
conta.status === 'Recebido';
return Number(
conta.valor_quitacao ??
(ehEstornoRecebido
? -Number(conta.valor || 0)
: Number(conta.valor || 0))
);
}
function contaEhEstorno(conta: ContaCreditoQuitacao) {
return valorContaQuitacao(conta) < 0;
}
export function QuitacoesCreditoPage() { export function QuitacoesCreditoPage() {
const [bancosCredito, setBancosCredito] = useState<BancoQuitacao[]>([]); const [bancosCredito, setBancosCredito] = useState<BancoQuitacao[]>([]);
const [bancosPagamento, setBancosPagamento] = useState<BancoQuitacao[]>([]); const [bancosPagamento, setBancosPagamento] = useState<BancoQuitacao[]>([]);
@ -142,7 +159,7 @@ export function QuitacoesCreditoPage() {
}, [contas, idsSelecionados]); }, [contas, idsSelecionados]);
const totalSelecionado = useMemo(() => { const totalSelecionado = useMemo(() => {
return contasSelecionadas.reduce((total, conta) => total + Number(conta.valor || 0), 0); return contasSelecionadas.reduce((total, conta) => total + valorContaQuitacao(conta), 0);
}, [contasSelecionadas]); }, [contasSelecionadas]);
const todasSelecionadas = contas.length > 0 && idsSelecionados.length === contas.length; const todasSelecionadas = contas.length > 0 && idsSelecionados.length === contas.length;
@ -627,8 +644,12 @@ export function QuitacoesCreditoPage() {
sx={{ margin: 0 }} sx={{ margin: 0 }}
/> />
<Typography fontWeight={950} whiteSpace="nowrap"> <Typography
{formatarValor(conta.valor)} fontWeight={950}
whiteSpace="nowrap"
color={contaEhEstorno(conta) ? 'success.main' : 'inherit'}
>
{formatarValor(valorContaQuitacao(conta))}
</Typography> </Typography>
</Stack> </Stack>
@ -639,6 +660,9 @@ export function QuitacoesCreditoPage() {
variant="outlined" variant="outlined"
color={statusChipColor(conta.status) as any} color={statusChipColor(conta.status) as any}
/> />
{contaEhEstorno(conta) && (
<Chip label="Estorno" size="small" color="success" variant="outlined" />
)}
<Chip <Chip
label={`${conta.parcela}/${conta.parcelas}`} label={`${conta.parcela}/${conta.parcelas}`}
size="small" size="small"
@ -726,8 +750,11 @@ export function QuitacoesCreditoPage() {
<TableCell>{formatarData(conta.datavencimento)}</TableCell> <TableCell>{formatarData(conta.datavencimento)}</TableCell>
<TableCell align="right"> <TableCell align="right">
<Typography fontWeight={950}> <Typography
{formatarValor(conta.valor)} fontWeight={950}
color={contaEhEstorno(conta) ? 'success.main' : 'inherit'}
>
{formatarValor(valorContaQuitacao(conta))}
</Typography> </Typography>
</TableCell> </TableCell>
@ -967,4 +994,4 @@ export function QuitacoesCreditoPage() {
</Stack> </Stack>
</Box> </Box>
); );
} }

View File

@ -15,6 +15,8 @@ export type ContaCreditoQuitacao = {
datavencimento: string | null; datavencimento: string | null;
databaixa: string | null; databaixa: string | null;
valor: number; valor: number;
/** Valor com sinal usado na quitação: saídas positivas e estornos negativos. */
valor_quitacao?: number;
parcela: number; parcela: number;
parcelas: number; parcelas: number;
status: string; status: string;
@ -107,4 +109,4 @@ export type ApiResponse<T> = {
ok: boolean; ok: boolean;
message?: string; message?: string;
data: T; data: T;
}; };