This commit is contained in:
2026-02-17 20:53:00 -06:00
parent 84b0836234
commit a0c62d639e
4 changed files with 191 additions and 99 deletions

109
server.js
View File

@@ -457,31 +457,32 @@ 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, created_from_quote_id } = req.body;
const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, created_from_quote_id, scheduled_send_date } = req.body;
const client = await pool.connect();
try {
await client.query('BEGIN');
// Validate invoice_number is provided and is numeric
if (!invoice_number || !/^\d+$/.test(invoice_number)) {
await client.query('ROLLBACK');
return res.status(400).json({ error: 'Invalid invoice number. Must be a numeric value.' });
}
// Check if invoice number already exists
const existingInvoice = await client.query(
'SELECT id FROM invoices WHERE invoice_number = $1',
[invoice_number]
);
if (existingInvoice.rows.length > 0) {
await client.query('ROLLBACK');
return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` });
// invoice_number ist jetzt OPTIONAL — wird erst beim QBO Export vergeben
// Wenn angegeben, muss sie numerisch sein und darf nicht existieren
if (invoice_number && invoice_number.trim() !== '') {
if (!/^\d+$/.test(invoice_number)) {
await client.query('ROLLBACK');
return res.status(400).json({ error: 'Invalid invoice number. Must be a numeric value.' });
}
const existingInvoice = await client.query(
'SELECT id FROM invoices WHERE invoice_number = $1',
[invoice_number]
);
if (existingInvoice.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)) {
@@ -493,19 +494,22 @@ app.post('/api/invoices', async (req, res) => {
const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100);
const total = subtotal + tax_amount;
// invoice_number kann NULL sein
const invNum = (invoice_number && invoice_number.trim() !== '') ? invoice_number : null;
const sendDate = scheduled_send_date || null;
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, created_from_quote_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`,
[invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, created_from_quote_id]
`INSERT INTO invoices (invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, created_from_quote_id, scheduled_send_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`,
[invNum, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, created_from_quote_id, sendDate]
);
const invoiceId = invoiceResult.rows[0].id;
for (let i = 0; i < items.length; i++) {
await client.query(
// qbo_item_id hinzugefügt
'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'] // Default '9' (Parts)
[invoiceId, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i, items[i].qbo_item_id || '9']
);
}
@@ -556,7 +560,7 @@ app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => {
return res.status(400).json({ error: 'Cannot convert quote with TBD items to invoice. Please update all TBD items first.' });
}
const invoice_number = await getNextInvoiceNumber();
const invoice_number = null;
const invoiceDate = new Date().toISOString().split('T')[0];
const invoiceResult = await client.query(
@@ -588,31 +592,32 @@ 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 } = req.body;
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');
// Validate invoice_number is provided and is numeric
if (!invoice_number || !/^\d+$/.test(invoice_number)) {
// invoice_number ist optional. Wenn angegeben und nicht leer, muss sie numerisch sein
const invNum = (invoice_number && invoice_number.trim() !== '') ? invoice_number : null;
if (invNum && !/^\d+$/.test(invNum)) {
await client.query('ROLLBACK');
return res.status(400).json({ error: 'Invalid invoice number. Must be a numeric value.' });
}
// Check if invoice number already exists (excluding current invoice)
const existingInvoice = await client.query(
'SELECT id FROM invoices WHERE invoice_number = $1 AND id != $2',
[invoice_number, id]
);
if (existingInvoice.rows.length > 0) {
await client.query('ROLLBACK');
return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` });
if (invNum) {
const existingInvoice = await client.query(
'SELECT id FROM invoices WHERE invoice_number = $1 AND id != $2',
[invNum, id]
);
if (existingInvoice.rows.length > 0) {
await client.query('ROLLBACK');
return res.status(400).json({ error: `Invoice number ${invNum} already exists.` });
}
}
let subtotal = 0;
for (const item of items) {
const amount = parseFloat(item.amount.replace(/[$,]/g, ''));
if (!isNaN(amount)) {
@@ -623,12 +628,13 @@ app.put('/api/invoices/:id', async (req, res) => {
const tax_rate = 8.25;
const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100);
const total = subtotal + tax_amount;
const sendDate = scheduled_send_date || null;
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, updated_at = CURRENT_TIMESTAMP
WHERE id = $11`,
[invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, id]
tax_rate = $7, subtotal = $8, tax_amount = $9, total = $10, scheduled_send_date = $11, updated_at = CURRENT_TIMESTAMP
WHERE id = $12`,
[invNum, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, sendDate, id]
);
await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]);
@@ -651,6 +657,7 @@ app.put('/api/invoices/:id', async (req, res) => {
}
});
app.delete('/api/invoices/:id', async (req, res) => {
const { id } = req.params;
const client = await pool.connect();
@@ -1580,7 +1587,29 @@ app.patch('/api/invoices/:id/mark-unpaid', async (req, res) => {
}
});
app.patch('/api/invoices/:id/reset-qbo', async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query(
`UPDATE invoices
SET qbo_id = NULL, qbo_sync_token = NULL, qbo_doc_number = NULL, invoice_number = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1 RETURNING *`,
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Invoice not found' });
}
console.log(`🔄 Invoice ID ${id} QBO-Verknüpfung zurückgesetzt`);
res.json(result.rows[0]);
} catch (error) {
console.error('Error resetting QBO link:', error);
res.status(500).json({ error: 'Error resetting QBO link' });
}
});