Tally POS

A point-of-sale and inventory system for shops, cafés and small retailers. It runs on ordinary PHP/MySQL hosting, in English, French and Arabic (right-to-left), and works on desktop, tablet and phone.

What's included

  • Cashier screen: barcode or name search, camera barcode scanner, cart, line and invoice discounts, tax, held (parked) sales, customers, cash received with change to give back.
  • Customer credit ("kridi"): sell on account, credit limits, record payments, customer statements, total owed at a glance.
  • Import products from Excel/CSV with a check of every row before saving.
  • One-click updates: upload the new files, open the site, press Update. Your data stays.
  • Invoices as A4 pages or PDF, and 58 mm / 80 mm thermal receipts.
  • Products with photos, categories, suppliers, expiry dates, low-stock alerts, barcode label printing.
  • Stock-in, bulk shipment receiving, full stock-movement history.
  • Refunds (full or partial) that put stock back.
  • Reports with real profit (sale price minus the cost price frozen at the time of sale), charts, CSV export.
  • Admin and cashier roles, activity log, login brute-force protection, one-click database backup.
  • Web installer, English / French / Arabic interface, demo-data generator, demo mode.

Requirements

PHP8.0 or newer (developed and tested on 8.3). Extensions: pdo_mysql, mbstring, dom, json. Recommended: gd.
DatabaseMySQL 5.7+ or MariaDB 10.3+ (tested on MariaDB 10.11).
Web serverApache (with .htaccess enabled) or Nginx. Any shared hosting with cPanel works.
BrowserA current Chrome, Edge, Firefox or Safari. The camera scanner needs HTTPS.
InternetNot needed at runtime: Bootstrap, icons, charts and fonts are bundled.

The installer checks all of this for you on its first screen.

Installation

Shared hosting (cPanel)

  1. In cPanel open MySQL® Databases. Create a database, create a user with a strong password, and add the user to the database with All Privileges. Write the three values down.
  2. Open File Manager and go to public_html (or a sub-folder such as public_html/pos).
  3. Upload the contents of the upload folder of this package (or upload the ZIP of it and click Extract). You should see index.php, admin, install… directly in the target folder.
  4. Visit https://your-domain.com/ (or /pos/). You are sent to the installer automatically.
  5. Follow the three steps: requirements → database details → store name and your admin account.
  6. When it finishes, delete the install folder and sign in.
Until the install folder is deleted, every admin sees a red warning bar on every page. The installer itself is locked after a successful install, but delete it anyway.

Sub-folder or root — no configuration

The app detects the folder it lives in. Installing at example.com/ or example.com/shop/ needs no edits.

Local computer (XAMPP, Laragon, MAMP)

Copy the upload contents to a folder inside htdocs / www, create an empty database in phpMyAdmin (utf8mb4), and open http://localhost/your-folder/.

VPS with Nginx

location / { try_files $uri $uri/ /index.php?$args; }

# never serve internals
location ~ ^/(config|includes|sql|lang|vendor|licenses)/ { deny all; }
location ~* \.(sql|md|log|lock)$ { deny all; }

# uploaded files must never execute
location ~* ^/assets/uploads/.*\.(php|phtml|phar|svg|html?)$ { deny all; }

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;   # match your PHP version
}

On Nginx the bundled .htaccess files are ignored, so the deny rules above are required.

Manual configuration (optional)

If you can't use the installer, import sql/schema.sql in phpMyAdmin, create your admin user by hand, copy config/config.sample.php to config/config.php and fill it in. The installer is the recommended route: it hashes your password correctly and rolls back if anything fails.

First steps

  1. Settings — store name, address, phone, currency symbol, tax rate, logo, receipt options.
  2. Categories and Suppliers (optional).
  3. Products — add them one by one, or use Receive shipment to scan barcodes and add many at once.
  4. Users — create an account for each cashier.
  5. Open the POS screen and ring up a test sale.

If you ticked Add sample data during install, use Demo data → Reset the database before going live to remove the samples in one step.

Users & roles

AdminEverything: products, stock, reports, refunds, users, settings, backup.
CashierThe POS screen, invoices/receipts, and Customer credit (to record payments). Cannot change prices or stock, and is sent straight to the POS after signing in.
  • Passwords need at least 8 characters and are stored hashed.
  • After 5 wrong passwords the email + IP pair is locked for 15 minutes.
  • Deactivating a user signs them out within a minute.
  • A user who has made sales can't be deleted (invoices point to them); they are deactivated instead.
  • You can't delete, demote or deactivate the last active admin.
  • Forgot the admin password? Ask another admin to reset it in Users. If nobody can sign in, run in phpMyAdmin: UPDATE users SET password='…' WHERE id=1; using a bcrypt hash generated by any PHP password_hash() tool.

Products & stock

  • Barcode — type/scan one, or press Generate for a valid EAN-13. Barcodes are unique.
  • Sell price / cost price — cost price is what makes the profit figures in reports correct.
  • Minimum quantity — at or below it, the product appears in the low-stock banner and dashboard list.
  • Expiry date — rows turn yellow within 7 days and red when expired.
  • Photos — JPG, PNG or WEBP up to 2 MB. Files are checked by content, not by name.
  • Deleting — a product that was never sold is removed together with its stock-in history. A product that appears on past invoices is deactivated instead: it disappears from the POS but old invoices and reports stay correct. Saving it again re-activates it.
  • Stock in adds quantity to one product; Receive shipment handles a whole delivery, creating new products on the fly.
  • Import a whole catalogue from a spreadsheet: see Import products.
  • Print labels — barcode labels for shelves.

Selling

  1. Scan a barcode into the search box and press Enter, or type a name and click the result. The camera button scans with a phone or webcam (HTTPS required).
  2. Adjust quantities, add a per-line discount or an invoice discount. Tax is added from Settings after discounts.
  3. Optionally choose a customer, pick the payment method, and press Checkout.
  4. Cash: type the amount the customer hands over (or press Exact, or tap the 20 / 50 / 100 / 200 buttons — tapping 100 twice means 200). The screen shows the change to give back, in green, or what is still missing, in red. The change also prints on the invoice and receipt. Leaving the amount empty works as before.
  5. Cash + Card: for a customer who pays part in cash and part by card. Type one side and the other fills itself in; both amounts print on the invoice and receipt.
  • Hold parks the current cart (with a note) so you can serve someone else; Held brings it back. Cashiers see their own held carts; admins see all.
  • Prices, tax and stock are always recalculated on the server. The browser can't change them.
  • Stock is locked during checkout, so two cashiers can't sell the last unit twice.
  • Quick buttons: tick Quick button on the POS screen on a product and it appears as a one-tap tile above the search box — handy for bread, water, cigarettes and anything else you ring up all day.

Cash shifts

A shift is one open cash drawer. It answers the question every shop owner asks at closing time: is the money in the till the money that should be there?

  1. Opening. The cashier signs in, counts what is already in the drawer and types it. Selling is blocked until they do.
  2. During the day. Every sale is attached to that shift. Money that leaves the till — bread, a delivery, a supplier paid in cash — is recorded with Cash expense and a reason.
  3. Closing. The cashier presses Close shift. The screen shows what the drawer should hold, they count it, type the number, and the difference appears immediately: green when it matches, orange when there is too much, red when it is short. An optional note explains anything unusual.

What the system expects in the drawer:

opening float + cash sales + credit paid back in cash − cash expenses − refunds handed back

  • Card payments are shown separately and never counted as cash. The cash half of a Cash + Card invoice is.
  • A refund given over the counter comes out of the drawer of whoever gave it, so the count still matches at closing.
  • Each cashier has their own drawer; two people can be open at the same time on two machines.
  • The owner sees everything under Reports → Shift history: expected, counted, the difference, and a detail page with that shift's invoices and every movement out of the till.

Don't want shifts? Settings → Cash drawer → turn off Require an open shift before selling. The POS then behaves exactly as it did in 1.2.0, and sales are simply not attached to a drawer.

Receipts & printing

In Settings → Printing choose what happens after a sale:

Do nothingThe sale is recorded; print later from Reports.
Open the A4 invoiceOpens the printable invoice in a new tab, with a PDF download button.
Print a thermal receiptPrints a 58 mm or 80 mm receipt immediately, without opening a tab.

Thermal printer setup

  1. Install your receipt printer's driver so it appears in the computer's printer list (Windows, macOS and Linux all work; USB, network and Bluetooth models).
  2. Set the receipt paper width in Settings (80 mm is the common size; small handheld models use 58 mm).
  3. Open a receipt once (Reports → invoice → Thermal receipt) and, in the browser print dialog, choose your printer, Margins: None, and turn Headers and footers off. The browser remembers these choices.

Printing without the dialog

Browsers always ask before printing. On a dedicated till you can skip the dialog:

  • Chrome / Edge: start the browser with --kiosk-printing (add it to the shortcut's Target) and make the thermal printer the default printer. Receipts then print silently at checkout.
  • Firefox: in about:config set print.always_print_silent to true.
Receipts are printed through the browser using the printer's normal driver. Tally POS does not send raw ESC/POS commands, so it does not open cash drawers or cut paper by itself; most drivers cut automatically at the end of a page, and many drivers can be set to open the drawer on each print job.

The receipt footer (e.g. a thank-you message or tax number) is set in Settings. The logo is printed in black and white.

Customer credit ("kridi")

Many shops let regular customers buy now and pay later. Tally POS keeps that account for you.

Selling on credit

  1. In the POS choose the customer (customers who already owe money show the amount next to their name).
  2. Set the payment method to On credit. It only becomes available once a customer is chosen.
  3. Optionally type what the customer pays now (a down payment). The rest stays on their account. The screen shows "Stays on credit".

Credit limit

In Customers, edit a customer and set a credit limit. A credit sale that would take the customer above the limit is refused with a clear message. 0 means no limit.

Getting paid

Open Customer credit in the menu (admins and cashiers). You see everyone who owes money and the total owed. Open a customer's Statement to see every credit sale and every payment with a running balance, and to record a payment (cash, card or other, with an optional note). A payment can't be higher than what the customer owes.

The dashboard shows the total owed by all customers.

Good to know

  • Credit sales count as sales in reports on the day of the sale, like any other sale. The money arrives later, when you record the payment.
  • Refunding a credit sale first reduces what the customer still owes. Only the rest is given back as money, and the screen tells you how much to hand back.
  • A customer with credit history (unpaid sales or payments) can't be deleted.

Refunds

Open Reports, find the invoice and click Refund. Choose how many units of each line come back. Stock is restored, the refunded amount (with proportional tax) is recorded, and the invoice becomes Partially refunded or Refunded. Profit figures exclude returned units.

If you have a shift open, the cash you hand back is deducted from your drawer automatically, so the end-of-day count still adds up. On a credit invoice the customer's debt is reduced first and only the remainder is given back as money.

Reports

Day, week, month, year or a custom date range: total sales, invoice count, average invoice, items sold, net profit and margin, top products, a daily chart and the invoice list. Export CSV opens directly in Excel, Arabic included. Use the print button (or Save as PDF in the print dialog) for a paper copy.

Import products (Excel / CSV)

Admin → Import products. Ideal when you already have your catalogue in a spreadsheet.

  1. Click Download an example file to see the format, or Export my current products to get your catalogue in the same format.
  2. In Excel save your sheet as CSV UTF-8 (File → Save As). Both comma and semicolon separators work, and prices like 12,50 are understood. Old Excel "CSV" files (Windows encoding) are converted automatically.
  3. Upload the file and choose what happens if a barcode already exists: skip the row, or update the product (its quantity is added to the stock).
  4. You get a check of every row (which are ready, which have errors and why). Nothing is saved yet.
  5. Press Import. Rows with errors are skipped; everything else is imported in one go (all or nothing if something unexpected fails).
Columnsname and price are required. Optional: barcode, cost_price, quantity, min_quantity, category, supplier, expiry_date (2027-01-31 or 31/01/2027). Column names in French (nom, prix, prix_achat, quantité, catégorie, fournisseur, code_barres, DLC) are recognised too; the order doesn't matter.
Categories, suppliersMatched by name; created automatically when they don't exist yet.
Limits2 MB and 5,000 rows per file. Split bigger catalogues.
Stock historyOpening quantities are recorded as "Opening stock" (new products) or "Import" (updated products).

Backup & restore

Back up

Backup in the menu downloads the whole database as one .sql file. No mysqldump needed. Do this regularly and keep copies off the server. The file contains password hashes, so treat it as confidential.

Restore

  1. Open phpMyAdmin, select the (empty or old) database.
  2. Import → choose the .sql file → Go. The file drops and recreates the tables.

Product photos and your logo live in assets/uploads/, which is not part of the SQL backup. Copy that folder too.

Very large databases (hundreds of thousands of sales) may exceed the host's PHP time or memory limit during download; in that case use your host's own backup tool or phpMyAdmin's export.

Languages

English, French and Arabic are included. Visitors switch with the AR / EN / FR buttons on the sign-in page and in the top bar. Arabic switches the whole layout to right-to-left. The installer sets the default language.

Change the default language later

Edit config/config.php: 'default_lang' => 'fr' (or 'en', 'ar').

Edit any text

All texts live in lang/en.php, lang/fr.php and lang/ar.php as 'key' => 'text'. Change the text on the right, keep the key. :name-style placeholders are filled in by the app; keep them in your translation.

Add another language (for example Spanish)

  1. Copy lang/en.php to lang/es.php and translate the values. Set 'lang_native_name' => 'Español'.
  2. That's it: the new language appears automatically in the switcher and in the installer.
  3. Right-to-left language (Persian, Urdu, Hebrew)? Its code is already listed in RTL_LANGS at the top of includes/i18n.php; add your code there if it isn't.

The demo-data names (products, customers…) are keys too: demo_prod_1 … demo_prod_15, demo_cat_*, demo_sup_*, demo_cust_*.

Rename the product

The name in the sidebar and page titles is app_name in the language files.

Demo data & reset

Admin → Demo data has two tools. The installer's Add sample data option runs the first one.

Generate demo dataAdds 5 categories, 2 suppliers, 15 products with opening stock, 4 customers and 25 invoices spread over the last 30 days (including several sales on credit with partial payments), so reports, profit and charts have something to show. Names follow the language you are using at that moment. Stock always matches the stock history.
Reset the databaseDeletes all products (and their photos), categories, suppliers, customers, invoices, held sales and the activity log, so the owner can start from zero without reinstalling. Users and Settings are kept. To run it you must type the confirmation word shown on screen and re-enter your admin password. It cannot be undone: download a backup first.

Both tools are disabled when demo mode is on, and the menu entry is hidden.

Demo mode

To run a public demo, set 'demo_mode' => true in config/config.php. Visitors can browse and sell, but changing users, changing settings, downloading backups and the demo-data tools are blocked, and a banner says so. Combine it with a scheduled restore of a clean SQL backup (a cron job importing your .sql file every hour) so demo data never piles up.

Security checklist

  1. Delete the install folder after installing.
  2. Use HTTPS (free with Let's Encrypt on most hosts). Session cookies are marked secure automatically when HTTPS is used.
  3. Keep 'debug' => false in config/config.php.
  4. Give each cashier their own account and use strong passwords.
  5. Make sure config/config.php is not world-readable (the installer sets 0640).
  6. On Nginx, add the deny rules from the installation section.
  7. Back up the database and assets/uploads/ regularly.
  8. Keep PHP up to date.

Built in

Prepared SQL statements everywhere, CSRF tokens on every form and API call, output escaping, hashed passwords, session-ID rotation on login, HttpOnly/SameSite cookies, login lockout, content-checked image uploads (no SVG), no execution of uploaded files, server-side price/stock/tax calculation, database errors logged rather than shown.

Troubleshooting

"Database connection failed"Check the values in config/config.php. On some hosts the host is not localhost (see your hosting panel). To redo the setup, delete config/config.php, empty the database and open the installer again.
Blank page or "Something went wrong"The real error is in the PHP error log (cPanel → Errors, or error_log in the app folder). For a quick look set 'debug' => true temporarily, then turn it off again.
Installer says a folder is not writableSet config/ and assets/uploads/ to permission 755 (or 775) in File Manager. If config/ stays read-only, the installer shows the file to create by hand.
Redirects to the installerconfig/config.php is missing. Restore it from your backup, or reinstall.
Logged out constantlyThe host's session folder may be unwritable or the clock wrong. Ask the host, or set a writable session.save_path.
PDF fails or shows squaresEnable the dom and mbstring PHP extensions. Fonts are cached in the system temp folder; nothing in vendor/ needs to be writable.
Logo or product photo won't uploadJPG, PNG or WEBP under 2 MB. Also check upload_max_filesize and post_max_size in PHP settings, and folder permissions.
Camera scanner doesn't startBrowsers only allow the camera on HTTPS (or localhost), and you must allow camera access.
Receipt prints with margins or page headersIn the print dialog: Margins → None, Headers and footers → off, and pick the thermal printer with the right paper size.
Locked out after wrong passwordsWait 15 minutes, or in phpMyAdmin run DELETE FROM login_attempts;
Still the old version right after uploading new filesSome hosts cache compiled PHP (OPcache) for a minute or more. Wait a little, or ask your host to clear the cache, then reload.
"The system is being updated" for cashiersFiles were updated but the database wasn't. Sign in as admin and press Run the update.
Import says the first line must contain column namesThe file needs a header row with at least name and price. Download the example file and compare.
Can't choose "On credit" in the POSPick a customer first (add them in Customers if needed).
Numbers show the wrong currencySettings → Currency.

Updating to a new version

  1. Back up first: Admin → Backup (database) and copy the assets/uploads folder.
  2. Upload the new files over the old ones, keeping your config/config.php and your assets/uploads folder. (The package never contains a config.php, so a normal "overwrite" upload is safe. Don't upload the install folder again.)
  3. Sign in as admin. If the new version changes the database, you are taken to the Update page automatically. Press Run the update. It lists exactly what it will apply.
  4. That's it. Cashiers who open the app during the update see a short "system is being updated" message until an admin has run it.

Each database change is a numbered file in sql/updates/, applied once and in order; the database remembers its version, so running the update twice does nothing. If an update fails, the page shows the exact error and the version doesn't move: restore your backup and contact support.

Credits

Tally POS includes the following open-source components. Full license texts are in the licenses folder of the script.

Bootstrap 5.3.3MIT
Bootstrap Icons 1.11.3MIT
Chart.js 4.4.4MIT
JsBarcode 3.11.6MIT
html5-qrcode 2.3.8Apache-2.0
Cairo font (via Fontsource)SIL Open Font License 1.1
Dompdf 2.0.8LGPL-2.1
php-font-lib, php-svg-libLGPL-2.1 / LGPL-3.0
PHP CSS Parser, HTML5-PHPMIT
DejaVu fonts (bundled with Dompdf)Bitstream Vera / DejaVu license

Dompdf and its font/SVG libraries are LGPL-licensed and live in their own vendor folder, so you can replace them with newer versions (see the note in vendor/autoload.php).