fix
This commit is contained in:
parent
b5ac7f0807
commit
ab2f064de9
383
server.js
383
server.js
|
|
@ -10,9 +10,6 @@ const { makeQboApiCall, getOAuthClient, saveTokens, resetOAuthClient } = require
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
const QBO_LABOR_ID = '5';
|
|
||||||
const QBO_PARTS_ID = '9';
|
|
||||||
|
|
||||||
// Global browser instance
|
// Global browser instance
|
||||||
let browser = null;
|
let browser = null;
|
||||||
|
|
||||||
|
|
@ -124,6 +121,15 @@ async function getNextInvoiceNumber() {
|
||||||
return String(parseInt(result.rows[0].max_number) + 1);
|
return String(parseInt(result.rows[0].max_number) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// INVOICE CREATE + UPDATE — Auto QBO Export/Sync
|
||||||
|
// ERSETZE POST /api/invoices und PUT /api/invoices/:id
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
const QBO_LABOR_ID = '5';
|
||||||
|
const QBO_PARTS_ID = '9';
|
||||||
|
|
||||||
|
|
||||||
// --- Helper: QBO Invoice Export (create) ---
|
// --- Helper: QBO Invoice Export (create) ---
|
||||||
async function exportInvoiceToQbo(invoiceId, client) {
|
async function exportInvoiceToQbo(invoiceId, client) {
|
||||||
const invoiceRes = await client.query(`
|
const invoiceRes = await client.query(`
|
||||||
|
|
@ -155,8 +161,14 @@ async function exportInvoiceToQbo(invoiceId, client) {
|
||||||
let nextDocNumber = (parseInt(maxNumResult.rows[0].max_num) + 1).toString();
|
let nextDocNumber = (parseInt(maxNumResult.rows[0].max_num) + 1).toString();
|
||||||
|
|
||||||
const lineItems = items.map(item => {
|
const lineItems = items.map(item => {
|
||||||
const rate = parseFloat(item.rate.replace(/[^0-9.]/g, '')) || 0;
|
const parseNum = (val) => {
|
||||||
const amount = parseFloat(item.amount.replace(/[^0-9.]/g, '')) || 0;
|
if (val === null || val === undefined) return 0;
|
||||||
|
if (typeof val === 'number') return val;
|
||||||
|
return parseFloat(String(val).replace(/[^0-9.\-]/g, '')) || 0;
|
||||||
|
};
|
||||||
|
const rate = parseNum(item.rate);
|
||||||
|
const qty = parseNum(item.quantity) || 1;
|
||||||
|
const amount = rate * qty;
|
||||||
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 {
|
||||||
|
|
@ -166,7 +178,7 @@ async function exportInvoiceToQbo(invoiceId, client) {
|
||||||
"SalesItemLineDetail": {
|
"SalesItemLineDetail": {
|
||||||
"ItemRef": { "value": itemRefId, "name": itemRefName },
|
"ItemRef": { "value": itemRefId, "name": itemRefName },
|
||||||
"UnitPrice": rate,
|
"UnitPrice": rate,
|
||||||
"Qty": parseFloat(item.quantity) || 1
|
"Qty": qty
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -250,10 +262,20 @@ async function syncInvoiceToQbo(invoiceId, client) {
|
||||||
if (currentSyncToken === undefined) throw new Error('Could not get SyncToken from QBO');
|
if (currentSyncToken === undefined) throw new Error('Could not get SyncToken from QBO');
|
||||||
|
|
||||||
const lineItems = itemsRes.rows.map(item => {
|
const lineItems = itemsRes.rows.map(item => {
|
||||||
const rate = parseFloat(item.rate.replace(/[^0-9.]/g, '')) || 0;
|
// Robust parsing: handle both string ("$1,250.00") and numeric types
|
||||||
const amount = parseFloat(item.amount.replace(/[^0-9.]/g, '')) || 0;
|
const parseNum = (val) => {
|
||||||
|
if (val === null || val === undefined) return 0;
|
||||||
|
if (typeof val === 'number') return val;
|
||||||
|
return parseFloat(String(val).replace(/[^0-9.\-]/g, '')) || 0;
|
||||||
|
};
|
||||||
|
const rate = parseNum(item.rate);
|
||||||
|
const qty = parseNum(item.quantity) || 1;
|
||||||
|
const amount = rate * qty; // Always compute amount = rate * qty for QBO
|
||||||
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";
|
||||||
|
|
||||||
|
console.log(` 📋 Item: qty=${qty}, rate=${rate}, amount=${amount}, ref=${itemRefId}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"DetailType": "SalesItemLineDetail",
|
"DetailType": "SalesItemLineDetail",
|
||||||
"Amount": amount,
|
"Amount": amount,
|
||||||
|
|
@ -309,6 +331,183 @@ async function syncInvoiceToQbo(invoiceId, client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// POST /api/invoices — Create + Auto QBO Export
|
||||||
|
// =====================================================
|
||||||
|
app.post('/api/invoices', async (req, res) => {
|
||||||
|
const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, scheduled_send_date, created_from_quote_id } = req.body;
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// invoice_number kann leer sein — wird von QBO vergeben
|
||||||
|
// Falls angegeben, validieren
|
||||||
|
if (invoice_number && !/^\d+$/.test(invoice_number)) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ error: 'Invoice number must be numeric.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporäre Nummer falls leer
|
||||||
|
const tempNumber = invoice_number || `DRAFT-${Date.now()}`;
|
||||||
|
|
||||||
|
if (invoice_number) {
|
||||||
|
const existing = await client.query('SELECT id FROM invoices WHERE invoice_number = $1', [invoice_number]);
|
||||||
|
if (existing.rows.length > 0) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subtotal = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
const amount = parseFloat(item.amount.replace(/[$,]/g, ''));
|
||||||
|
if (!isNaN(amount)) subtotal += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tax_rate = 8.25;
|
||||||
|
const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100);
|
||||||
|
const total = subtotal + tax_amount;
|
||||||
|
|
||||||
|
const invoiceResult = await client.query(
|
||||||
|
`INSERT INTO invoices (invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date, created_from_quote_id)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`,
|
||||||
|
[tempNumber, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, created_from_quote_id]
|
||||||
|
);
|
||||||
|
const invoiceId = invoiceResult.rows[0].id;
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order, qbo_item_id) VALUES ($1, $2, $3, $4, $5, $6, $7)',
|
||||||
|
[invoiceId, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i, items[i].qbo_item_id || '9']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
|
||||||
|
// Auto QBO Export (falls Kunde in QBO)
|
||||||
|
let qboResult = null;
|
||||||
|
try {
|
||||||
|
qboResult = await exportInvoiceToQbo(invoiceId, client);
|
||||||
|
if (qboResult.skipped) {
|
||||||
|
console.log(`ℹ️ Invoice ${invoiceId} not exported to QBO: ${qboResult.reason}`);
|
||||||
|
}
|
||||||
|
} catch (qboErr) {
|
||||||
|
console.error(`⚠️ Auto QBO export failed for Invoice ${invoiceId}:`, qboErr.message);
|
||||||
|
// Nicht abbrechen — lokal wurde gespeichert
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
...invoiceResult.rows[0],
|
||||||
|
qbo_id: qboResult?.qbo_id || null,
|
||||||
|
qbo_doc_number: qboResult?.qbo_doc_number || null
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
console.error('Error creating invoice:', error);
|
||||||
|
res.status(500).json({ error: 'Error creating invoice' });
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// PUT /api/invoices/:id — Update + Auto QBO Sync
|
||||||
|
// =====================================================
|
||||||
|
app.put('/api/invoices/:id', async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, scheduled_send_date } = req.body;
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// Invoice-Nummer validieren (falls angegeben)
|
||||||
|
if (invoice_number && !/^\d+$/.test(invoice_number)) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ error: 'Invoice number must be numeric.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoice_number) {
|
||||||
|
const existing = await client.query('SELECT id FROM invoices WHERE invoice_number = $1 AND id != $2', [invoice_number, id]);
|
||||||
|
if (existing.rows.length > 0) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subtotal = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
const amount = parseFloat(item.amount.replace(/[$,]/g, ''));
|
||||||
|
if (!isNaN(amount)) subtotal += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tax_rate = 8.25;
|
||||||
|
const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100);
|
||||||
|
const total = subtotal + tax_amount;
|
||||||
|
|
||||||
|
// Update lokal — invoice_number nur ändern wenn angegeben
|
||||||
|
if (invoice_number) {
|
||||||
|
await client.query(
|
||||||
|
`UPDATE invoices SET invoice_number = $1, customer_id = $2, invoice_date = $3, terms = $4, auth_code = $5, tax_exempt = $6,
|
||||||
|
tax_rate = $7, subtotal = $8, tax_amount = $9, total = $10, scheduled_send_date = $11, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $12`,
|
||||||
|
[invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await client.query(
|
||||||
|
`UPDATE invoices SET customer_id = $1, invoice_date = $2, terms = $3, auth_code = $4, tax_exempt = $5,
|
||||||
|
tax_rate = $6, subtotal = $7, tax_amount = $8, total = $9, scheduled_send_date = $10, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $11`,
|
||||||
|
[customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items neu schreiben
|
||||||
|
await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]);
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order, qbo_item_id) VALUES ($1, $2, $3, $4, $5, $6, $7)',
|
||||||
|
[id, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i, items[i].qbo_item_id || '9']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
|
||||||
|
// Auto QBO: Export if not yet in QBO, Sync if already in QBO
|
||||||
|
let qboResult = null;
|
||||||
|
try {
|
||||||
|
// Prüfe ob Invoice schon in QBO
|
||||||
|
const checkRes = await client.query('SELECT qbo_id FROM invoices WHERE id = $1', [id]);
|
||||||
|
const hasQboId = !!checkRes.rows[0]?.qbo_id;
|
||||||
|
|
||||||
|
if (hasQboId) {
|
||||||
|
qboResult = await syncInvoiceToQbo(id, client);
|
||||||
|
} else {
|
||||||
|
qboResult = await exportInvoiceToQbo(id, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qboResult.skipped) {
|
||||||
|
console.log(`ℹ️ Invoice ${id}: ${qboResult.reason}`);
|
||||||
|
}
|
||||||
|
} catch (qboErr) {
|
||||||
|
console.error(`⚠️ Auto QBO failed for Invoice ${id}:`, qboErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, qbo_synced: !!qboResult?.success, qbo_id: qboResult?.qbo_id || null, qbo_doc_number: qboResult?.qbo_doc_number || null });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
console.error('Error updating invoice:', error);
|
||||||
|
res.status(500).json({ error: 'Error updating invoice' });
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Logo endpoints
|
// Logo endpoints
|
||||||
app.get('/api/logo-info', async (req, res) => {
|
app.get('/api/logo-info', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -771,85 +970,6 @@ app.get('/api/invoices/:id', async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.post('/api/invoices', async (req, res) => {
|
|
||||||
const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, scheduled_send_date, created_from_quote_id } = req.body;
|
|
||||||
|
|
||||||
const client = await pool.connect();
|
|
||||||
try {
|
|
||||||
await client.query('BEGIN');
|
|
||||||
|
|
||||||
// invoice_number kann leer sein — wird von QBO vergeben
|
|
||||||
// Falls angegeben, validieren
|
|
||||||
if (invoice_number && !/^\d+$/.test(invoice_number)) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
return res.status(400).json({ error: 'Invoice number must be numeric.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporäre Nummer falls leer
|
|
||||||
const tempNumber = invoice_number || `DRAFT-${Date.now()}`;
|
|
||||||
|
|
||||||
if (invoice_number) {
|
|
||||||
const existing = await client.query('SELECT id FROM invoices WHERE invoice_number = $1', [invoice_number]);
|
|
||||||
if (existing.rows.length > 0) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let subtotal = 0;
|
|
||||||
for (const item of items) {
|
|
||||||
const amount = parseFloat(item.amount.replace(/[$,]/g, ''));
|
|
||||||
if (!isNaN(amount)) subtotal += amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tax_rate = 8.25;
|
|
||||||
const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100);
|
|
||||||
const total = subtotal + tax_amount;
|
|
||||||
|
|
||||||
const invoiceResult = await client.query(
|
|
||||||
`INSERT INTO invoices (invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date, created_from_quote_id)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`,
|
|
||||||
[tempNumber, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, created_from_quote_id]
|
|
||||||
);
|
|
||||||
const invoiceId = invoiceResult.rows[0].id;
|
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
await client.query(
|
|
||||||
'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order, qbo_item_id) VALUES ($1, $2, $3, $4, $5, $6, $7)',
|
|
||||||
[invoiceId, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i, items[i].qbo_item_id || '9']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.query('COMMIT');
|
|
||||||
|
|
||||||
// Auto QBO Export (falls Kunde in QBO)
|
|
||||||
let qboResult = null;
|
|
||||||
try {
|
|
||||||
qboResult = await exportInvoiceToQbo(invoiceId, client);
|
|
||||||
if (qboResult.skipped) {
|
|
||||||
console.log(`ℹ️ Invoice ${invoiceId} not exported to QBO: ${qboResult.reason}`);
|
|
||||||
}
|
|
||||||
} catch (qboErr) {
|
|
||||||
console.error(`⚠️ Auto QBO export failed for Invoice ${invoiceId}:`, qboErr.message);
|
|
||||||
// Nicht abbrechen — lokal wurde gespeichert
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
...invoiceResult.rows[0],
|
|
||||||
qbo_id: qboResult?.qbo_id || null,
|
|
||||||
qbo_doc_number: qboResult?.qbo_doc_number || null
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
console.error('Error creating invoice:', error);
|
|
||||||
res.status(500).json({ error: 'Error creating invoice' });
|
|
||||||
} finally {
|
|
||||||
client.release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => {
|
app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
|
|
@ -916,96 +1036,7 @@ app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put('/api/invoices/:id', async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, scheduled_send_date } = req.body;
|
|
||||||
|
|
||||||
const client = await pool.connect();
|
|
||||||
try {
|
|
||||||
await client.query('BEGIN');
|
|
||||||
|
|
||||||
// Invoice-Nummer validieren (falls angegeben)
|
|
||||||
if (invoice_number && !/^\d+$/.test(invoice_number)) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
return res.status(400).json({ error: 'Invoice number must be numeric.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invoice_number) {
|
|
||||||
const existing = await client.query('SELECT id FROM invoices WHERE invoice_number = $1 AND id != $2', [invoice_number, id]);
|
|
||||||
if (existing.rows.length > 0) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let subtotal = 0;
|
|
||||||
for (const item of items) {
|
|
||||||
const amount = parseFloat(item.amount.replace(/[$,]/g, ''));
|
|
||||||
if (!isNaN(amount)) subtotal += amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tax_rate = 8.25;
|
|
||||||
const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100);
|
|
||||||
const total = subtotal + tax_amount;
|
|
||||||
|
|
||||||
// Update lokal — invoice_number nur ändern wenn angegeben
|
|
||||||
if (invoice_number) {
|
|
||||||
await client.query(
|
|
||||||
`UPDATE invoices SET invoice_number = $1, customer_id = $2, invoice_date = $3, terms = $4, auth_code = $5, tax_exempt = $6,
|
|
||||||
tax_rate = $7, subtotal = $8, tax_amount = $9, total = $10, scheduled_send_date = $11, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $12`,
|
|
||||||
[invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, id]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await client.query(
|
|
||||||
`UPDATE invoices SET customer_id = $1, invoice_date = $2, terms = $3, auth_code = $4, tax_exempt = $5,
|
|
||||||
tax_rate = $6, subtotal = $7, tax_amount = $8, total = $9, scheduled_send_date = $10, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $11`,
|
|
||||||
[customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items neu schreiben
|
|
||||||
await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]);
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
await client.query(
|
|
||||||
'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order, qbo_item_id) VALUES ($1, $2, $3, $4, $5, $6, $7)',
|
|
||||||
[id, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i, items[i].qbo_item_id || '9']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.query('COMMIT');
|
|
||||||
|
|
||||||
// Auto QBO: Export if not yet in QBO, Sync if already in QBO
|
|
||||||
let qboResult = null;
|
|
||||||
try {
|
|
||||||
// Prüfe ob Invoice schon in QBO
|
|
||||||
const checkRes = await client.query('SELECT qbo_id FROM invoices WHERE id = $1', [id]);
|
|
||||||
const hasQboId = !!checkRes.rows[0]?.qbo_id;
|
|
||||||
|
|
||||||
if (hasQboId) {
|
|
||||||
qboResult = await syncInvoiceToQbo(id, client);
|
|
||||||
} else {
|
|
||||||
qboResult = await exportInvoiceToQbo(id, client);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (qboResult.skipped) {
|
|
||||||
console.log(`ℹ️ Invoice ${id}: ${qboResult.reason}`);
|
|
||||||
}
|
|
||||||
} catch (qboErr) {
|
|
||||||
console.error(`⚠️ Auto QBO failed for Invoice ${id}:`, qboErr.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true, qbo_synced: !!qboResult?.success, qbo_id: qboResult?.qbo_id || null, qbo_doc_number: qboResult?.qbo_doc_number || null });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
console.error('Error updating invoice:', error);
|
|
||||||
res.status(500).json({ error: 'Error updating invoice' });
|
|
||||||
} finally {
|
|
||||||
client.release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete('/api/invoices/:id', async (req, res) => {
|
app.delete('/api/invoices/:id', async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue