update
This commit is contained in:
parent
451f6f66c1
commit
444e8555f3
114
server.js
114
server.js
|
|
@ -1171,55 +1171,28 @@ app.post('/api/invoices/:id/export', async (req, res) => {
|
|||
: 'https://sandbox-quickbooks.api.intuit.com';
|
||||
|
||||
|
||||
// -------------------------------------------
|
||||
// --- SCHRITT 3a: NÄCHSTE NUMMER ERMITTELN ---
|
||||
console.log("🔍 Frage QBO nach der höchsten Rechnungsnummer...");
|
||||
// --- SCHRITT 3a: NÄCHSTE NUMMER ERMITTELN (lokal) ---
|
||||
// Wir nehmen das Maximum aus qbo_doc_number UND invoice_number
|
||||
const maxNumResult = await client.query(`
|
||||
SELECT GREATEST(
|
||||
COALESCE((SELECT MAX(CAST(qbo_doc_number AS INTEGER)) FROM invoices WHERE qbo_doc_number ~ '^[0-9]+$'), 0),
|
||||
COALESCE((SELECT MAX(CAST(invoice_number AS INTEGER)) FROM invoices WHERE invoice_number ~ '^[0-9]+$'), 0)
|
||||
) as max_num
|
||||
`);
|
||||
let nextDocNumber = (parseInt(maxNumResult.rows[0].max_num) + 1).toString();
|
||||
console.log(`✅ Nächste Nummer (aus lokaler DB): ${nextDocNumber}`);
|
||||
|
||||
// Alle Rechnungen laden (nur DocNumber Feld), nach DocNumber absteigend
|
||||
// QBO unterstützt leider kein MAX() oder CAST, daher holen wir die letzten 100
|
||||
// und ermitteln die höchste rein numerische Nummer clientseitig.
|
||||
const numQuery = "SELECT DocNumber FROM Invoice ORDERBY DocNumber DESC MAXRESULTS 100";
|
||||
const lastNumResponse = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(numQuery)}`,
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
const lastNumData = lastNumResponse.getJson ? lastNumResponse.getJson() : lastNumResponse.json;
|
||||
let nextDocNumber = null;
|
||||
|
||||
if (lastNumData.QueryResponse?.Invoice?.length > 0) {
|
||||
// Nur rein numerische DocNumbers betrachten (keine "110444-A" etc.)
|
||||
const numericDocs = lastNumData.QueryResponse.Invoice
|
||||
.map(inv => inv.DocNumber)
|
||||
.filter(dn => /^\d+$/.test(dn))
|
||||
.map(dn => parseInt(dn, 10))
|
||||
.sort((a, b) => b - a); // Absteigend
|
||||
|
||||
if (numericDocs.length > 0) {
|
||||
const highest = numericDocs[0];
|
||||
nextDocNumber = (highest + 1).toString();
|
||||
console.log(`✅ Höchste numerische Nummer: ${highest}. Neue Nummer: ${nextDocNumber}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if (!nextDocNumber) {
|
||||
console.log("⚠️ Konnte keine Nummer aus QBO ermitteln. Verwende lokale Nummer.");
|
||||
nextDocNumber = invoice.invoice_number;
|
||||
}
|
||||
|
||||
// 4. QBO JSON bauen
|
||||
const lineItems = items.map(item => {
|
||||
const rate = parseFloat(item.rate.replace(/[^0-9.]/g, '')) || 0;
|
||||
const amount = parseFloat(item.amount.replace(/[^0-9.]/g, '')) || 0;
|
||||
|
||||
const itemRefId = item.qbo_item_id || QBO_PARTS_ID;
|
||||
const itemRefId = item.qbo_item_id || QBO_PARTS_ID;
|
||||
const itemRefName = itemRefId == QBO_LABOR_ID ? "Labor:Labor" : "Parts:Parts";
|
||||
|
||||
return {
|
||||
"DetailType": "SalesItemLineDetail",
|
||||
"Amount": amount,
|
||||
"Description": item.description,
|
||||
"Description": item.description,
|
||||
"SalesItemLineDetail": {
|
||||
"ItemRef": { "value": itemRefId, "name": itemRefName },
|
||||
"UnitPrice": rate,
|
||||
|
|
@ -1230,49 +1203,62 @@ app.post('/api/invoices/:id/export', async (req, res) => {
|
|||
|
||||
const qboInvoicePayload = {
|
||||
"CustomerRef": { "value": invoice.customer_qbo_id },
|
||||
|
||||
// HIER SETZEN WIR DIE ERMITTELTE NUMMER EIN
|
||||
"DocNumber": nextDocNumber,
|
||||
|
||||
"TxnDate": invoice.invoice_date.toISOString().split('T')[0],
|
||||
"Line": lineItems,
|
||||
"CustomerMemo": { "value": invoice.auth_code ? `Auth: ${invoice.auth_code}` : "" },
|
||||
|
||||
// Status auf "Verschickt" setzen
|
||||
"EmailStatus": "EmailSent",
|
||||
"BillEmail": { "Address": invoice.email || "" }
|
||||
};
|
||||
|
||||
console.log(`📤 Sende Rechnung an QBO (DocNumber: ${nextDocNumber})...`);
|
||||
|
||||
const createResponse = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/invoice`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(qboInvoicePayload)
|
||||
});
|
||||
// 5. An QBO senden — mit Retry bei Duplicate
|
||||
let qboInvoice = null;
|
||||
const MAX_RETRIES = 5;
|
||||
|
||||
const responseData = createResponse.getJson ? createResponse.getJson() : createResponse.json;
|
||||
// Check auf Unterobjekt "Invoice"
|
||||
const qboInvoice = responseData.Invoice || responseData;
|
||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||
console.log(`📤 Sende Rechnung an QBO (DocNumber: ${qboInvoicePayload.DocNumber})...`);
|
||||
|
||||
console.log("🔍 FULL QBO RESPONSE (ID):", qboInvoice.Id);
|
||||
const createResponse = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/invoice`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(qboInvoicePayload)
|
||||
});
|
||||
|
||||
if (!qboInvoice.Id) {
|
||||
console.error("FULL RESPONSE DUMP:", JSON.stringify(responseData, null, 2));
|
||||
throw new Error("QBO hat keine ID zurückgegeben.");
|
||||
const responseData = createResponse.getJson ? createResponse.getJson() : createResponse.json;
|
||||
|
||||
// Prüfe auf Duplicate Error (code 6140)
|
||||
if (responseData.Fault?.Error?.[0]?.code === '6140') {
|
||||
const oldNum = parseInt(qboInvoicePayload.DocNumber);
|
||||
qboInvoicePayload.DocNumber = (oldNum + 1).toString();
|
||||
console.log(`⚠️ DocNumber ${oldNum} existiert bereits. Versuche ${qboInvoicePayload.DocNumber}...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
qboInvoice = responseData.Invoice || responseData;
|
||||
|
||||
if (qboInvoice.Id) {
|
||||
break; // Erfolg!
|
||||
} else {
|
||||
console.error("FULL RESPONSE DUMP:", JSON.stringify(responseData, null, 2));
|
||||
throw new Error("QBO hat keine ID zurückgegeben: " +
|
||||
(responseData.Fault?.Error?.[0]?.Message || JSON.stringify(responseData)));
|
||||
}
|
||||
}
|
||||
|
||||
if (!qboInvoice || !qboInvoice.Id) {
|
||||
throw new Error(`Konnte nach ${MAX_RETRIES} Versuchen keine freie DocNumber finden.`);
|
||||
}
|
||||
|
||||
console.log(`✅ QBO Rechnung erstellt! ID: ${qboInvoice.Id}, DocNumber: ${qboInvoice.DocNumber}`);
|
||||
|
||||
// 6. DB Update: Wir speichern AUCH die QBO-Nummer, damit wir wissen, wie sie drüben heißt
|
||||
// 6. DB Update
|
||||
await client.query(
|
||||
`UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3, invoice_number = $3 WHERE id = $4`,
|
||||
[qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, id]
|
||||
`UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3, invoice_number = $4 WHERE id = $5`,
|
||||
[qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, qboInvoice.DocNumber, id]
|
||||
);
|
||||
|
||||
// Wir geben die neue Nummer zurück, damit das Frontend Bescheid weiß
|
||||
res.json({ success: true, qbo_id: qboInvoice.Id, qbo_doc_number: qboInvoice.DocNumber });
|
||||
res.json({ success: true, qbo_id: qboInvoice.Id, qbo_doc_number: qboInvoice.DocNumber });
|
||||
|
||||
} catch (error) {
|
||||
console.error("QBO Export Error:", error);
|
||||
|
|
|
|||
Loading…
Reference in New Issue