ajuste na regra de criação do movimento estorno e quitação de crédito considerando estorno
This commit is contained in:
parent
f085f2623d
commit
65634edcdf
|
|
@ -147,7 +147,13 @@ async function criar(req, res) {
|
|||
return res.status(201).json({
|
||||
ok: true,
|
||||
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,
|
||||
valorQuitado: resultado.valorQuitado,
|
||||
impactoSaldo: resultado.impactoSaldo || [],
|
||||
|
|
@ -238,4 +244,4 @@ module.exports = {
|
|||
criar,
|
||||
listarHistorico,
|
||||
buscarPorId,
|
||||
};
|
||||
};
|
||||
|
|
@ -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) {
|
||||
const [rows] = await connection.query(
|
||||
`
|
||||
|
|
@ -205,10 +218,18 @@ async function listarBancosPagamento(idUsuario) {
|
|||
function montarWhereContasPreview(filtros = {}) {
|
||||
const where = [
|
||||
'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 = ?',
|
||||
'b.idusuarios = ?',
|
||||
`NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM contasquitadasref cqr
|
||||
WHERE cqr.idcontasapagar = cp.idcontasapagar
|
||||
)`,
|
||||
];
|
||||
|
||||
const params = [
|
||||
|
|
@ -305,6 +326,11 @@ async function previewQuitacao(idUsuario, filtros = {}) {
|
|||
[...params, limite, offset]
|
||||
);
|
||||
|
||||
const contas = rows.map((conta) => ({
|
||||
...conta,
|
||||
valor_quitacao: arredondarMoeda(valorContaNaQuitacao(conta)),
|
||||
}));
|
||||
|
||||
const [countRows] = await pool.query(
|
||||
`
|
||||
SELECT COUNT(*) AS total
|
||||
|
|
@ -318,7 +344,12 @@ async function previewQuitacao(idUsuario, filtros = {}) {
|
|||
`
|
||||
SELECT
|
||||
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,
|
||||
MAX(cp.datavencimento) AS ultimoVencimento
|
||||
${FROM_CONTA_JOIN}
|
||||
|
|
@ -333,7 +364,7 @@ async function previewQuitacao(idUsuario, filtros = {}) {
|
|||
|
||||
return {
|
||||
bancoCredito,
|
||||
contas: rows,
|
||||
contas,
|
||||
pagination: {
|
||||
total,
|
||||
limite,
|
||||
|
|
@ -343,8 +374,11 @@ async function previewQuitacao(idUsuario, filtros = {}) {
|
|||
},
|
||||
summary: {
|
||||
quantidade: Number(resumo.quantidade || 0),
|
||||
totalAberto: arredondarMoeda(resumo.valorTotal || 0),
|
||||
valorTotal: arredondarMoeda(resumo.valorTotal || 0),
|
||||
saldoAtualCredito: 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)),
|
||||
primeiroVencimento: resumo.primeiroVencimento || null,
|
||||
ultimoVencimento: resumo.ultimoVencimento || null,
|
||||
|
|
@ -366,6 +400,11 @@ async function buscarContasSelecionadasParaUpdate(connection, ids, idBancoCredit
|
|||
WHERE cp.idcontasapagar IN (${placeholders})
|
||||
AND cp.idbancos = ?
|
||||
AND b.idusuarios = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM contasquitadasref cqr
|
||||
WHERE cqr.idcontasapagar = cp.idcontasapagar
|
||||
)
|
||||
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.`);
|
||||
}
|
||||
|
||||
if (conta.movimento !== 'Saida') {
|
||||
throw criarErro(`A conta ${conta.idcontasapagar} não é uma saída de crédito.`);
|
||||
}
|
||||
|
||||
if (conta.status !== 'A pagar') {
|
||||
throw criarErro(`A conta ${conta.idcontasapagar} não está em aberto para pagamento.`);
|
||||
if (!contaEhSaidaAberta(conta) && !contaEhEstornoRecebido(conta)) {
|
||||
throw criarErro(
|
||||
`A conta ${conta.idcontasapagar} não é uma saída em aberto nem um estorno recebido.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
throw criarErro('O valor total da quitação deve ser maior que zero.');
|
||||
|
|
@ -555,6 +594,8 @@ async function criarQuitacaoCredito(idUsuario, dados = {}) {
|
|||
idbancos_p = ?,
|
||||
update_date = NOW()
|
||||
WHERE idcontasapagar IN (${placeholders})
|
||||
AND movimento = 'Saida'
|
||||
AND status = 'A pagar'
|
||||
`,
|
||||
[
|
||||
dataQuitacao,
|
||||
|
|
@ -837,4 +878,4 @@ module.exports = {
|
|||
criarQuitacaoCredito,
|
||||
listarHistorico,
|
||||
buscarQuitacaoPorId,
|
||||
};
|
||||
};
|
||||
|
|
@ -59,7 +59,12 @@ type FormSectionProps = {
|
|||
};
|
||||
|
||||
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) {
|
||||
|
|
@ -79,13 +84,11 @@ function calcularVencimentoCreditoProximoMes(diaVencimento: number, dataBaseISO?
|
|||
|
||||
const diaSeguro = Math.min(Number(diaVencimento), ultimoDiaProximoMes);
|
||||
|
||||
return new Date(
|
||||
proximoMes.getFullYear(),
|
||||
proximoMes.getMonth(),
|
||||
diaSeguro
|
||||
)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
const anoVencimento = proximoMes.getFullYear();
|
||||
const mesVencimento = String(proximoMes.getMonth() + 1).padStart(2, '0');
|
||||
const diaVencimentoFormatado = String(diaSeguro).padStart(2, '0');
|
||||
|
||||
return `${anoVencimento}-${mesVencimento}-${diaVencimentoFormatado}`;
|
||||
}
|
||||
|
||||
function toNumberOrNull(value: string): number | null {
|
||||
|
|
@ -287,6 +290,10 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
|||
return ['Recebido', 'A receber'];
|
||||
}
|
||||
|
||||
if (movimento === 'Estorno') {
|
||||
return ['Recebido'];
|
||||
}
|
||||
|
||||
if (movimento === 'Saida' || movimento === 'Sangria') {
|
||||
return ['Pago', 'A pagar'];
|
||||
}
|
||||
|
|
@ -393,7 +400,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
|||
|
||||
useEffect(() => {
|
||||
if (isEdit) return;
|
||||
if (movimento !== 'Saida') return;
|
||||
if (movimento !== 'Saida' && movimento !== 'Estorno') return;
|
||||
if (!bancoSelecionado) return;
|
||||
if (Number(bancoSelecionado.debito) !== 0) return;
|
||||
if (!bancoSelecionado.dia_vencimento) return;
|
||||
|
|
@ -405,9 +412,15 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) {
|
|||
|
||||
setDataVencimento(novaDataVencimento);
|
||||
|
||||
if (status === 'Pago') {
|
||||
if (movimento === 'Saida' && status === 'Pago') {
|
||||
setStatus('A pagar');
|
||||
setDataBaixa('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (movimento === 'Estorno') {
|
||||
setStatus('Recebido');
|
||||
setDataBaixa(dataEntrada || hojeISO());
|
||||
}
|
||||
}, [
|
||||
isEdit,
|
||||
|
|
|
|||
|
|
@ -102,6 +102,23 @@ function impactoColor(valorDelta: number) {
|
|||
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() {
|
||||
const [bancosCredito, setBancosCredito] = useState<BancoQuitacao[]>([]);
|
||||
const [bancosPagamento, setBancosPagamento] = useState<BancoQuitacao[]>([]);
|
||||
|
|
@ -142,7 +159,7 @@ export function QuitacoesCreditoPage() {
|
|||
}, [contas, idsSelecionados]);
|
||||
|
||||
const totalSelecionado = useMemo(() => {
|
||||
return contasSelecionadas.reduce((total, conta) => total + Number(conta.valor || 0), 0);
|
||||
return contasSelecionadas.reduce((total, conta) => total + valorContaQuitacao(conta), 0);
|
||||
}, [contasSelecionadas]);
|
||||
|
||||
const todasSelecionadas = contas.length > 0 && idsSelecionados.length === contas.length;
|
||||
|
|
@ -627,8 +644,12 @@ export function QuitacoesCreditoPage() {
|
|||
sx={{ margin: 0 }}
|
||||
/>
|
||||
|
||||
<Typography fontWeight={950} whiteSpace="nowrap">
|
||||
{formatarValor(conta.valor)}
|
||||
<Typography
|
||||
fontWeight={950}
|
||||
whiteSpace="nowrap"
|
||||
color={contaEhEstorno(conta) ? 'success.main' : 'inherit'}
|
||||
>
|
||||
{formatarValor(valorContaQuitacao(conta))}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
|
|
@ -639,6 +660,9 @@ export function QuitacoesCreditoPage() {
|
|||
variant="outlined"
|
||||
color={statusChipColor(conta.status) as any}
|
||||
/>
|
||||
{contaEhEstorno(conta) && (
|
||||
<Chip label="Estorno" size="small" color="success" variant="outlined" />
|
||||
)}
|
||||
<Chip
|
||||
label={`${conta.parcela}/${conta.parcelas}`}
|
||||
size="small"
|
||||
|
|
@ -726,8 +750,11 @@ export function QuitacoesCreditoPage() {
|
|||
<TableCell>{formatarData(conta.datavencimento)}</TableCell>
|
||||
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight={950}>
|
||||
{formatarValor(conta.valor)}
|
||||
<Typography
|
||||
fontWeight={950}
|
||||
color={contaEhEstorno(conta) ? 'success.main' : 'inherit'}
|
||||
>
|
||||
{formatarValor(valorContaQuitacao(conta))}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
|
|
@ -967,4 +994,4 @@ export function QuitacoesCreditoPage() {
|
|||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ export type ContaCreditoQuitacao = {
|
|||
datavencimento: string | null;
|
||||
databaixa: string | null;
|
||||
valor: number;
|
||||
/** Valor com sinal usado na quitação: saídas positivas e estornos negativos. */
|
||||
valor_quitacao?: number;
|
||||
parcela: number;
|
||||
parcelas: number;
|
||||
status: string;
|
||||
|
|
@ -107,4 +109,4 @@ export type ApiResponse<T> = {
|
|||
ok: boolean;
|
||||
message?: string;
|
||||
data: T;
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue