mirror of
https://github.com/kbenestad/ClubLedger.git
synced 2026-06-18 09:44:33 +00:00
feat: admin-configurable default paper size for receipts/statements
Adds a Paper Size setting (A4/A5) to the General section of Admin settings. Receipts and statements pre-select the configured size and apply the correct @page margins; staff can still override per-print. https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
This commit is contained in:
parent
6aa4c45616
commit
7b4e33254c
3 changed files with 31 additions and 14 deletions
36
main.py
36
main.py
|
|
@ -88,6 +88,8 @@ CONFIG = {
|
||||||
"receipt_footer_cashier": "",
|
"receipt_footer_cashier": "",
|
||||||
# Timezone for display (IANA name); defaults to server local timezone
|
# Timezone for display (IANA name); defaults to server local timezone
|
||||||
"timezone": _server_timezone(),
|
"timezone": _server_timezone(),
|
||||||
|
# Default paper size for receipts and statements
|
||||||
|
"paper_size": "A4",
|
||||||
}
|
}
|
||||||
|
|
||||||
DB_PATH = "clubledger.db"
|
DB_PATH = "clubledger.db"
|
||||||
|
|
@ -499,6 +501,8 @@ class AppSettingsUpdate(BaseModel):
|
||||||
receipt_footer_cashier: Optional[str] = None
|
receipt_footer_cashier: Optional[str] = None
|
||||||
# Timezone
|
# Timezone
|
||||||
timezone: Optional[str] = None
|
timezone: Optional[str] = None
|
||||||
|
# Default paper size
|
||||||
|
paper_size: Optional[str] = None
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Page routes
|
# Page routes
|
||||||
|
|
@ -737,20 +741,24 @@ def transactions(member_id: int, limit: int = 50, offset: int = 0,
|
||||||
# Print views (no auth – opened as new-tab popups)
|
# Print views (no auth – opened as new-tab popups)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _print_size_script():
|
def _print_size_script(s: dict):
|
||||||
return """<script>
|
size = "A5" if (s.get("paper_size") or "A4").upper() == "A5" else "A4"
|
||||||
function setSize(s){
|
return f"""<script>
|
||||||
|
function setSize(s){{
|
||||||
var el=document.getElementById('psStyle');
|
var el=document.getElementById('psStyle');
|
||||||
if(!el){el=document.createElement('style');el.id='psStyle';document.head.appendChild(el);}
|
if(!el){{el=document.createElement('style');el.id='psStyle';document.head.appendChild(el);}}
|
||||||
el.textContent='@media print{@page{size:'+s+';margin:'+(s==='A5'?'10mm':'16mm')+';}}';}
|
el.textContent='@media print{{@page{{size:'+s+';margin:'+(s==='A5'?'10mm':'16mm')+';}}}}';}}
|
||||||
setSize('A4');
|
setSize('{size}');
|
||||||
</script>"""
|
</script>"""
|
||||||
|
|
||||||
def _print_controls():
|
def _print_controls(s: dict):
|
||||||
return """<div class="no-print controls">
|
size = "A5" if (s.get("paper_size") or "A4").upper() == "A5" else "A4"
|
||||||
|
a4_chk = ' checked' if size == "A4" else ''
|
||||||
|
a5_chk = ' checked' if size == "A5" else ''
|
||||||
|
return f"""<div class="no-print controls">
|
||||||
<span class="size-label">Paper:</span>
|
<span class="size-label">Paper:</span>
|
||||||
<label><input type="radio" name="ps" value="A4" checked onchange="setSize('A4')"> A4</label>
|
<label><input type="radio" name="ps" value="A4"{a4_chk} onchange="setSize('A4')"> A4</label>
|
||||||
<label><input type="radio" name="ps" value="A5" onchange="setSize('A5')"> A5</label>
|
<label><input type="radio" name="ps" value="A5"{a5_chk} onchange="setSize('A5')"> A5</label>
|
||||||
<button class="print-btn" onclick="window.print()">Print</button>
|
<button class="print-btn" onclick="window.print()">Print</button>
|
||||||
</div>"""
|
</div>"""
|
||||||
|
|
||||||
|
|
@ -905,7 +913,7 @@ def statement(member_id: int):
|
||||||
|
|
||||||
return f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
return f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
||||||
<title>Statement — {member['name']}</title><style>{RECEIPT_CSS}</style></head><body>
|
<title>Statement — {member['name']}</title><style>{RECEIPT_CSS}</style></head><body>
|
||||||
{_print_controls()}
|
{_print_controls(s)}
|
||||||
{_biz_header_html(s)}
|
{_biz_header_html(s)}
|
||||||
<hr>
|
<hr>
|
||||||
<h2>Account Statement</h2>
|
<h2>Account Statement</h2>
|
||||||
|
|
@ -919,7 +927,7 @@ def statement(member_id: int):
|
||||||
</tr></thead><tbody>{rows_html}</tbody></table>
|
</tr></thead><tbody>{rows_html}</tbody></table>
|
||||||
<div class="balance-box">Current Balance: {fmt(bal)}</div>
|
<div class="balance-box">Current Balance: {fmt(bal)}</div>
|
||||||
{('<div class="footer">' + footer + '</div>') if footer else ''}
|
{('<div class="footer">' + footer + '</div>') if footer else ''}
|
||||||
{_print_size_script()}</body></html>"""
|
{_print_size_script(s)}</body></html>"""
|
||||||
|
|
||||||
@app.get("/receipt/{entry_id}", response_class=HTMLResponse)
|
@app.get("/receipt/{entry_id}", response_class=HTMLResponse)
|
||||||
def receipt(entry_id: int):
|
def receipt(entry_id: int):
|
||||||
|
|
@ -1008,14 +1016,14 @@ def receipt(entry_id: int):
|
||||||
|
|
||||||
return f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
return f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
||||||
<title>Receipt — {member['name']}</title><style>{RECEIPT_CSS}</style></head><body>
|
<title>Receipt — {member['name']}</title><style>{RECEIPT_CSS}</style></head><body>
|
||||||
{_print_controls()}
|
{_print_controls(s)}
|
||||||
{_biz_header_html(s)}
|
{_biz_header_html(s)}
|
||||||
<hr>
|
<hr>
|
||||||
<div class="rx-title">{title}</div>
|
<div class="rx-title">{title}</div>
|
||||||
{body_html}
|
{body_html}
|
||||||
<hr>
|
<hr>
|
||||||
{('<div class="footer">' + footer + '</div>') if footer else ''}
|
{('<div class="footer">' + footer + '</div>') if footer else ''}
|
||||||
{_print_size_script()}</body></html>"""
|
{_print_size_script(s)}</body></html>"""
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Products
|
# Products
|
||||||
|
|
|
||||||
|
|
@ -439,6 +439,7 @@ async function loadAdminSettings() {
|
||||||
document.getElementById('s-max-charge').value = ((s.max_charge || 0) / div).toFixed(2);
|
document.getElementById('s-max-charge').value = ((s.max_charge || 0) / div).toFixed(2);
|
||||||
document.getElementById('s-overdraft-policy').value = s.overdraft_policy || 'never';
|
document.getElementById('s-overdraft-policy').value = s.overdraft_policy || 'never';
|
||||||
document.getElementById('s-timezone').value = s.timezone || '';
|
document.getElementById('s-timezone').value = s.timezone || '';
|
||||||
|
document.getElementById('s-paper-size').value = s.paper_size || 'A4';
|
||||||
document.getElementById('s-min-hint').textContent = `in ${majorUnit}`;
|
document.getElementById('s-min-hint').textContent = `in ${majorUnit}`;
|
||||||
document.getElementById('s-max-hint').textContent = `in ${majorUnit}`;
|
document.getElementById('s-max-hint').textContent = `in ${majorUnit}`;
|
||||||
document.getElementById('s-charge-hint').textContent= `in ${majorUnit}`;
|
document.getElementById('s-charge-hint').textContent= `in ${majorUnit}`;
|
||||||
|
|
@ -502,6 +503,7 @@ async function saveSettings() {
|
||||||
max_charge: Math.round(parseFloat(_sv('s-max-charge')) * div),
|
max_charge: Math.round(parseFloat(_sv('s-max-charge')) * div),
|
||||||
overdraft_policy: _sv('s-overdraft-policy'),
|
overdraft_policy: _sv('s-overdraft-policy'),
|
||||||
timezone: _svt('s-timezone'),
|
timezone: _svt('s-timezone'),
|
||||||
|
paper_size: _sv('s-paper-size'),
|
||||||
// Business address
|
// Business address
|
||||||
biz_address1: _svt('s-biz-address1'),
|
biz_address1: _svt('s-biz-address1'),
|
||||||
biz_address2: _svt('s-biz-address2'),
|
biz_address2: _svt('s-biz-address2'),
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,13 @@
|
||||||
<label>Timezone <span class="label-hint">(IANA name, e.g. Europe/London, Asia/Bangkok — default is server timezone)</span></label>
|
<label>Timezone <span class="label-hint">(IANA name, e.g. Europe/London, Asia/Bangkok — default is server timezone)</span></label>
|
||||||
<input type="text" id="s-timezone" placeholder="e.g. Europe/London">
|
<input type="text" id="s-timezone" placeholder="e.g. Europe/London">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Default paper size <span class="label-hint">(for receipts and statements)</span></label>
|
||||||
|
<select id="s-paper-size">
|
||||||
|
<option value="A4">A4</option>
|
||||||
|
<option value="A5">A5</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel-divider"></div>
|
<div class="panel-divider"></div>
|
||||||
<h3 class="sub-heading">Business Address</h3>
|
<h3 class="sub-heading">Business Address</h3>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue