update
This commit is contained in:
180
server.js
180
server.js
@@ -1354,6 +1354,186 @@ app.get('/api/qbo/status', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/qbo/import-unpaid', async (req, res) => {
|
||||
const dbClient = await pool.connect();
|
||||
|
||||
try {
|
||||
const oauthClient = getOAuthClient();
|
||||
const companyId = oauthClient.getToken().realmId;
|
||||
const baseUrl = process.env.QBO_ENVIRONMENT === 'production'
|
||||
? 'https://quickbooks.api.intuit.com'
|
||||
: 'https://sandbox-quickbooks.api.intuit.com';
|
||||
|
||||
// 1. Alle unbezahlten Rechnungen aus QBO holen
|
||||
// Balance > '0' = noch nicht vollständig bezahlt
|
||||
// MAXRESULTS 1000 = sicherheitshalber hoch setzen
|
||||
console.log('📥 QBO Import: Lade unbezahlte Rechnungen...');
|
||||
|
||||
const query = "SELECT * FROM Invoice WHERE Balance > '0' ORDERBY DocNumber ASC MAXRESULTS 1000";
|
||||
const response = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(query)}`,
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
const data = response.getJson ? response.getJson() : response.json;
|
||||
const qboInvoices = data.QueryResponse?.Invoice || [];
|
||||
|
||||
console.log(`📋 ${qboInvoices.length} unbezahlte Rechnungen in QBO gefunden.`);
|
||||
|
||||
if (qboInvoices.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
imported: 0,
|
||||
skipped: 0,
|
||||
skippedNoCustomer: 0,
|
||||
message: 'Keine unbezahlten Rechnungen in QBO gefunden.'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Lokale Kunden laden (die mit QBO verknüpft sind)
|
||||
const customersResult = await dbClient.query(
|
||||
'SELECT id, qbo_id, name, taxable FROM customers WHERE qbo_id IS NOT NULL'
|
||||
);
|
||||
const customerMap = new Map();
|
||||
customersResult.rows.forEach(c => customerMap.set(c.qbo_id, c));
|
||||
|
||||
// 3. Bereits importierte QBO-Rechnungen ermitteln (nach qbo_id)
|
||||
const existingResult = await dbClient.query(
|
||||
'SELECT qbo_id FROM invoices WHERE qbo_id IS NOT NULL'
|
||||
);
|
||||
const existingQboIds = new Set(existingResult.rows.map(r => r.qbo_id));
|
||||
|
||||
// 4. Import durchführen
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
let skippedNoCustomer = 0;
|
||||
const skippedCustomerNames = [];
|
||||
|
||||
await dbClient.query('BEGIN');
|
||||
|
||||
for (const qboInv of qboInvoices) {
|
||||
const qboId = String(qboInv.Id);
|
||||
|
||||
// Bereits importiert? → Überspringen
|
||||
if (existingQboIds.has(qboId)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Kunde lokal vorhanden?
|
||||
const customerQboId = String(qboInv.CustomerRef?.value || '');
|
||||
const localCustomer = customerMap.get(customerQboId);
|
||||
|
||||
if (!localCustomer) {
|
||||
skippedNoCustomer++;
|
||||
const custName = qboInv.CustomerRef?.name || 'Unbekannt';
|
||||
if (!skippedCustomerNames.includes(custName)) {
|
||||
skippedCustomerNames.push(custName);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Werte aus QBO-Rechnung extrahieren
|
||||
const docNumber = qboInv.DocNumber || '';
|
||||
const txnDate = qboInv.TxnDate || new Date().toISOString().split('T')[0];
|
||||
const syncToken = qboInv.SyncToken || '';
|
||||
|
||||
// Terms aus QBO mappen (SalesTermRef)
|
||||
let terms = 'Net 30';
|
||||
if (qboInv.SalesTermRef?.name) {
|
||||
terms = qboInv.SalesTermRef.name;
|
||||
}
|
||||
|
||||
// Tax: Prüfen ob TaxLine vorhanden
|
||||
const taxAmount = qboInv.TxnTaxDetail?.TotalTax || 0;
|
||||
const taxExempt = taxAmount === 0;
|
||||
|
||||
// Subtotal berechnen (Total - Tax)
|
||||
const total = parseFloat(qboInv.TotalAmt) || 0;
|
||||
const subtotal = total - taxAmount;
|
||||
const taxRate = subtotal > 0 && !taxExempt ? (taxAmount / subtotal * 100) : 8.25;
|
||||
|
||||
// Memo als auth_code (falls vorhanden)
|
||||
const authCode = qboInv.CustomerMemo?.value || '';
|
||||
|
||||
// Rechnung einfügen
|
||||
const invoiceResult = await dbClient.query(
|
||||
`INSERT INTO invoices
|
||||
(invoice_number, customer_id, invoice_date, terms, auth_code,
|
||||
tax_exempt, tax_rate, subtotal, tax_amount, total,
|
||||
qbo_id, qbo_sync_token, qbo_doc_number)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
RETURNING id`,
|
||||
[docNumber, localCustomer.id, txnDate, terms, authCode,
|
||||
taxExempt, taxRate, subtotal, taxAmount, total,
|
||||
qboId, syncToken, docNumber]
|
||||
);
|
||||
|
||||
const localInvoiceId = invoiceResult.rows[0].id;
|
||||
|
||||
// Line Items importieren
|
||||
const lines = qboInv.Line || [];
|
||||
let itemOrder = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
// Nur SalesItemLineDetail-Zeilen (keine SubTotalLine etc.)
|
||||
if (line.DetailType !== 'SalesItemLineDetail') continue;
|
||||
|
||||
const detail = line.SalesItemLineDetail || {};
|
||||
const qty = String(detail.Qty || 1);
|
||||
const rate = String(detail.UnitPrice || 0);
|
||||
const amount = String(line.Amount || 0);
|
||||
const description = line.Description || '';
|
||||
|
||||
// Item-Typ ermitteln (Labor=5, Parts=9)
|
||||
const itemRefValue = detail.ItemRef?.value || '9';
|
||||
const itemRefName = (detail.ItemRef?.name || '').toLowerCase();
|
||||
let qboItemId = '9'; // Default: Parts
|
||||
if (itemRefValue === '5' || itemRefName.includes('labor')) {
|
||||
qboItemId = '5';
|
||||
}
|
||||
|
||||
await dbClient.query(
|
||||
`INSERT INTO invoice_items
|
||||
(invoice_id, quantity, description, rate, amount, item_order, qbo_item_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[localInvoiceId, qty, description, rate, amount, itemOrder, qboItemId]
|
||||
);
|
||||
itemOrder++;
|
||||
}
|
||||
|
||||
imported++;
|
||||
console.log(` ✅ Importiert: #${docNumber} (${localCustomer.name}) - $${total}`);
|
||||
}
|
||||
|
||||
await dbClient.query('COMMIT');
|
||||
|
||||
const message = [
|
||||
`${imported} Rechnungen importiert.`,
|
||||
skipped > 0 ? `${skipped} bereits vorhanden (übersprungen).` : '',
|
||||
skippedNoCustomer > 0 ? `${skippedNoCustomer} übersprungen (Kunde nicht verknüpft: ${skippedCustomerNames.join(', ')}).` : ''
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
console.log(`📥 QBO Import abgeschlossen: ${message}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
imported,
|
||||
skipped,
|
||||
skippedNoCustomer,
|
||||
skippedCustomerNames,
|
||||
message
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await dbClient.query('ROLLBACK');
|
||||
console.error('❌ QBO Import Error:', error);
|
||||
res.status(500).json({ error: 'Import fehlgeschlagen: ' + error.message });
|
||||
} finally {
|
||||
dbClient.release();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Start server and browser
|
||||
|
||||
Reference in New Issue
Block a user