import sys
import os
import csv
import io
import json
import threading
import webbrowser
import requests
import webview
from datetime import datetime
from flask import Flask, render_template_string, request, jsonify, Response, send_file
from num2words import num2words
# ReportLab for PDF receipt generation
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
app = Flask(_name_)
# ==========================================
# COMPANY & MANAGER.IO API CONFIGURATION
# ==========================================
COMPANY_NAME = βSVL (Pvt) Ltd.β
BUSINESS_KEY = βogYOU1ZMIChQdnQpIEx0ZC4β
MANAGER_URL = βhttps://mydomainname.manager.ioβ
API_KEY = βmy access tokenβ
TRANSACTIONS = []
def parse_amount(val):
if isinstance(val, dict):
for sub_k in \['number', 'value', 'amount'\]:
if sub_k in val and val\[sub_k\] is not None:
return parse_amount(val\[sub_k\])
return 0.0
try:
return float(val)
except (ValueError, TypeError):
return 0.0
def amount_to_words(amount):
try:
val = parse_amount(amount)
rupees = int(val)
cents = int(round((val - rupees) \* 100))
words = num2words(rupees, lang='en').title().replace("-", " ")
if cents > 0:
return f"\*\*\*{words} Rupees and {cents:02d}/100 Only\*\*\*"
else:
return f"\*\*\*{words} Rupees Only\*\*\*"
except Exception:
return ""
def get_auth_session():
"""Configures requests session for Manager.io Cloud API."""
session = requests.Session()
session.auth = (API_KEY, '')
session.headers.update({
"Accept": "application/json",
"Content-Type": "application/json"
})
return session
# ==========================================
# MANAGER.IO INVOICE & RECEIPT LINKING
# ==========================================
def fetch_invoices_from_manager(query=ββ, start_dt=ββ, end_dt=ββ):
"""Fetches sales invoices from Manager.io to allow settlement at POS."""
session = get_auth_session()
url = f"{MANAGER_URL}/api/{BUSINESS_KEY}/sales-invoices"
try:
res = session.get(url, timeout=10)
if res.status_code != 200:
return \[\], f"HTTP {res.status_code}: {res.text\[:100\]}"
data = res.json()
raw_invoices = data.get('salesInvoices', \[\]) if isinstance(data, dict) else data
if not raw_invoices and isinstance(data, dict):
raw_invoices = list(data.values())
invoices = \[\]
for inv in raw_invoices:
if not isinstance(inv, dict):
continue
inv_key = inv.get('Key') or inv.get('key')
inv_num = inv.get('Reference') or inv.get('reference') or inv.get('InvoiceNumber') or 'N/A'
inv_date = str(inv.get('IssueDate') or inv.get('date') or inv.get('Date') or '')
amt = parse_amount(inv.get('InvoiceAmount') or inv.get('Amount') or inv.get('amount'))
customer = inv.get('Customer') or 'General Customer'
if isinstance(customer, dict):
customer = customer.get('Name') or customer.get('name') or 'General Customer'
lines = inv.get('Lines', \[\])
\# Safe String Filter Matching
if query and (query.lower() not in str(inv_num).lower() and query.lower() not in str(customer).lower()):
continue
if start_dt and inv_date and inv_date < start_dt.split("T")\[0\]:
continue
if end_dt and inv_date and inv_date > end_dt.split("T")\[0\]:
continue
invoices.append({
"key": inv_key,
"invoice_number": inv_num,
"date": inv_date,
"customer": customer,
"amount": amt,
"lines": lines
})
return invoices, f"OK ({len(invoices)} Invoices)"
except Exception as e:
return \[\], str(e)
def link_receipt_to_manager_invoice(invoice_key, pos_ref, amount, payment_method):
"""Creates a payment receipt in Manager.io linked directly to the Sales Invoice."""
session = get_auth_session()
url = f"{MANAGER_URL}/api/{BUSINESS_KEY}/receipts"
payload = {
"Date": datetime.now().strftime("%Y-%m-%d"),
"Reference": pos_ref,
"Description": f"POS Settlement ({pos_ref}) - Method: {payment_method}",
"Lines": \[
{
"SalesInvoice": invoice_key,
"Amount": amount
}
\],
"Amount": amount
}
try:
res = session.post(url, json=payload, timeout=10)
if res.status_code in (200, 201):
res_data = res.json() if res.text else {}
key = res_data.get('Key') or res_data.get('key') or pos_ref
return True, key
else:
return False, f"HTTP {res.status_code}: {res.text\[:120\]}"
except Exception as e:
return False, str(e)
# ==========================================
# UI TEMPLATES
# ==========================================
POS_TEMPLATE = ββ"
<title>POS Settlement Terminal - {{ company_name }}</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f4f6f9; margin: 0; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; background: white; padding: 25px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.header-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
h2 { color: #1e293b; margin: 0 0 5px 0; }
.subtitle { color: #64748b; font-weight: 500; }
.pos-grid { display: grid; grid-template-columns: 1fr 480px; gap: 20px; }
.panel { background: #fff; border: 1px solid #e2e8f0; border-radius: 6px; padding: 15px; }
.filter-panel { background: #f8fafc; border: 1px solid #cbd5e1; padding: 12px; border-radius: 6px; margin-bottom: 15px; display: flex; gap: 10px; align-items: flex-end; }
.filter-group { display: flex; flex-direction: column; gap: 4px; flex: 1; }
.filter-group label { font-size: 11px; font-weight: bold; color: #475569; }
.filter-input { padding: 8px; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 13px; }
.invoice-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
.invoice-table th, .invoice-table td { padding: 10px; border-bottom: 1px solid #e2e8f0; text-align: left; font-size: 13px; }
.invoice-table th { background: #f1f5f9; color: #475569; }
.invoice-row { cursor: pointer; }
.invoice-row:hover { background: #eff6ff; }
.invoice-row.selected { background: #dbeafe; border-left: 4px solid #2563eb; }
.totals-box { background: #f8fafc; padding: 12px; border-radius: 6px; margin-bottom: 15px; border: 1px solid #e2e8f0; }
.total-row.grand { font-size: 18px; font-weight: bold; color: #1e293b; }
.cash-box { background: #eff6ff; border: 1px solid #bfdbfe; padding: 12px; border-radius: 6px; margin-bottom: 15px; }
.cash-box label { font-size: 12px; font-weight: bold; color: #1e40af; display: block; margin-bottom: 4px; }
.cash-input { width: 100%; padding: 8px; border: 1px solid #93c5fd; border-radius: 4px; font-size: 16px; font-weight: bold; color: #1e293b; box-sizing: border-box; }
.change-display { font-size: 15px; font-weight: bold; color: #166534; margin-top: 8px; display: flex; justify-content: space-between; }
.payment-method { display: flex; gap: 10px; margin-bottom: 12px; }
.pay-btn { flex: 1; padding: 8px; border: 1px solid #cbd5e1; background: #f8fafc; border-radius: 4px; font-weight: bold; font-size: 12px; cursor: pointer; text-align: center; }
.pay-btn.active { background: #2563eb; color: white; border-color: #2563eb; }
.btn-action { width: 100%; padding: 12px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 15px; margin-top: 8px; }
.btn-checkout { background: #16a34a; color: white; }
.btn-filter { padding: 8px 14px; background: #2563eb; color: white; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; }
.nav-bar { display: flex; gap: 10px; margin-bottom: 15px; }
.nav-btn { padding: 8px 16px; background: #e2e8f0; color: #334155; border-radius: 6px; text-decoration: none; font-weight: 600; font-size: 13px; }
.nav-btn.active { background: #2563eb; color: white; }
.status-sync { background: #dcfce7; color: #166534; padding: 6px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; display: inline-block; margin-bottom: 10px; }
.status-err { background: #fef2f2; color: #991b1b; padding: 6px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; display: inline-block; margin-bottom: 10px; }
#debugBox { display: none; background: #1e293b; color: #f8fafc; padding: 12px; border-radius: 6px; margin-bottom: 15px; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; }
</style>
<div class="header-row">
<div>
<h2>POS Invoice Settlement & Cash Audit</h2>
<div class="subtitle">{{ company_name }}</div>
</div>
<div>
<a href="http://127.0.0.1:5000" target="\_blank" style="font-size: 13px; text-decoration: none; font-weight: bold; color: #0284c7;">Open in Browser</a>
</div>
</div>
<div class="nav-bar">
<a href="/" class="nav-btn active">POS Settlement</a>
<a href="/history" class="nav-btn">Cash Audit Ledger</a>
<a href="/export-excel" target="\_blank" class="nav-btn" style="background:#16a34a; color:white;">Export Excel</a>
<a href="/export-pdf" target="\_blank" class="nav-btn" style="background:#dc2626; color:white;">Export PDF</a>
</div>
<div id="debugBox"></div>
<div class="pos-grid">
<div class="panel">
{% if invoices|length > 0 %}
<div class="status-sync">β Manager.io Cloud Connected ({{ invoices|length }} Invoices Found)</div>
{% else %}
<div class="status-err">β οΈ Status: {{ api_status }}</div>
{% endif %}
<form method="GET" action="/" class="filter-panel">
<div class="filter-group">
<label>Search Invoice / Customer</label>
<input type="text" name="query" class="filter-input" placeholder="Invoice # or Customer..." value="{{ query }}">
</div>
<div class="filter-group">
<label>Start Date</label>
<input type="date" name="start_dt" class="filter-input" value="{{ start_dt }}">
</div>
<div class="filter-group">
<label>End Date</label>
<input type="date" name="end_dt" class="filter-input" value="{{ end_dt }}">
</div>
<button type="submit" class="btn-filter">Filter</button>
</form>
<div style="max-height: 480px; overflow-y: auto;">
<table class="invoice-table">
<thead>
<tr>
<th>Date</th>
<th>Invoice #</th>
<th>Customer</th>
<th>Amount (LKR)</th>
</tr>
</thead>
<tbody>
{% for inv in invoices %}
<tr class="invoice-row" onclick="selectInvoice('{{ inv.key }}', '{{ inv.invoice_number }}', '{{ inv.customer|escape }}', {{ inv.amount }})">
<td>{{ inv.date }}</td>
<td><strong>{{ inv.invoice_number }}</strong></td>
<td>{{ inv.customer }}</td>
<td>Rs. {{ "{:,.2f}".format(inv.amount) }}</td>
</tr>
{% endfor %}
{% if invoices|length == 0 %}
<tr><td colspan="4" style="text-align: center; color: #64748b; padding: 20px;">No invoices found matching criteria.</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<div class="panel">
<h3 style="margin-top: 0; color: #1e293b;">Settlement Details</h3>
<div class="totals-box">
<div style="margin-bottom: 6px; font-size: 13px; color: #475569;">
<span>Selected Invoice: </span><strong id="selectedInvNum">None</strong>
</div>
<div style="margin-bottom: 6px; font-size: 13px; color: #475569;">
<span>Customer: </span><strong id="selectedCustomer">N/A</strong>
</div>
<div class="total-row grand">
<span>Amount Due:</span>
<span id="grandTotal">Rs. 0.00</span>
</div>
</div>
<div class="cash-box">
<label>PAYMENT METHOD</label>
<div class="payment-method">
<div class="pay-btn active" id="pay-CASH" onclick="setPaymentMethod('CASH')">CASH</div>
<div class="pay-btn" id="pay-CARD" onclick="setPaymentMethod('CARD')">CARD</div>
<div class="pay-btn" id="pay-ONLINE" onclick="setPaymentMethod('ONLINE')">ONLINE</div>
</div>
<div id="cashRenderedGroup">
<label>CASH RENDERED (LKR)</label>
<input type="number" id="cashRendered" class="cash-input" placeholder="0.00" onkeyup="calculateChange()" onchange="calculateChange()">
<div class="change-display">
<span>Change Returned:</span>
<span id="changeReturned">Rs. 0.00</span>
</div>
</div>
</div>
<button class="btn-action btn-checkout" id="btnCheckout" onclick="processSettlement()">Complete Settlement & Link Reference</button>
</div>
</div>
ββ"
RECEIPT_TEMPLATE = ββ"
<title>POS Receipt - {{ pos_ref }}</title>
<style>
body { font-family: 'Courier New', monospace; margin: 0; padding: 20px; background: #fff; }
.receipt-box { width: 80mm; margin: 0 auto; padding: 10px; border: 1px dashed #ccc; }
.header { text-align: center; margin-bottom: 10px; }
.title { font-weight: bold; font-size: 15px; }
.sub { font-size: 11px; color: #555; }
.line { border-bottom: 1px dashed #000; margin: 8px 0; }
table { width: 100%; font-size: 12px; }
.num { text-align: right; }
.words { font-size: 10px; margin-top: 8px; font-style: italic; }
.no-print { text-align: center; margin-bottom: 15px; }
.btn-print { padding: 8px 16px; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer; }
@media print { .no-print { display: none; } .receipt-box { border: none; } }
</style>
<button onclick="window.print()" class="btn-print">π¨οΈ Print Receipt</button>
<div class="header">
<div class="title">{{ company_name }}</div>
<div class="sub">Official Payment Receipt</div>
<div class="sub">POS Ref: {{ pos_ref }}</div>
<div class="sub">Manager Invoice Ref: {{ invoice_number }}</div>
<div class="sub">Date/Time: {{ date }}</div>
</div>
<div class="line"></div>
<table>
<tr><td>Customer:</td><td class="num">{{ customer }}</td></tr>
<tr><td><strong>TOTAL SETTLED:</strong></td><td class="num"><strong>Rs. {{ "{:,.2f}".format(total) }}</strong></td></tr>
<tr><td>Payment Method:</td><td class="num">{{ payment_method }}</td></tr>
{% if payment_method == 'CASH' %}
<tr><td>Cash Rendered:</td><td class="num">Rs. {{ "{:,.2f}".format(cash_rendered) }}</td></tr>
<tr><td>Change Returned:</td><td class="num">Rs. {{ "{:,.2f}".format(change_returned) }}</td></tr>
{% endif %}
</table>
<div class="line"></div>
<div class="words">{{ words }}</div>
<div class="line"></div>
<div class="header" style="font-size: 10px; margin-top: 10px;">Payment Linked to Manager.io Cloud</div>
ββ"
LEDGER_TEMPLATE = ββ"
<title>Cash Audit Ledger - {{ company_name }}</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f4f6f9; margin: 0; padding: 20px; }
.container { max-width: 1300px; margin: 0 auto; background: white; padding: 25px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
h2 { color: #1e293b; margin-top: 0; }
.nav-bar { display: flex; gap: 10px; margin-bottom: 20px; }
.nav-btn { padding: 8px 16px; background: #e2e8f0; color: #334155; border-radius: 6px; text-decoration: none; font-weight: 600; font-size: 13px; }
.nav-btn.active { background: #2563eb; color: white; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; text-align: left; font-size: 13px; }
th { background: #f1f5f9; color: #475569; }
.audit-pill { background: #f1f5f9; border: 1px solid #cbd5e1; padding: 3px 6px; border-radius: 4px; font-size: 11px; font-weight: bold; }
</style>
<h2>POS Cash Rendered Audit Ledger</h2>
<div class="nav-bar">
<a href="/" class="nav-btn">POS Settlement</a>
<a href="/history" class="nav-btn active">Cash Audit Ledger</a>
<a href="/export-excel" target="\_blank" class="nav-btn" style="background:#16a34a; color:white;">Export Excel</a>
<a href="/export-pdf" target="\_blank" class="nav-btn" style="background:#dc2626; color:white;">Export PDF</a>
</div>
<table>
<thead>
<tr>
<th>Date / Time</th>
<th>POS Ref</th>
<th>Manager Invoice #</th>
<th>Customer</th>
<th>Method</th>
<th>Settled Amt</th>
<th>Cash Rendered</th>
<th>Change Returned</th>
<th>Receipt</th>
</tr>
</thead>
<tbody>
{% for t in transactions %}
<tr>
<td>{{ t.date }}</td>
<td><strong>{{ t.pos_ref }}</strong></td>
<td>{{ t.invoice_number }}</td>
<td>{{ t.customer }}</td>
<td><span class="audit-pill">{{ t.payment_method }}</span></td>
<td>Rs. {{ "{:,.2f}".format(t.total) }}</td>
<td>Rs. {{ "{:,.2f}".format(t.cash_rendered) }}</td>
<td><span style="color: #166534; font-weight: bold;">Rs. {{ "{:,.2f}".format(t.change_returned) }}</span></td>
<td><a href="/receipt?ref={{ t.pos_ref }}" target="\_blank" style="color:#2563eb; font-weight:bold;">Receipt</a></td>
</tr>
{% endfor %}
{% if transactions|length == 0 %}
<tr><td colspan="9" style="text-align: center; color: #64748b; padding: 25px;">No settlement audit records logged yet.</td></tr>
{% endif %}
</tbody>
</table>
ββ"
# ==========================================
# FLASK ROUTES
# ==========================================
@app.route(β/β)
def index():
query = request.args.get('query', '')
start_dt = request.args.get('start_dt', '')
end_dt = request.args.get('end_dt', '')
invoices, status = fetch_invoices_from_manager(query, start_dt, end_dt)
return render_template_string(
POS_TEMPLATE,
invoices=invoices,
api_status=status,
company_name=COMPANY_NAME,
query=query,
start_dt=start_dt,
end_dt=end_dt
)
@app.route(β/historyβ)
def history():
return render_template_string(LEDGER_TEMPLATE, transactions=TRANSACTIONS, company_name=COMPANY_NAME)
@app.route(β/settle-invoiceβ, methods=[βPOSTβ])
def settle_invoice():
try:
data = request.get_json(force=True) or {}
inv_key = data.get('invoice_key')
inv_num = data.get('invoice_number')
customer = data.get('customer')
amount = float(data.get('amount', 0.0))
payment_method = data.get('payment_method', 'CASH')
cash_rendered = float(data.get('cash_rendered', 0.0))
change_returned = float(data.get('change_returned', 0.0))
if not inv_key:
return jsonify({"success": False, "error": "No invoice selected."})
pos_ref = f"POS-{inv_num}-{int(datetime.now().timestamp())}"
success, ref_or_err = link_receipt_to_manager_invoice(inv_key, pos_ref, amount, payment_method)
if not success:
return jsonify({"success": False, "error": ref_or_err})
date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
tx = {
"pos_ref": pos_ref,
"invoice_number": inv_num,
"customer": customer,
"date": date_str,
"total": amount,
"payment_method": payment_method,
"cash_rendered": cash_rendered if payment_method == 'CASH' else amount,
"change_returned": change_returned if payment_method == 'CASH' else 0.0
}
TRANSACTIONS.insert(0, tx)
return jsonify({"success": True, "pos_ref": pos_ref})
except Exception as e:
return jsonify({"success": False, "error": str(e)})
@app.route(β/receiptβ)
def print_receipt():
ref = request.args.get('ref', '')
tx = next((t for t in TRANSACTIONS if t\['pos_ref'\] == ref), None)
if not tx:
return "Receipt not found.", 404
words = amount_to_words(tx\['total'\])
return render_template_string(
RECEIPT_TEMPLATE,
company_name=COMPANY_NAME,
pos_ref=tx\['pos_ref'\],
invoice_number=tx\['invoice_number'\],
customer=tx\['customer'\],
date=tx\['date'\],
total=tx\['total'\],
payment_method=tx\['payment_method'\],
cash_rendered=tx\['cash_rendered'\],
change_returned=tx\['change_returned'\],
words=words
)
@app.route(β/export-excelβ)
def export_excel():
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(\['Date/Time', 'POS Ref', 'Manager Invoice #', 'Customer', 'Payment Method', 'Settled (LKR)', 'Cash Rendered (LKR)', 'Change Returned (LKR)'\])
for t in TRANSACTIONS:
writer.writerow(\[
t\['date'\],
t\['pos_ref'\],
t\['invoice_number'\],
t\['customer'\],
t\['payment_method'\],
f"{t\['total'\]:.2f}",
f"{t\['cash_rendered'\]:.2f}",
f"{t\['change_returned'\]:.2f}"
\])
output.seek(0)
filename = f"POS_Cash_Audit\_{datetime.now().strftime('%Y%m%d\_%H%M%S')}.csv"
return Response(
output.getvalue(),
mimetype="text/csv",
headers={"Content-Disposition": f"attachment;filename={filename}"}
)
@app.route(β/export-pdfβ)
def export_pdf():
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=letter)
styles = getSampleStyleSheet()
elements = \[\]
title_style = ParagraphStyle('DocTitle', parent=styles\['Heading1'\], fontSize=16, leading=20, textColor=colors.HexColor('#1e293b'))
subtitle_style = ParagraphStyle('DocSubtitle', parent=styles\['Normal'\], fontSize=11, leading=14, textColor=colors.HexColor('#64748b'))
elements.append(Paragraph(f"{COMPANY_NAME} - Cash Audit Report", title_style))
elements.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", subtitle_style))
elements.append(Spacer(1, 15))
table_data = \[\['Date/Time', 'POS Ref', 'Invoice #', 'Method', 'Settled', 'Rendered', 'Change'\]\]
for t in TRANSACTIONS:
table_data.append(\[
t\['date'\],
t\['pos_ref'\],
t\['invoice_number'\],
t\['payment_method'\],
f"Rs. {t\['total'\]:,.2f}",
f"Rs. {t\['cash_rendered'\]:,.2f}",
f"Rs. {t\['change_returned'\]:,.2f}"
\])
t = Table(table_data, colWidths=\[100, 110, 70, 60, 70, 70, 70\])
t.setStyle(TableStyle(\[
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2563eb')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), 9),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#cbd5e1')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 8),
('ROWBACKGROUNDS', (0, 1), (-1, -1), \[colors.white, colors.HexColor('#f8fafc')\])
\]))
elements.append(t)
doc.build(elements)
buffer.seek(0)
filename = f"POS_Cash_Audit\_{datetime.now().strftime('%Y%m%d\_%H%M%S')}.pdf"
return send_file(
buffer,
as_attachment=True,
download_name=filename,
mimetype='application/pdf'
)
def run_flask():
app.run(host="127.0.0.1", port=5000, debug=False, use_reloader=False)
if _name_ == β_main_β:
t = threading.Thread(target=run_flask)
t.daemon = True
t.start()
window = webview.create_window(
title=f"POS Invoice Settlement - {COMPANY_NAME}",
url="http://127.0.0.1:5000",
width=1350,
height=850,
resizable=True
)
webview.start()
sys.exit()
Above is a simple POS I tried to create. However, 401 error comes. Can you kindly help on this script to avoid this error?