Feat: Cashflow auf paid_amount, Differenz-Badge, Kulanz-Abschreibung im Bezahlt-Modal (SW by-v984)
This commit is contained in:
parent
0f6b5afd6a
commit
e714580d77
5 changed files with 70 additions and 14 deletions
|
|
@ -408,7 +408,7 @@ async def serve_media(path: str, request: _Request):
|
|||
raise _HE(404, "Nicht gefunden.")
|
||||
return _media_response(filepath)
|
||||
|
||||
APP_VER = "983" # muss mit APP_VER in app.js übereinstimmen
|
||||
APP_VER = "984" # muss mit APP_VER in app.js übereinstimmen
|
||||
|
||||
@app.get("/.well-known/assetlinks.json")
|
||||
async def assetlinks():
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class InvoiceCreate(BaseModel):
|
|||
class PayBody(BaseModel):
|
||||
paid_at: str
|
||||
paid_amount: float
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class CancelBody(BaseModel):
|
||||
|
|
@ -373,7 +374,9 @@ def get_cashflow(admin=Depends(require_admin)):
|
|||
with db() as conn:
|
||||
monthly = conn.execute("""
|
||||
SELECT substr(created_at, 1, 7) AS month,
|
||||
SUM(amount_gross) AS revenue,
|
||||
SUM(CASE WHEN status='paid'
|
||||
THEN COALESCE(paid_amount, amount_gross)
|
||||
ELSE amount_gross END) AS revenue,
|
||||
COUNT(*) AS count
|
||||
FROM invoices
|
||||
WHERE status IN ('sent', 'paid')
|
||||
|
|
@ -383,7 +386,10 @@ def get_cashflow(admin=Depends(require_admin)):
|
|||
|
||||
year = datetime.now().year
|
||||
total_year = conn.execute(
|
||||
"SELECT COALESCE(SUM(amount_gross),0) FROM invoices WHERE status IN ('sent','paid') AND created_at LIKE ?",
|
||||
"""SELECT COALESCE(SUM(CASE WHEN status='paid'
|
||||
THEN COALESCE(paid_amount, amount_gross)
|
||||
ELSE amount_gross END), 0)
|
||||
FROM invoices WHERE status IN ('sent','paid') AND created_at LIKE ?""",
|
||||
(f"{year}%",)
|
||||
).fetchone()[0]
|
||||
|
||||
|
|
@ -705,10 +711,16 @@ def pay_invoice(invoice_id: int, data: PayBody, admin=Depends(require_admin)):
|
|||
raise HTTPException(404, "Rechnung nicht gefunden.")
|
||||
if row["status"] == "cancelled":
|
||||
raise HTTPException(400, "Stornierte Rechnung kann nicht als bezahlt markiert werden.")
|
||||
conn.execute(
|
||||
"UPDATE invoices SET status='paid', paid_at=?, paid_amount=? WHERE id=?",
|
||||
(data.paid_at, data.paid_amount, invoice_id)
|
||||
)
|
||||
if data.notes:
|
||||
conn.execute(
|
||||
"UPDATE invoices SET status='paid', paid_at=?, paid_amount=?, notes=? WHERE id=?",
|
||||
(data.paid_at, data.paid_amount, data.notes, invoice_id)
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE invoices SET status='paid', paid_at=?, paid_amount=? WHERE id=?",
|
||||
(data.paid_at, data.paid_amount, invoice_id)
|
||||
)
|
||||
row = conn.execute("SELECT * FROM invoices WHERE id=?", (invoice_id,)).fetchone()
|
||||
return _row_to_dict(row)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '983'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '984'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
||||
|
|
|
|||
|
|
@ -3797,6 +3797,14 @@ window.Page_admin = (() => {
|
|||
</td>
|
||||
<td class="adm-td" style="text-align:right;font-weight:700;white-space:nowrap">
|
||||
${_fmtEur(inv.amount_gross)}
|
||||
${inv.status === 'paid' && inv.paid_amount != null && Math.abs(inv.paid_amount - inv.amount_gross) >= 0.01
|
||||
? `<div style="font-size:10px;color:var(--c-warning,#d97706);font-weight:500">
|
||||
erhalten: ${_fmtEur(inv.paid_amount)}
|
||||
${inv.paid_amount < inv.amount_gross
|
||||
? `<span style="color:var(--c-danger)">-${_fmtEur(inv.amount_gross - inv.paid_amount)}</span>`
|
||||
: ''}
|
||||
</div>`
|
||||
: ''}
|
||||
</td>
|
||||
<td class="adm-td">${_statusBadge(inv.status)}</td>
|
||||
<td class="adm-td" style="font-size:var(--text-xs);color:var(--c-text-muted);white-space:nowrap">
|
||||
|
|
@ -4091,10 +4099,13 @@ window.Page_admin = (() => {
|
|||
<input class="form-control" name="paid_at" type="date" value="${today}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" style="font-size:var(--text-xs)">Betrag (€) *</label>
|
||||
<input class="form-control" name="paid_amount" type="number" min="0" step="0.01"
|
||||
<label class="form-label" style="font-size:var(--text-xs)">Eingegangener Betrag (€) *</label>
|
||||
<input class="form-control" name="paid_amount" id="${id}-amt" type="number" min="0" step="0.01"
|
||||
value="${defaultAmount.toFixed(2)}" required>
|
||||
</div>
|
||||
<div id="${id}-diff" style="display:none;padding:var(--space-2) var(--space-3);
|
||||
border-radius:var(--radius-md);background:#fff8f0;border:1px solid #f0a060;
|
||||
font-size:var(--text-xs);color:#c05000;line-height:1.6"></div>
|
||||
</form>
|
||||
`,
|
||||
footer: `
|
||||
|
|
@ -4103,18 +4114,51 @@ window.Page_admin = (() => {
|
|||
`,
|
||||
});
|
||||
|
||||
// Differenz live anzeigen
|
||||
const amtEl = document.getElementById(`${id}-amt`);
|
||||
const diffEl = document.getElementById(`${id}-diff`);
|
||||
const _checkDiff = () => {
|
||||
const entered = parseFloat(amtEl?.value) || 0;
|
||||
const diff = defaultAmount - entered;
|
||||
if (Math.abs(diff) < 0.01) { diffEl.style.display = 'none'; return; }
|
||||
diffEl.style.display = 'block';
|
||||
if (diff > 0) {
|
||||
diffEl.innerHTML = `Differenz: <strong>-${diff.toFixed(2)} €</strong> weniger als fakturiert.<br>
|
||||
<label style="display:flex;align-items:center;gap:6px;margin-top:4px;cursor:pointer">
|
||||
<input type="checkbox" id="${id}-kulanz">
|
||||
<span>Als Kulanz/Forderungsverlust abschreiben (Notiz wird automatisch eingetragen)</span>
|
||||
</label>`;
|
||||
} else {
|
||||
diffEl.innerHTML = `Überzahlung: <strong>+${(-diff).toFixed(2)} €</strong> mehr eingegangen.`;
|
||||
diffEl.style.background = '#f0fff8';
|
||||
diffEl.style.borderColor = '#34d399';
|
||||
diffEl.style.color = '#065f46';
|
||||
}
|
||||
};
|
||||
amtEl?.addEventListener('input', _checkDiff);
|
||||
|
||||
document.getElementById(id)?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.target);
|
||||
const fd = new FormData(e.target);
|
||||
const paidAmount = parseFloat(fd.get('paid_amount'));
|
||||
const diff = defaultAmount - paidAmount;
|
||||
const kulanz = diff > 0.01 && document.getElementById(`${id}-kulanz`)?.checked;
|
||||
|
||||
const submitBtn = document.querySelector(`button[form="${id}"]`);
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
try {
|
||||
const kulanzNote = kulanz
|
||||
? `Forderungsverlust/Kulanz: ${diff.toFixed(2)} EUR nicht eingegangen (${fd.get('paid_at')}). Als Kulanz abgeschrieben.`
|
||||
: null;
|
||||
await API.post(`/admin/invoices/${invoiceId}/pay`, {
|
||||
paid_at: fd.get('paid_at'),
|
||||
paid_amount: parseFloat(fd.get('paid_amount')),
|
||||
paid_amount: paidAmount,
|
||||
...(kulanzNote ? { notes: kulanzNote } : {}),
|
||||
});
|
||||
UI.modal.close();
|
||||
UI.toast.success('Rechnung als bezahlt markiert.');
|
||||
UI.toast.success(kulanz
|
||||
? `Bezahlt (${paidAmount.toFixed(2)} €) · ${diff.toFixed(2)} € als Kulanz notiert.`
|
||||
: 'Rechnung als bezahlt markiert.');
|
||||
reload();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler.');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v983';
|
||||
const CACHE_VERSION = 'by-v984';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue