update expense
This commit is contained in:
@@ -33,6 +33,7 @@ let isSaving = false;
|
|||||||
|
|
||||||
let selectedFile = null; // File object aus <input type="file">
|
let selectedFile = null; // File object aus <input type="file">
|
||||||
let attachmentLimits = null; // { maxBytes, maxMb, allowedMimeTypes, allowedExtensions }
|
let attachmentLimits = null; // { maxBytes, maxMb, allowedMimeTypes, allowedExtensions }
|
||||||
|
let editingExpenseId = null; // null = Create-Modus, sonst = Purchase-ID im Edit-Modus
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
// Helpers
|
// Helpers
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
@@ -138,14 +139,14 @@ function renderFilePreview() {
|
|||||||
// Public Entry
|
// Public Entry
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function openExpenseModal({ onSaved } = {}) {
|
export async function openExpenseModal({ onSaved, expense = null } = {}) {
|
||||||
onSavedCb = onSaved || null;
|
onSavedCb = onSaved || null;
|
||||||
selectedVendorId = null;
|
selectedVendorId = null;
|
||||||
selectedVendorName = '';
|
selectedVendorName = '';
|
||||||
lineCount = 0;
|
lineCount = 0;
|
||||||
isSaving = false;
|
isSaving = false;
|
||||||
|
editingExpenseId = expense ? expense.id : null;
|
||||||
|
|
||||||
// Lade Stammdaten parallel (alles aus dem Cache → schnell)
|
|
||||||
try {
|
try {
|
||||||
[vendors, expenseAccounts, paymentAccounts, paymentMethods, attachmentLimits] = await Promise.all([
|
[vendors, expenseAccounts, paymentAccounts, paymentMethods, attachmentLimits] = await Promise.all([
|
||||||
window.API.accounting.getVendors('', 1000),
|
window.API.accounting.getVendors('', 1000),
|
||||||
@@ -155,7 +156,6 @@ export async function openExpenseModal({ onSaved } = {}) {
|
|||||||
window.API.accounting.getAttachmentLimits()
|
window.API.accounting.getAttachmentLimits()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Errors aus dem Backend abfangen
|
|
||||||
for (const [name, data] of [['vendors', vendors], ['expenseAccounts', expenseAccounts], ['paymentAccounts', paymentAccounts], ['paymentMethods', paymentMethods]]) {
|
for (const [name, data] of [['vendors', vendors], ['expenseAccounts', expenseAccounts], ['paymentAccounts', paymentAccounts], ['paymentMethods', paymentMethods]]) {
|
||||||
if (data && data.error) {
|
if (data && data.error) {
|
||||||
alert(`Failed to load ${name}: ${data.error}\n\nTry "Sync from QBO" first.`);
|
alert(`Failed to load ${name}: ${data.error}\n\nTry "Sync from QBO" first.`);
|
||||||
@@ -168,6 +168,11 @@ export async function openExpenseModal({ onSaved } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderModal();
|
renderModal();
|
||||||
|
|
||||||
|
// Im Edit-Modus: Formular mit bestehenden Daten befüllen
|
||||||
|
if (expense) {
|
||||||
|
prefillForm(expense);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
@@ -192,6 +197,43 @@ function resetForm() {
|
|||||||
updateTotal();
|
updateTotal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prefillForm(expense) {
|
||||||
|
// Vendor
|
||||||
|
if (expense.vendorId) {
|
||||||
|
selectVendor(expense.vendorId, expense.vendorName || expense.vendorId);
|
||||||
|
}
|
||||||
|
// Payment Account
|
||||||
|
if (expense.accountId) {
|
||||||
|
const pa = document.getElementById('exp-payment-account');
|
||||||
|
if (pa) pa.value = expense.accountId;
|
||||||
|
}
|
||||||
|
// Datum
|
||||||
|
if (expense.txnDate) {
|
||||||
|
document.getElementById('exp-date').value = expense.txnDate;
|
||||||
|
}
|
||||||
|
// Payment Method
|
||||||
|
if (expense.paymentMethodId) {
|
||||||
|
const pm = document.getElementById('exp-payment-method');
|
||||||
|
if (pm) pm.value = expense.paymentMethodId;
|
||||||
|
}
|
||||||
|
// Ref / Memo
|
||||||
|
if (expense.refNo) document.getElementById('exp-ref-no').value = expense.refNo;
|
||||||
|
if (expense.memo) document.getElementById('exp-memo').value = expense.memo;
|
||||||
|
|
||||||
|
// Lines — bestehende Zeilen ersetzen
|
||||||
|
document.getElementById('exp-lines-tbody').innerHTML = '';
|
||||||
|
lineCount = 0;
|
||||||
|
const lines = expense.lines && expense.lines.length ? expense.lines : [{}];
|
||||||
|
lines.forEach(l => {
|
||||||
|
addLine();
|
||||||
|
const rows = document.querySelectorAll('#exp-lines-tbody tr');
|
||||||
|
const tr = rows[rows.length - 1];
|
||||||
|
if (l.accountId) tr.querySelector('.exp-line-account').value = l.accountId;
|
||||||
|
if (l.description) tr.querySelector('.exp-line-desc').value = l.description;
|
||||||
|
if (l.amount != null) tr.querySelector('.exp-line-amount').value = l.amount;
|
||||||
|
});
|
||||||
|
updateTotal();
|
||||||
|
}
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
// Render
|
// Render
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
@@ -203,7 +245,7 @@ function renderModal() {
|
|||||||
<div id="expense-modal" class="modal fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-start justify-center z-50">
|
<div id="expense-modal" class="modal fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-start justify-center z-50">
|
||||||
<div class="relative mx-auto p-6 border w-full max-w-5xl shadow-lg rounded-lg bg-white my-8">
|
<div class="relative mx-auto p-6 border w-full max-w-5xl shadow-lg rounded-lg bg-white my-8">
|
||||||
<div class="flex justify-between items-center mb-5">
|
<div class="flex justify-between items-center mb-5">
|
||||||
<h3 class="text-2xl font-bold text-gray-900">📝 New Expense</h3>
|
<h3 class="text-2xl font-bold text-gray-900">${editingExpenseId ? '✏️ Edit Expense' : '📝 New Expense'}</h3>
|
||||||
<button onclick="window.expenseModal.close()" class="text-gray-400 hover:text-gray-600">
|
<button onclick="window.expenseModal.close()" class="text-gray-400 hover:text-gray-600">
|
||||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
@@ -326,19 +368,19 @@ function renderModal() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="exp-error" class="hidden mb-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700"></div>
|
<div id="exp-error" class="hidden mb-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700"></div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button type="button" onclick="window.expenseModal.close()"
|
<button type="button" onclick="window.expenseModal.close()"
|
||||||
class="px-5 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-md">
|
class="px-5 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-md">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="exp-save-new-btn" onclick="window.expenseModal.save('new')"
|
${editingExpenseId ? '' : `
|
||||||
class="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md font-medium">
|
<button type="button" id="exp-save-new-btn" onclick="window.expenseModal.save('new')"
|
||||||
Save & New
|
class="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md font-medium">
|
||||||
</button>
|
Save & New
|
||||||
|
</button>`}
|
||||||
<button type="button" id="exp-save-close-btn" onclick="window.expenseModal.save('close')"
|
<button type="button" id="exp-save-close-btn" onclick="window.expenseModal.save('close')"
|
||||||
class="px-5 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md font-medium">
|
class="px-5 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md font-medium">
|
||||||
Save & Close
|
${editingExpenseId ? 'Save Changes' : 'Save & Close'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -676,11 +718,13 @@ async function save(mode) {
|
|||||||
setButtonsDisabled(true);
|
setButtonsDisabled(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.API.accounting.createExpense(payload);
|
const result = editingExpenseId
|
||||||
if (result.error) {
|
? await window.API.accounting.updateExpense(editingExpenseId, payload)
|
||||||
showError(result.error);
|
: await window.API.accounting.createExpense(payload);
|
||||||
return;
|
if (result.error) {
|
||||||
}
|
showError(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Attachment hochladen, falls vorhanden
|
// Attachment hochladen, falls vorhanden
|
||||||
if (selectedFile && result.id) {
|
if (selectedFile && result.id) {
|
||||||
@@ -728,7 +772,7 @@ function setButtonsDisabled(disabled, customText) {
|
|||||||
if (b) b.disabled = disabled;
|
if (b) b.disabled = disabled;
|
||||||
const text = customText || (disabled ? 'Saving…' : null);
|
const text = customText || (disabled ? 'Saving…' : null);
|
||||||
if (a) a.textContent = text || 'Save & New';
|
if (a) a.textContent = text || 'Save & New';
|
||||||
if (b) b.textContent = text || 'Save & Close';
|
if (b) b.textContent = text || (editingExpenseId ? 'Save Changes' : 'Save & Close');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(msg) {
|
function showError(msg) {
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ const API = {
|
|||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
}).then(r => r.json()),
|
}).then(r => r.json()),
|
||||||
|
|
||||||
|
updateExpense: (id, data) => fetch(`/api/accounting/expenses/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}).then(r => r.json()),
|
||||||
|
|
||||||
attachToExpense: (expenseId, file, note) => {
|
attachToExpense: (expenseId, file, note) => {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('file', file);
|
fd.append('file', file);
|
||||||
|
|||||||
@@ -623,6 +623,11 @@ function renderExpensesTable(expenses) {
|
|||||||
</details>`
|
</details>`
|
||||||
: escapeHtml(e.lines[0]?.accountName || '');
|
: escapeHtml(e.lines[0]?.accountName || '');
|
||||||
|
|
||||||
|
const editBtn = expOnlyMine
|
||||||
|
? `<button onclick='window.accountingView.editExpense(${JSON.stringify(JSON.stringify(e))})'
|
||||||
|
class="text-blue-600 hover:text-blue-800 text-xs font-medium">Edit</button>`
|
||||||
|
: `<span class="text-gray-300 text-xs" title="Only expenses created from this app can be edited">—</span>`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr class="border-t hover:bg-gray-50 align-top">
|
<tr class="border-t hover:bg-gray-50 align-top">
|
||||||
<td class="px-3 py-2 text-sm whitespace-nowrap">${escapeHtml(e.txnDate || '')}</td>
|
<td class="px-3 py-2 text-sm whitespace-nowrap">${escapeHtml(e.txnDate || '')}</td>
|
||||||
@@ -632,6 +637,7 @@ function renderExpensesTable(expenses) {
|
|||||||
<td class="px-3 py-2 text-sm">${escapeHtml(e.refNo || '')}</td>
|
<td class="px-3 py-2 text-sm">${escapeHtml(e.refNo || '')}</td>
|
||||||
<td class="px-3 py-2 text-sm text-gray-500">${escapeHtml(e.memo || '')}</td>
|
<td class="px-3 py-2 text-sm text-gray-500">${escapeHtml(e.memo || '')}</td>
|
||||||
<td class="px-3 py-2 text-sm text-right whitespace-nowrap text-red-600">${fmtMoney(e.totalAmt)}</td>
|
<td class="px-3 py-2 text-sm text-right whitespace-nowrap text-red-600">${fmtMoney(e.totalAmt)}</td>
|
||||||
|
<td class="px-3 py-2 text-sm text-center">${editBtn}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
@@ -647,6 +653,7 @@ function renderExpensesTable(expenses) {
|
|||||||
<th class="px-3 py-2 text-left font-medium text-gray-700">Ref</th>
|
<th class="px-3 py-2 text-left font-medium text-gray-700">Ref</th>
|
||||||
<th class="px-3 py-2 text-left font-medium text-gray-700">Memo</th>
|
<th class="px-3 py-2 text-left font-medium text-gray-700">Memo</th>
|
||||||
<th class="px-3 py-2 text-right font-medium text-gray-700">Amount</th>
|
<th class="px-3 py-2 text-right font-medium text-gray-700">Amount</th>
|
||||||
|
<th class="px-3 py-2 text-center font-medium text-gray-700">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>${tbody}</tbody>
|
<tbody>${tbody}</tbody>
|
||||||
@@ -683,7 +690,19 @@ export function refreshAll() {
|
|||||||
loadAccountsOverview();
|
loadAccountsOverview();
|
||||||
if (registerAccountId) loadRegister();
|
if (registerAccountId) loadRegister();
|
||||||
}
|
}
|
||||||
|
export async function editExpense(expenseJson) {
|
||||||
|
let expense;
|
||||||
|
try {
|
||||||
|
expense = typeof expenseJson === 'string' ? JSON.parse(expenseJson) : expenseJson;
|
||||||
|
} catch (e) {
|
||||||
|
alert('Could not open expense for editing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await openExpenseModal({
|
||||||
|
expense,
|
||||||
|
onSaved: () => loadExpenses()
|
||||||
|
});
|
||||||
|
}
|
||||||
window.accountingView = {
|
window.accountingView = {
|
||||||
renderAccountingView,
|
renderAccountingView,
|
||||||
refreshAll,
|
refreshAll,
|
||||||
@@ -695,5 +714,6 @@ window.accountingView = {
|
|||||||
loadBalanceSheet,
|
loadBalanceSheet,
|
||||||
loadExpenses,
|
loadExpenses,
|
||||||
openNewExpense,
|
openNewExpense,
|
||||||
|
editExpense,
|
||||||
toggleSection
|
toggleSection
|
||||||
};
|
};
|
||||||
@@ -268,6 +268,15 @@ router.post('/expenses/:id/attach', (req, res, next) => {
|
|||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err) { handleQboError(err, res, 'attach'); }
|
} catch (err) { handleQboError(err, res, 'attach'); }
|
||||||
});
|
});
|
||||||
|
// ─── PUT /api/accounting/expenses/:id ───────────────────────────────
|
||||||
|
// Aktualisiert eine bestehende QBO Purchase (Expense).
|
||||||
|
// Body wie POST /expenses: { vendorId, paymentAccountId, txnDate, paymentMethodId?, refNo?, memo?, lines: [...] }
|
||||||
|
router.put('/expenses/:id', express.json(), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await accountingService.updateExpense(req.params.id, req.body || {});
|
||||||
|
res.json(result);
|
||||||
|
} catch (err) { handleQboError(err, res, 'expense-update'); }
|
||||||
|
});
|
||||||
router.get('/attachments/limits', (req, res) => {
|
router.get('/attachments/limits', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
maxBytes: ATTACHMENT_MAX_BYTES,
|
maxBytes: ATTACHMENT_MAX_BYTES,
|
||||||
|
|||||||
@@ -898,6 +898,152 @@ async function createExpense(data) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert eine bestehende QBO Purchase (Expense).
|
||||||
|
* QBO erfordert ein VOLLSTÄNDIGES Update — die komplette Line-Liste muss
|
||||||
|
* mitgeschickt werden, sonst gehen Zeilen verloren.
|
||||||
|
*
|
||||||
|
* @param {string} purchaseId
|
||||||
|
* @param {Object} data — gleiche Struktur wie createExpense
|
||||||
|
* @returns {{ id, txnDate, totalAmt, lineCount, vendorName, accountName }}
|
||||||
|
*/
|
||||||
|
async function updateExpense(purchaseId, data) {
|
||||||
|
if (!purchaseId) throw badRequest('purchaseId is required');
|
||||||
|
if (!data.vendorId) throw badRequest('vendorId is required');
|
||||||
|
if (!data.paymentAccountId) throw badRequest('paymentAccountId is required');
|
||||||
|
if (!data.txnDate) throw badRequest('txnDate is required');
|
||||||
|
if (!Array.isArray(data.lines) || data.lines.length === 0) {
|
||||||
|
throw badRequest('At least one line is required');
|
||||||
|
}
|
||||||
|
for (const [i, line] of data.lines.entries()) {
|
||||||
|
if (!line.accountId) throw badRequest(`Line ${i + 1}: accountId is required`);
|
||||||
|
const amt = Number(line.amount);
|
||||||
|
if (!isFinite(amt) || amt <= 0) {
|
||||||
|
throw badRequest(`Line ${i + 1}: amount must be a positive number`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { companyId, baseUrl } = getClientInfo();
|
||||||
|
|
||||||
|
// ── Aktuelle Purchase laden (für SyncToken) ──
|
||||||
|
const getUrl = withMinorVersion(`${baseUrl}/v3/company/${companyId}/purchase/${purchaseId}`);
|
||||||
|
const getResponse = await makeQboApiCall({ url: getUrl, method: 'GET' });
|
||||||
|
const getData = getJson(getResponse);
|
||||||
|
throwIfFault(getData, 'Purchase fetch');
|
||||||
|
|
||||||
|
const current = getData.Purchase;
|
||||||
|
if (!current || !current.Id) {
|
||||||
|
throw badRequest(`Purchase ${purchaseId} not found in QBO`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Payment-Account-Type bestimmen ──
|
||||||
|
const acctRow = await pool.query(
|
||||||
|
`SELECT qbo_id, name, account_type FROM qbo_account_cache WHERE qbo_id = $1`,
|
||||||
|
[data.paymentAccountId]
|
||||||
|
);
|
||||||
|
if (acctRow.rows.length === 0) {
|
||||||
|
throw badRequest(`Payment account ${data.paymentAccountId} not in cache. Run sync first.`);
|
||||||
|
}
|
||||||
|
const paymentAcct = acctRow.rows[0];
|
||||||
|
const paymentType = paymentAcct.account_type === 'Credit Card' ? 'CreditCard' : 'Check';
|
||||||
|
|
||||||
|
const vendorRow = await pool.query(
|
||||||
|
`SELECT display_name FROM qbo_vendor_cache WHERE qbo_id = $1`,
|
||||||
|
[data.vendorId]
|
||||||
|
);
|
||||||
|
const vendorName = vendorRow.rows[0]?.display_name || data.vendorId;
|
||||||
|
|
||||||
|
const totalAmt = data.lines.reduce((sum, l) => sum + Number(l.amount), 0);
|
||||||
|
|
||||||
|
// ── Update-Payload — vollständig, mit Id + SyncToken ──
|
||||||
|
const payload = {
|
||||||
|
Id: current.Id,
|
||||||
|
SyncToken: current.SyncToken,
|
||||||
|
AccountRef: { value: paymentAcct.qbo_id, name: paymentAcct.name },
|
||||||
|
EntityRef: { value: data.vendorId, type: 'Vendor', name: vendorName },
|
||||||
|
TxnDate: data.txnDate,
|
||||||
|
PaymentType: paymentType,
|
||||||
|
Line: data.lines.map(line => ({
|
||||||
|
DetailType: 'AccountBasedExpenseLineDetail',
|
||||||
|
Amount: Number(line.amount),
|
||||||
|
Description: line.description || undefined,
|
||||||
|
AccountBasedExpenseLineDetail: {
|
||||||
|
AccountRef: { value: String(line.accountId) }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
payload.DocNumber = data.refNo ? String(data.refNo).slice(0, 21) : undefined;
|
||||||
|
payload.PrivateNote = data.memo ? String(data.memo) : undefined;
|
||||||
|
if (data.paymentMethodId) {
|
||||||
|
payload.PaymentMethodRef = { value: String(data.paymentMethodId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = withMinorVersion(`${baseUrl}/v3/company/${companyId}/purchase`);
|
||||||
|
const requestSummary = `UPDATE ${purchaseId} | ${vendorName} | ${data.txnDate} | $${totalAmt.toFixed(2)} | ${data.lines.length} line(s)`;
|
||||||
|
|
||||||
|
let qboResponse;
|
||||||
|
try {
|
||||||
|
const response = await makeQboApiCall({
|
||||||
|
url,
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
qboResponse = getJson(response);
|
||||||
|
} catch (err) {
|
||||||
|
await writeAuditLog({
|
||||||
|
action: 'expense.update',
|
||||||
|
entityType: 'Purchase',
|
||||||
|
entityQboId: purchaseId,
|
||||||
|
status: 'error',
|
||||||
|
requestExcerpt: requestSummary,
|
||||||
|
responseExcerpt: err.message
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qboResponse.Fault) {
|
||||||
|
const msg = qboResponse.Fault.Error.map(e =>
|
||||||
|
`${e.code}: ${e.Message}${e.Detail ? ' - ' + e.Detail : ''}`
|
||||||
|
).join('; ');
|
||||||
|
await writeAuditLog({
|
||||||
|
action: 'expense.update',
|
||||||
|
entityType: 'Purchase',
|
||||||
|
entityQboId: purchaseId,
|
||||||
|
status: 'error',
|
||||||
|
requestExcerpt: requestSummary,
|
||||||
|
responseExcerpt: msg
|
||||||
|
});
|
||||||
|
const err = new Error('QBO Purchase update failed: ' + msg);
|
||||||
|
err.qboFault = qboResponse.Fault;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const purchase = qboResponse.Purchase;
|
||||||
|
if (!purchase || !purchase.Id) throw new Error('QBO returned no Purchase id');
|
||||||
|
|
||||||
|
await writeAuditLog({
|
||||||
|
action: 'expense.update',
|
||||||
|
entityType: 'Purchase',
|
||||||
|
entityQboId: purchase.Id,
|
||||||
|
status: 'success',
|
||||||
|
requestExcerpt: requestSummary,
|
||||||
|
responseExcerpt: `Purchase ${purchase.Id} updated, total $${Number(purchase.TotalAmt).toFixed(2)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ QBO Expense updated: Purchase ${purchase.Id} — ${requestSummary}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: purchase.Id,
|
||||||
|
txnDate: purchase.TxnDate,
|
||||||
|
totalAmt: Number(purchase.TotalAmt),
|
||||||
|
lineCount: data.lines.length,
|
||||||
|
vendorName,
|
||||||
|
accountName: paymentAcct.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function badRequest(msg) {
|
function badRequest(msg) {
|
||||||
const err = new Error(msg);
|
const err = new Error(msg);
|
||||||
err.statusCode = 400;
|
err.statusCode = 400;
|
||||||
@@ -1121,6 +1267,7 @@ module.exports = {
|
|||||||
// Phase 2 Lieferung 2 — Mutations + List
|
// Phase 2 Lieferung 2 — Mutations + List
|
||||||
createVendor,
|
createVendor,
|
||||||
createExpense,
|
createExpense,
|
||||||
|
updateExpense,
|
||||||
listExpenses,
|
listExpenses,
|
||||||
|
|
||||||
// Phase 2 Lieferung 3
|
// Phase 2 Lieferung 3
|
||||||
|
|||||||
Reference in New Issue
Block a user