guthaben
This commit is contained in:
@@ -68,6 +68,109 @@ function applyCustomerTaxStatus(customerId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Customer Credit
|
||||
//
|
||||
// Das Guthaben selbst wird in QBO gefuehrt (negativer A/R-Saldo). Die App
|
||||
// merkt sich nur, welcher Betrag auf DIESER Rechnung ausgewiesen wird, damit
|
||||
// der Kunde den Endbetrag nachvollziehen kann. Subtotal, Tax und Total bleiben
|
||||
// unberuehrt — ein Guthaben ist eine Zahlungsverrechnung, kein Rabatt.
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Guthaben des aktuell gewaehlten Kunden laut QBO (null = noch nicht geprueft). */
|
||||
let availableCredit = null;
|
||||
|
||||
function creditEls() {
|
||||
return {
|
||||
banner: document.getElementById('invoice-credit-banner'),
|
||||
text: document.getElementById('invoice-credit-available-text'),
|
||||
fields: document.getElementById('invoice-credit-fields'),
|
||||
amount: document.getElementById('invoice-credit-applied'),
|
||||
memo: document.getElementById('invoice-credit-memo'),
|
||||
balance: document.getElementById('invoice-balance-due')
|
||||
};
|
||||
}
|
||||
|
||||
/** Betrag im Guthabenfeld — 0, wenn leer oder ungueltig. */
|
||||
function appliedCredit() {
|
||||
const el = document.getElementById('invoice-credit-applied');
|
||||
const n = parseFloat(el?.value);
|
||||
return isFinite(n) && n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
function resetCreditUi() {
|
||||
availableCredit = null;
|
||||
const e = creditEls();
|
||||
if (e.banner) e.banner.classList.add('hidden');
|
||||
if (e.fields) e.fields.style.display = 'none';
|
||||
if (e.amount) e.amount.value = '0';
|
||||
if (e.memo) e.memo.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragt QBO nach Guthaben des gewaehlten Kunden. Fehler bleiben stumm: ein
|
||||
* nicht erreichbares QBO darf das Anlegen einer Rechnung nicht blockieren.
|
||||
*/
|
||||
async function checkCustomerCredit(customerId) {
|
||||
availableCredit = null;
|
||||
const e = creditEls();
|
||||
if (e.banner) e.banner.classList.add('hidden');
|
||||
if (!customerId) return;
|
||||
|
||||
try {
|
||||
const credits = await window.API.accounting.getCustomerCredits();
|
||||
if (!Array.isArray(credits)) return;
|
||||
const match = credits.find(c => c.customerId === parseInt(customerId));
|
||||
if (!match || !(match.credit > 0)) return;
|
||||
|
||||
availableCredit = match.credit;
|
||||
if (e.text) e.text.textContent =
|
||||
`${match.name} has a credit of $${match.credit.toFixed(2)} on account.`;
|
||||
if (e.banner) e.banner.classList.remove('hidden');
|
||||
} catch (err) {
|
||||
console.log('Customer credit check skipped:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Uebernimmt das Guthaben in die Rechnung — gedeckelt auf das Rechnungstotal. */
|
||||
export function applyCustomerCredit() {
|
||||
if (!(availableCredit > 0)) return;
|
||||
const e = creditEls();
|
||||
const total = currentInvoiceTotal();
|
||||
const value = Math.min(availableCredit, total);
|
||||
|
||||
if (value <= 0) {
|
||||
alert('Please add items first — a credit can only be applied to an invoice amount.');
|
||||
return;
|
||||
}
|
||||
if (e.fields) e.fields.style.display = 'block';
|
||||
if (e.amount) e.amount.value = value.toFixed(2);
|
||||
if (e.memo && !e.memo.value) e.memo.value = 'Credit on account';
|
||||
if (value < availableCredit) {
|
||||
alert(`Only $${value.toFixed(2)} of the $${availableCredit.toFixed(2)} credit fits on this invoice. `
|
||||
+ 'The remainder stays on account in QBO.');
|
||||
}
|
||||
updateInvoiceTotals();
|
||||
}
|
||||
|
||||
export function clearCustomerCredit() {
|
||||
const e = creditEls();
|
||||
if (e.amount) e.amount.value = '0';
|
||||
if (e.memo) e.memo.value = '';
|
||||
if (e.fields) e.fields.style.display = 'none';
|
||||
updateInvoiceTotals();
|
||||
}
|
||||
|
||||
function currentInvoiceTotal() {
|
||||
const items = getItems('invoice-items');
|
||||
const taxExempt = document.getElementById('invoice-tax-exempt')?.checked;
|
||||
let subtotal = 0;
|
||||
items.forEach(item => {
|
||||
subtotal += parseFloat(String(item.amount).replace(/[$,]/g, '')) || 0;
|
||||
});
|
||||
return subtotal + (taxExempt ? 0 : subtotal * 8.25 / 100);
|
||||
}
|
||||
|
||||
function updateRecurringChildUi(invoice = null) {
|
||||
const recurringCb = document.getElementById('invoice-recurring');
|
||||
if (!recurringCb) return;
|
||||
@@ -205,6 +308,17 @@ async function loadInvoiceForEdit(invoiceId) {
|
||||
document.getElementById('invoice-authorization').value = data.invoice.auth_code || '';
|
||||
document.getElementById('invoice-tax-exempt').checked = data.invoice.tax_exempt;
|
||||
document.getElementById('invoice-bill-to-name').value = data.invoice.bill_to_name || '';
|
||||
|
||||
// Guthaben einer bestehenden Rechnung wiederherstellen
|
||||
resetCreditUi();
|
||||
const savedCredit = parseFloat(data.invoice.credit_applied) || 0;
|
||||
if (savedCredit > 0) {
|
||||
const ce = creditEls();
|
||||
if (ce.fields) ce.fields.style.display = 'block';
|
||||
if (ce.amount) ce.amount.value = savedCredit.toFixed(2);
|
||||
if (ce.memo) ce.memo.value = data.invoice.credit_memo || '';
|
||||
}
|
||||
checkCustomerCredit(data.invoice.customer_id);
|
||||
|
||||
// Worker
|
||||
populateWorkerDropdown();
|
||||
@@ -254,6 +368,7 @@ function prepareNewInvoice() {
|
||||
document.getElementById('invoice-modal-title').textContent = 'New Invoice';
|
||||
document.getElementById('invoice-form').reset();
|
||||
document.getElementById('invoice-items').innerHTML = '';
|
||||
resetCreditUi();
|
||||
document.getElementById('invoice-terms').value = 'Net 14';
|
||||
document.getElementById('invoice-number').value = '';
|
||||
document.getElementById('invoice-send-date').value = '';
|
||||
@@ -297,6 +412,18 @@ export function updateInvoiceTotals() {
|
||||
document.getElementById('invoice-tax').textContent = taxExempt ? '$0.00' : `$${taxAmount.toFixed(2)}`;
|
||||
document.getElementById('invoice-total').textContent = `$${total.toFixed(2)}`;
|
||||
document.getElementById('invoice-tax-row').style.display = taxExempt ? 'none' : 'block';
|
||||
|
||||
// Guthabenzeile: Betrag nie ueber dem Total, sonst waere die Balance Due negativ.
|
||||
const credit = appliedCredit();
|
||||
const e = creditEls();
|
||||
if (credit > 0) {
|
||||
if (credit > total && e.amount) e.amount.value = total.toFixed(2);
|
||||
const applied = Math.min(credit, total);
|
||||
if (e.fields) e.fields.style.display = 'block';
|
||||
if (e.balance) e.balance.textContent = `$${(total - applied).toFixed(2)}`;
|
||||
} else if (e.fields) {
|
||||
e.fields.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleInvoiceSubmit(e) {
|
||||
@@ -317,6 +444,8 @@ export async function handleInvoiceSubmit(e) {
|
||||
worker: document.getElementById('invoice-worker')?.value || null,
|
||||
is_recurring: isRecurring,
|
||||
recurring_interval: recurringInterval,
|
||||
credit_applied: appliedCredit(),
|
||||
credit_memo: document.getElementById('invoice-credit-memo')?.value?.trim() || null,
|
||||
items: getItems('invoice-items')
|
||||
};
|
||||
|
||||
@@ -357,6 +486,9 @@ export function initInvoiceModal() {
|
||||
const taxExempt = document.getElementById('invoice-tax-exempt');
|
||||
if (taxExempt) taxExempt.addEventListener('change', updateInvoiceTotals);
|
||||
|
||||
const creditAmount = document.getElementById('invoice-credit-applied');
|
||||
if (creditAmount) creditAmount.addEventListener('input', updateInvoiceTotals);
|
||||
|
||||
// Recurring toggle
|
||||
const recurringCb = document.getElementById('invoice-recurring');
|
||||
const recurringGroup = document.getElementById('invoice-recurring-group');
|
||||
@@ -373,12 +505,17 @@ export function initInvoiceModal() {
|
||||
// Only auto-apply when creating new (not editing existing)
|
||||
if (!currentInvoiceId && customerHidden.value) {
|
||||
applyCustomerTaxStatus(customerHidden.value);
|
||||
checkCustomerCredit(customerHidden.value);
|
||||
}
|
||||
});
|
||||
observer.observe(customerHidden, { attributes: true, attributeFilter: ['value'] });
|
||||
}
|
||||
}
|
||||
|
||||
window.invoiceModal = {
|
||||
applyCustomerCredit,
|
||||
clearCustomerCredit
|
||||
};
|
||||
window.openInvoiceModal = openInvoiceModal;
|
||||
window.closeInvoiceModal = closeInvoiceModal;
|
||||
window.addInvoiceItem = addInvoiceItem;
|
||||
|
||||
Reference in New Issue
Block a user