Compare commits

...

3 Commits

Author SHA1 Message Date
sebseb7
adfcd90dcf Enhance transaction filtering by time range in API
- Implemented dynamic SQL WHERE clause to filter transactions based on various time range formats: quarter, year, and month.
- Removed redundant post-processing logic for filtering transactions, as the SQL query now handles this directly.
- Updated summary calculations to reflect the new transaction filtering approach, ensuring accurate reporting of totals and JTL matches.
2025-08-23 04:17:17 +02:00
sebseb7
bb610e0480 Add duplicate transaction check in CSV import process
- Implemented a check for existing transactions in the database to prevent duplicates during CSV imports.
- Added SQL query to count existing transactions based on key fields before insertion.
- Enhanced error handling to log and skip duplicate transactions, improving data integrity during the import process.
2025-08-22 23:46:20 +02:00
sebseb7
44d6cf6352 Update CSV import queries to include 'pending' status for datevlink field
- Modified SQL update queries in the csvImport.js file to allow for 'pending' status in addition to NULL or empty values for the datevlink field in both tUmsatzBeleg and tPdfObjekt tables. This change enhances the handling of datevlink updates during the import process.
2025-08-22 23:23:58 +02:00
2 changed files with 103 additions and 196 deletions

View File

@@ -71,6 +71,37 @@ router.post('/test-csv-import', async (req, res) => {
numericAmount = parseFloat(normalizedAmount) || 0; numericAmount = parseFloat(normalizedAmount) || 0;
} }
// Check for existing transaction to prevent duplicates
const duplicateCheckQuery = `
SELECT COUNT(*) as count FROM fibdash.CSVTransactions
WHERE buchungstag = @buchungstag
AND wertstellung = @wertstellung
AND umsatzart = @umsatzart
AND betrag = @betrag
AND beguenstigter_zahlungspflichtiger = @beguenstigter_zahlungspflichtiger
AND verwendungszweck = @verwendungszweck
`;
const duplicateCheckResult = await executeQuery(duplicateCheckQuery, {
buchungstag: transaction['Buchungstag'] || null,
wertstellung: transaction['Valutadatum'] || null,
umsatzart: transaction['Buchungstext'] || null,
betrag: numericAmount,
beguenstigter_zahlungspflichtiger: transaction['Beguenstigter/Zahlungspflichtiger'] || null,
verwendungszweck: transaction['Verwendungszweck'] || null
});
if (duplicateCheckResult.recordset[0].count > 0) {
console.log(`Skipping duplicate transaction at row ${i + 1}: ${transaction['Buchungstag']} - ${numericAmount}`);
errors.push({
row: i + 1,
error: 'Duplicate transaction (already exists in database)',
transaction: transaction
});
errorCount++;
continue;
}
const insertQuery = ` const insertQuery = `
INSERT INTO fibdash.CSVTransactions INSERT INTO fibdash.CSVTransactions
(buchungstag, wertstellung, umsatzart, betrag, betrag_original, waehrung, (buchungstag, wertstellung, umsatzart, betrag, betrag_original, waehrung,
@@ -246,6 +277,37 @@ router.post('/import-csv-transactions', authenticateToken, async (req, res) => {
numericAmount = parseFloat(normalizedAmount) || 0; numericAmount = parseFloat(normalizedAmount) || 0;
} }
// Check for existing transaction to prevent duplicates
const duplicateCheckQuery = `
SELECT COUNT(*) as count FROM fibdash.CSVTransactions
WHERE buchungstag = @buchungstag
AND wertstellung = @wertstellung
AND umsatzart = @umsatzart
AND betrag = @betrag
AND beguenstigter_zahlungspflichtiger = @beguenstigter_zahlungspflichtiger
AND verwendungszweck = @verwendungszweck
`;
const duplicateCheckResult = await executeQuery(duplicateCheckQuery, {
buchungstag: transaction['Buchungstag'] || null,
wertstellung: transaction['Valutadatum'] || null,
umsatzart: transaction['Buchungstext'] || null,
betrag: numericAmount,
beguenstigter_zahlungspflichtiger: transaction['Beguenstigter/Zahlungspflichtiger'] || null,
verwendungszweck: transaction['Verwendungszweck'] || null
});
if (duplicateCheckResult.recordset[0].count > 0) {
console.log(`Skipping duplicate transaction at row ${i + 1}: ${transaction['Buchungstag']} - ${numericAmount}`);
errors.push({
row: i + 1,
error: 'Duplicate transaction (already exists in database)',
transaction: transaction
});
errorCount++;
continue;
}
const insertQuery = ` const insertQuery = `
INSERT INTO fibdash.CSVTransactions INSERT INTO fibdash.CSVTransactions
(buchungstag, wertstellung, umsatzart, betrag, betrag_original, waehrung, (buchungstag, wertstellung, umsatzart, betrag, betrag_original, waehrung,
@@ -489,7 +551,7 @@ router.post('/import-datev-beleglinks', authenticateToken, async (req, res) => {
const updateQuery = ` const updateQuery = `
UPDATE eazybusiness.dbo.tUmsatzBeleg UPDATE eazybusiness.dbo.tUmsatzBeleg
SET datevlink = @datevlink SET datevlink = @datevlink
WHERE kUmsatzBeleg = @kUmsatzBeleg AND (datevlink IS NULL OR datevlink = '') WHERE kUmsatzBeleg = @kUmsatzBeleg AND (datevlink IS NULL OR datevlink = '' OR datevlink = 'pending')
`; `;
const updateResult = await executeQuery(updateQuery, { const updateResult = await executeQuery(updateQuery, {
@@ -515,7 +577,7 @@ router.post('/import-datev-beleglinks', authenticateToken, async (req, res) => {
const updateQuery = ` const updateQuery = `
UPDATE eazybusiness.dbo.tPdfObjekt UPDATE eazybusiness.dbo.tPdfObjekt
SET datevlink = @datevlink SET datevlink = @datevlink
WHERE kPdfObjekt = @kPdfObjekt AND (datevlink IS NULL OR datevlink = '') WHERE kPdfObjekt = @kPdfObjekt AND (datevlink IS NULL OR datevlink = '' OR datevlink = 'pending')
`; `;
const updateResult = await executeQuery(updateQuery, { const updateResult = await executeQuery(updateQuery, {

View File

@@ -10,6 +10,25 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
const { timeRange } = req.params; const { timeRange } = req.params;
const { executeQuery } = require('../../config/database'); const { executeQuery } = require('../../config/database');
// Build WHERE clause based on timeRange format
let timeWhereClause = '';
if (timeRange.includes('-Q')) {
// Quarter format: 2025-Q2
const [year, quarterPart] = timeRange.split('-Q');
const quarter = parseInt(quarterPart, 10);
const startMonth = (quarter - 1) * 3 + 1;
const endMonth = startMonth + 2;
timeWhereClause = `WHERE YEAR(csv.parsed_date) = ${year} AND MONTH(csv.parsed_date) BETWEEN ${startMonth} AND ${endMonth}`;
} else if (timeRange.length === 4) {
// Year format: 2025
timeWhereClause = `WHERE YEAR(csv.parsed_date) = ${timeRange}`;
} else {
// Month format: 2025-07
const [year, month] = timeRange.split('-');
timeWhereClause = `WHERE YEAR(csv.parsed_date) = ${year} AND MONTH(csv.parsed_date) = ${parseInt(month, 10)}`;
}
const query = ` const query = `
SELECT SELECT
csv.id as id, csv.id as id,
@@ -47,6 +66,7 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
LEFT JOIN fibdash.Kreditor k ON csv.kontonummer_iban = k.iban LEFT JOIN fibdash.Kreditor k ON csv.kontonummer_iban = k.iban
LEFT JOIN fibdash.BankingAccountTransactions bat ON csv.id = bat.csv_transaction_id LEFT JOIN fibdash.BankingAccountTransactions bat ON csv.id = bat.csv_transaction_id
LEFT JOIN fibdash.Kreditor ak ON bat.assigned_kreditor_id = ak.id LEFT JOIN fibdash.Kreditor ak ON bat.assigned_kreditor_id = ak.id
${timeWhereClause}
UNION ALL UNION ALL
@@ -84,6 +104,12 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
WHERE ABS(csv.numeric_amount - jtl.fBetrag) < 0.01 WHERE ABS(csv.numeric_amount - jtl.fBetrag) < 0.01
AND ABS(DATEDIFF(day, csv.parsed_date, jtl.dBuchungsdatum)) <= 1 AND ABS(DATEDIFF(day, csv.parsed_date, jtl.dBuchungsdatum)) <= 1
) )
${timeRange.includes('-Q') ?
`AND YEAR(jtl.dBuchungsdatum) = ${timeRange.split('-Q')[0]} AND MONTH(jtl.dBuchungsdatum) BETWEEN ${(parseInt(timeRange.split('-Q')[1], 10) - 1) * 3 + 1} AND ${(parseInt(timeRange.split('-Q')[1], 10) - 1) * 3 + 3}` :
timeRange.length === 4 ?
`AND YEAR(jtl.dBuchungsdatum) = ${timeRange}` :
`AND YEAR(jtl.dBuchungsdatum) = ${timeRange.split('-')[0]} AND MONTH(jtl.dBuchungsdatum) = ${parseInt(timeRange.split('-')[1], 10)}`
}
ORDER BY parsed_date DESC ORDER BY parsed_date DESC
`; `;
@@ -163,213 +189,32 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
links: [...new Set(transaction.links.map(l => JSON.stringify(l)))].map(l => JSON.parse(l)) links: [...new Set(transaction.links.map(l => JSON.stringify(l)))].map(l => JSON.parse(l))
})); }));
let filteredTransactions = []; // Transactions are already filtered by the SQL query, so we just need to sort them
const monthTransactions = transactions
if (timeRange.includes('-Q')) {
const [year, quarterPart] = timeRange.split('-Q');
const quarter = parseInt(quarterPart, 10);
const startMonth = (quarter - 1) * 3 + 1;
const endMonth = startMonth + 2;
filteredTransactions = transactions.filter(t => {
if (!t.monthYear) return false;
const [tYear, tMonth] = t.monthYear.split('-');
const monthNum = parseInt(tMonth, 10);
return tYear === year && monthNum >= startMonth && monthNum <= endMonth;
});
} else if (timeRange.length === 4) {
filteredTransactions = transactions.filter(t => {
if (!t.monthYear) return false;
const [tYear] = t.monthYear.split('-');
return tYear === timeRange;
});
} else {
filteredTransactions = transactions.filter(t => t.monthYear === timeRange);
}
const monthTransactions = filteredTransactions
.sort((a, b) => b.parsedDate - a.parsedDate); .sort((a, b) => b.parsedDate - a.parsedDate);
// Get JTL transactions for comparison // Since transactions are already filtered and joined with JTL data in SQL,
let jtlTransactions = []; // we don't need the complex post-processing logic anymore
let jtlDatabaseAvailable = false;
try {
jtlTransactions = await getJTLTransactions();
jtlDatabaseAvailable = true;
console.log('DEBUG: JTL database connected, found', jtlTransactions.length, 'transactions');
} catch (error) {
console.log('JTL database not available, continuing without JTL data:', error.message);
jtlDatabaseAvailable = false;
}
// Filter JTL transactions for the selected time period
let jtlMonthTransactions = [];
if (timeRange.includes('-Q')) {
const [year, quarterPart] = timeRange.split('-Q');
const quarter = parseInt(quarterPart, 10);
const startMonth = (quarter - 1) * 3 + 1;
const endMonth = startMonth + 2;
jtlMonthTransactions = jtlTransactions.filter(jtl => {
const jtlDate = new Date(jtl.dBuchungsdatum);
const jtlMonth = jtlDate.getMonth() + 1;
return jtlDate.getFullYear() === parseInt(year, 10) &&
jtlMonth >= startMonth && jtlMonth <= endMonth;
});
} else if (timeRange.length === 4) {
jtlMonthTransactions = jtlTransactions.filter(jtl => {
const jtlDate = new Date(jtl.dBuchungsdatum);
return jtlDate.getFullYear() === parseInt(timeRange, 10);
});
} else {
const [year, month] = timeRange.split('-');
jtlMonthTransactions = jtlTransactions.filter(jtl => {
const jtlDate = new Date(jtl.dBuchungsdatum);
return jtlDate.getFullYear() === parseInt(year, 10) &&
jtlDate.getMonth() === parseInt(month, 10) - 1;
});
}
// Get Kreditor information for IBAN lookup
let kreditorData = [];
try {
const kreditorQuery = `SELECT id, iban, name, kreditorId, is_banking FROM fibdash.Kreditor`;
const kreditorResult = await executeQuery(kreditorQuery);
kreditorData = kreditorResult.recordset || [];
} catch (error) {
console.log('Kreditor database not available, continuing without Kreditor data');
}
// Add JTL status and Kreditor information to each CSV transaction
const transactionsWithJTL = monthTransactions.map((transaction, index) => {
const amount = transaction.numericAmount;
const transactionDate = transaction.parsedDate;
if (index === 0) {
console.log('DEBUG First CSV transaction:', {
amount: amount,
transactionDate: transactionDate,
jtlMonthTransactionsCount: jtlMonthTransactions.length
});
if (jtlMonthTransactions.length > 0) {
console.log('DEBUG First JTL transaction:', {
amount: parseFloat(jtlMonthTransactions[0].fBetrag),
date: new Date(jtlMonthTransactions[0].dBuchungsdatum)
});
}
}
const jtlMatch = jtlMonthTransactions.find(jtl => {
const jtlAmount = parseFloat(jtl.fBetrag) || 0;
const jtlDate = new Date(jtl.dBuchungsdatum);
const amountMatch = Math.abs(amount - jtlAmount) < 0.01;
const dateMatch = transactionDate && jtlDate &&
transactionDate.getFullYear() === jtlDate.getFullYear() &&
transactionDate.getMonth() === jtlDate.getMonth() &&
transactionDate.getDate() === jtlDate.getDate();
if (index === 0 && (amountMatch || dateMatch)) {
console.log('DEBUG Potential match for first transaction:', {
csvAmount: amount,
jtlAmount: jtlAmount,
amountMatch: amountMatch,
csvDate: transactionDate,
jtlDate: jtlDate,
dateMatch: dateMatch,
bothMatch: amountMatch && dateMatch
});
}
return amountMatch && dateMatch;
});
const transactionIban = transaction['Kontonummer/IBAN'];
const kreditorMatch = transactionIban ? kreditorData.find(k => k.iban === transactionIban) : null;
return {
...transaction,
hasJTL: jtlDatabaseAvailable ? !!jtlMatch : undefined,
jtlId: jtlMatch ? jtlMatch.kZahlungsabgleichUmsatz : null,
isFromCSV: true,
jtlDatabaseAvailable,
pdfs: jtlMatch ? jtlMatch.pdfs || [] : [],
links: jtlMatch ? jtlMatch.links || [] : [],
kreditor: kreditorMatch ? {
id: kreditorMatch.id,
name: kreditorMatch.name,
kreditorId: kreditorMatch.kreditorId,
iban: kreditorMatch.iban,
is_banking: Boolean(kreditorMatch.is_banking)
} : null,
hasKreditor: !!kreditorMatch
};
});
const unmatchedJTLTransactions = jtlMonthTransactions
.filter(jtl => {
const jtlAmount = parseFloat(jtl.fBetrag) || 0;
const jtlDate = new Date(jtl.dBuchungsdatum);
const hasCSVMatch = monthTransactions.some(transaction => {
const amount = transaction.numericAmount;
const transactionDate = transaction.parsedDate;
const amountMatch = Math.abs(amount - jtlAmount) < 0.01;
const dateMatch = transactionDate && jtlDate &&
transactionDate.getFullYear() === jtlDate.getFullYear() &&
transactionDate.getMonth() === jtlDate.getMonth() &&
transactionDate.getDate() === jtlDate.getDate();
return amountMatch && dateMatch;
});
return !hasCSVMatch;
})
.map(jtl => ({
'Buchungstag': new Date(jtl.dBuchungsdatum).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: '2-digit'
}),
'Verwendungszweck': jtl.cVerwendungszweck || '',
'Buchungstext': 'JTL Transaction',
'Beguenstigter/Zahlungspflichtiger': jtl.cName || '',
'Kontonummer/IBAN': '',
'Betrag': jtl.fBetrag ? jtl.fBetrag.toString().replace('.', ',') : '0,00',
numericAmount: parseFloat(jtl.fBetrag) || 0,
parsedDate: new Date(jtl.dBuchungsdatum),
monthYear: timeRange,
hasJTL: true,
jtlId: jtl.kZahlungsabgleichUmsatz,
isFromCSV: false,
isJTLOnly: true,
pdfs: jtl.pdfs || [],
links: jtl.links || [],
kreditor: null,
hasKreditor: false
}));
const summary = { const summary = {
totalTransactions: filteredTransactions.length, totalTransactions: transactions.length,
totalIncome: filteredTransactions totalIncome: transactions
.filter(t => t.numericAmount > 0) .filter(t => t.numericAmount > 0)
.reduce((sum, t) => sum + t.numericAmount, 0), .reduce((sum, t) => sum + t.numericAmount, 0),
totalExpenses: filteredTransactions totalExpenses: transactions
.filter(t => t.numericAmount < 0) .filter(t => t.numericAmount < 0)
.reduce((sum, t) => sum + Math.abs(t.numericAmount), 0), .reduce((sum, t) => sum + Math.abs(t.numericAmount), 0),
netAmount: filteredTransactions.reduce((sum, t) => sum + t.numericAmount, 0), netAmount: transactions.reduce((sum, t) => sum + t.numericAmount, 0),
timeRange: timeRange, timeRange: timeRange,
jtlDatabaseAvailable: true, jtlDatabaseAvailable: true,
jtlMatches: filteredTransactions.filter(t => t.hasJTL === true && t.isFromCSV).length, jtlMatches: transactions.filter(t => t.hasJTL === true && t.isFromCSV).length,
jtlMissing: filteredTransactions.filter(t => t.hasJTL === false && t.isFromCSV).length, jtlMissing: transactions.filter(t => t.hasJTL === false && t.isFromCSV).length,
jtlOnly: filteredTransactions.filter(t => t.isJTLOnly === true).length, jtlOnly: transactions.filter(t => t.isJTLOnly === true).length,
csvOnly: filteredTransactions.filter(t => t.hasJTL === false && t.isFromCSV).length csvOnly: transactions.filter(t => t.hasJTL === false && t.isFromCSV).length
}; };
res.json({ res.json({
transactions: filteredTransactions, transactions: transactions,
summary summary
}); });
} catch (error) { } catch (error) {