A small, dependency-light Python toolkit for reading data directly out of Sage 50 (UK/Ireland) via ODBC -- no need to export a report to CSV first. Built for scripts that need to pull invoices, customer records, or ledger data straight from a live Sage install: reminder emails, dashboards, scheduled reports, reconciliations, and so on.
If this saves you time, consider buying me a coffee.
- Connects to Sage 50 via its own ODBC driver -- nothing extra to install on the Sage side
- No secrets in code or on disk -- DSN/username/password are read from environment variables, with a safe interactive prompt (hidden password input) for anything not set
- Column-name auto-detection: Sage 50's schema varies slightly between versions, so sagekit inspects the live table rather than assuming one fixed set of column names
DELETED_FLAGauto-detection: some installs store it as numeric (0/1), others as string ('N'/'Y') -- sagekit works out which- Two worked examples: outstanding invoices, customer list
pip install -r requirements.txtor, to install as an editable package:
pip install -e .You'll also need the Sage Line 50 ODBC driver, which Sage 50 installs alongside itself -- there's nothing separate to download. Confirm it's there by searching "ODBC Data Sources (64-bit)" in the Windows Start menu; you should see a Sage-related driver listed under the "Drivers" tab, and your DSN (see below) under "System DSN" or "User DSN".
The DSN (Data Source Name) identifies which Sage company database to connect to. To find it:
- Search "ODBC Data Sources (64-bit)" in Windows and open it.
- Look under the System DSN or User DSN tab for an entry
pointing at your Sage 50 data (often named something like
SageLine50v33, with the number matching your Sage version). - That name is your
SAGE_DSN.
If you're not sure, sagekit will prompt you for it at runtime -- you don't have to set it in advance.
Copy .env.example to .env if you want to pre-fill values (useful
for scheduled/unattended scripts):
cp .env.example .envSAGE_DSN=SageLine50v33
SAGE_UID=MANAGER
SAGE_PWD=
.env is already in .gitignore -- never commit real credentials.
Leaving SAGE_PWD blank (or unset) is recommended for anything you
run by hand -- sagekit will prompt for it with hidden input instead,
so your password never sits in a file or your shell history.
To load .env automatically in your own scripts, install
python-dotenv and add this before importing sagekit:
from dotenv import load_dotenv
load_dotenv()Sagekit itself doesn't require python-dotenv -- it only reads
os.environ, plus interactive prompts for anything missing.
from sagekit import connect
conn = connect() # prompts for DSN/username/password if not set
cursor = conn.cursor()
cursor.execute("SELECT * FROM AUDIT_HEADER WHERE TYPE = 'SI'")
rows = cursor.fetchall()
conn.close()Sage 50 must be running on the machine (or reachable on the network) while you query it via ODBC -- the driver talks to the live application, not a standalone database file.
Column names vary a little between Sage 50 versions. Rather than
hardcoding one version's names, use find_column() to ask the live
table what it's actually called:
from sagekit import connect, find_column
conn = connect()
cursor = conn.cursor()
outstanding_col = find_column(cursor, "AUDIT_HEADER", "outstanding_amount")
cursor.execute(f"SELECT {outstanding_col} FROM AUDIT_HEADER WHERE TYPE = 'SI'")Known concepts and the column-name variants sagekit checks for each
(see sagekit/discovery.py to extend this list):
| Concept | Variants checked |
|---|---|
transaction_date |
DATE, INV_DATE, TRANS_DATE, DATE_ENTERED |
due_date |
DUE_DATE, DATE_DUE |
outstanding_amount |
OUTSTANDING, OS_AMOUNT, OUTSTANDING_AMOUNT |
gross_amount |
GROSS_AMOUNT, AMOUNT, FOREIGN_GROSS_AMOUNT |
deleted_flag |
DELETED_FLAG, DELETED, RECORD_DELETED |
customer_name |
NAME, COMPANY_NAME, ACCOUNT_NAME |
email |
E_MAIL, EMAIL_1, EMAIL, EMAIL1, DIRECT_DEBITS_EMAIL |
payment_due_days |
PAYMENT_DUE_DAYS, PAY_DUE_DAYS |
payment_due_from |
PAYMENT_DUE_FROM_CODE, PAY_DUE_FROM_CODE |
Or use get_columns(cursor, table) to just see everything a table has:
from sagekit import get_columns
print(get_columns(cursor, "AUDIT_HEADER"))from sagekit import detect_deleted_flag
not_deleted = detect_deleted_flag(cursor, "AUDIT_HEADER", sample_type="SI")
cursor.execute(f"SELECT * FROM AUDIT_HEADER WHERE TYPE = 'SI' AND DELETED_FLAG = {not_deleted}")| Table | Contents |
|---|---|
AUDIT_HEADER |
One row per transaction (invoices, credits, receipts). Has TYPE, INV_REF, date/due-date, outstanding/gross amount, deleted flag |
SALES_LEDGER |
One row per customer account. Has name, email, payment terms, address fields |
AUDIT_SPLIT |
Line-level detail for each transaction (product codes, quantities, nominal codes) |
STOCK |
Product / stock item records |
NOMINAL_LEDGER |
Nominal account records |
For most accounts-receivable tasks, AUDIT_HEADER joined to
SALES_LEDGER on ACCOUNT_REF is all you need -- see
examples/outstanding_invoices.py.
Common codes on the sales side (verify against your own install -- see below):
| Code | Meaning |
|---|---|
SI |
Sales Invoice |
SC |
Sales Credit Note |
SA |
Sales Payment on Account (unallocated receipt) |
These letters are generally consistent across Sage 50 installs, but custom transaction types can exist. To check what your install actually has:
from sagekit import verify_transaction_types
print(verify_transaction_types(cursor))examples/outstanding_invoices.py-- list outstanding sales invoices, joined to customer namesexamples/list_customers.py-- dump customer accounts with email addresses
Run either directly once you've set up your .env (or just answer
the prompts):
python examples/outstanding_invoices.py
python examples/list_customers.pypip install pytest
pytestTests mock pyodbc, so they run offline without a real Sage install
or ODBC driver.
| Symptom | Likely cause | Fix |
|---|---|---|
Data source name not found |
DSN name wrong or not set up | Open "ODBC Data Sources (64-bit)", check the DSN exists and matches SAGE_DSN |
Login failed |
Wrong username/password | Try logging into Sage 50 directly first to confirm credentials |
0 rows returned |
Wrong DELETED_FLAG value or TYPE code for this install |
Use detect_deleted_flag() / verify_transaction_types() |
Column not found |
Column name varies by version | Use get_columns() or find_column() to inspect actual column names |
pyodbc not installed |
Missing package | pip install pyodbc |
| Connection hangs or times out | Sage 50 not running | Sage 50 must be open on the same machine (or accessible on the network) while querying via ODBC |
- Use the 64-bit ODBC driver with 64-bit Python -- mixing 32-bit and 64-bit causes connection failures.
- Credentials are never written to disk by sagekit itself -- they
live only in memory for the duration of a script (unless you choose
to put them in your own
.envfile for unattended use, at your own risk).
MIT -- see LICENSE. Do whatever you like with this; a credit or a Ko-fi tip is appreciated but never required.