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';
|
: 'https://sandbox-quickbooks.api.intuit.com';
|
||||||
|
|
||||||
|
|
||||||
// -------------------------------------------
|
// --- SCHRITT 3a: NÄCHSTE NUMMER ERMITTELN (lokal) ---
|
||||||
// --- SCHRITT 3a: NÄCHSTE NUMMER ERMITTELN ---
|
// Wir nehmen das Maximum aus qbo_doc_number UND invoice_number
|
||||||
console.log("🔍 Frage QBO nach der höchsten Rechnungsnummer...");
|
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
|
// 4. QBO JSON bauen
|
||||||
const lineItems = items.map(item => {
|
const lineItems = items.map(item => {
|
||||||
const rate = parseFloat(item.rate.replace(/[^0-9.]/g, '')) || 0;
|
const rate = parseFloat(item.rate.replace(/[^0-9.]/g, '')) || 0;
|
||||||
const amount = parseFloat(item.amount.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";
|
const itemRefName = itemRefId == QBO_LABOR_ID ? "Labor:Labor" : "Parts:Parts";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"DetailType": "SalesItemLineDetail",
|
"DetailType": "SalesItemLineDetail",
|
||||||
"Amount": amount,
|
"Amount": amount,
|
||||||
"Description": item.description,
|
"Description": item.description,
|
||||||
"SalesItemLineDetail": {
|
"SalesItemLineDetail": {
|
||||||
"ItemRef": { "value": itemRefId, "name": itemRefName },
|
"ItemRef": { "value": itemRefId, "name": itemRefName },
|
||||||
"UnitPrice": rate,
|
"UnitPrice": rate,
|
||||||
|
|
@ -1230,49 +1203,62 @@ app.post('/api/invoices/:id/export', async (req, res) => {
|
||||||
|
|
||||||
const qboInvoicePayload = {
|
const qboInvoicePayload = {
|
||||||
"CustomerRef": { "value": invoice.customer_qbo_id },
|
"CustomerRef": { "value": invoice.customer_qbo_id },
|
||||||
|
|
||||||
// HIER SETZEN WIR DIE ERMITTELTE NUMMER EIN
|
|
||||||
"DocNumber": nextDocNumber,
|
"DocNumber": nextDocNumber,
|
||||||
|
|
||||||
"TxnDate": invoice.invoice_date.toISOString().split('T')[0],
|
"TxnDate": invoice.invoice_date.toISOString().split('T')[0],
|
||||||
"Line": lineItems,
|
"Line": lineItems,
|
||||||
"CustomerMemo": { "value": invoice.auth_code ? `Auth: ${invoice.auth_code}` : "" },
|
"CustomerMemo": { "value": invoice.auth_code ? `Auth: ${invoice.auth_code}` : "" },
|
||||||
|
|
||||||
// Status auf "Verschickt" setzen
|
|
||||||
"EmailStatus": "EmailSent",
|
"EmailStatus": "EmailSent",
|
||||||
"BillEmail": { "Address": invoice.email || "" }
|
"BillEmail": { "Address": invoice.email || "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`📤 Sende Rechnung an QBO (DocNumber: ${nextDocNumber})...`);
|
// 5. An QBO senden — mit Retry bei Duplicate
|
||||||
|
let qboInvoice = null;
|
||||||
const createResponse = await makeQboApiCall({
|
const MAX_RETRIES = 5;
|
||||||
url: `${baseUrl}/v3/company/${companyId}/invoice`,
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(qboInvoicePayload)
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseData = createResponse.getJson ? createResponse.getJson() : createResponse.json;
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||||
// Check auf Unterobjekt "Invoice"
|
console.log(`📤 Sende Rechnung an QBO (DocNumber: ${qboInvoicePayload.DocNumber})...`);
|
||||||
const qboInvoice = responseData.Invoice || responseData;
|
|
||||||
|
|
||||||
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) {
|
const responseData = createResponse.getJson ? createResponse.getJson() : createResponse.json;
|
||||||
console.error("FULL RESPONSE DUMP:", JSON.stringify(responseData, null, 2));
|
|
||||||
throw new Error("QBO hat keine ID zurückgegeben.");
|
// 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}`);
|
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(
|
await client.query(
|
||||||
`UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3, invoice_number = $3 WHERE id = $4`,
|
`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, id]
|
[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) {
|
} catch (error) {
|
||||||
console.error("QBO Export Error:", error);
|
console.error("QBO Export Error:", error);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue