Restaurant POS Source Code

Lunch rush, table 7 orders two more iced teas, and the order never reaches the kitchen. At another table, a party of six wants separate checks and the cashier recalculates everythi...

Restaurant POS Source Code

Lunch rush, table 7 orders two more iced teas, and the order never reaches the kitchen. At another table, a party of six wants separate checks and the cashier recalculates everything on a calculator. Retail POS software was not built for this. This guide covers what makes a restaurant POS different, how its data should be structured, and what to check before adopting restaurant POS source code.

Why Retail POS Falls Short

In retail, a sale happens once: scan, pay, done. In a restaurant, an order stays open for as long as guests are seated. It is opened, added to several times, sent to the kitchen in rounds, and paid at the end, sometimes by several people. For a primer on POS basics, see what a POS system is and how it works.

NeedRetail POSRestaurant POS
Transaction lifecycleSingle paymentOpen tab, items added over time
Customer locationIrrelevantTable number and status
ProductsFixed SKUsMenu items with modifiers (size, spice level, toppings)
CoordinationCashier onlyServers, kitchen, cashier
PaymentOne billMerge or split checks
InventoryFinished goodsIngredients via recipes

Core Modules

  1. Menu and modifiers: categories, variants, and the ability to 86 an item instantly when it runs out.
  2. Tables and orders: open a tab per table, add items, move guests, merge or split checks, and see table status at a glance.
  3. Kitchen integration: new items appear on a kitchen display or print on a kitchen printer.
  4. Payments and split bills: cash, card, and digital wallets, split by person or by item.
  5. Reports: daily sales, best-selling items, peak hours, and server performance.
  6. Ingredient stock: each sale deducts ingredients according to its recipe.

Recommended Schema

The key is separating orders from payments, because one order can be paid in several parts. Each item also needs a kitchen status.

CREATE TABLE orders (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  dining_table_id INT UNSIGNED NULL,
  waiter_id BIGINT UNSIGNED NOT NULL,
  status ENUM('open','closed','void') DEFAULT 'open',
  opened_at DATETIME NOT NULL,
  closed_at DATETIME NULL
);

CREATE TABLE order_items (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  order_id BIGINT UNSIGNED NOT NULL,
  menu_id BIGINT UNSIGNED NOT NULL,
  qty SMALLINT UNSIGNED NOT NULL,
  unit_price DECIMAL(12,2) NOT NULL,
  note VARCHAR(150) NULL,
  kitchen_status ENUM('queued','cooking','served') DEFAULT 'queued',
  payment_id BIGINT UNSIGNED NULL
);

CREATE TABLE payments (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  order_id BIGINT UNSIGNED NOT NULL,
  method ENUM('cash','card','wallet') NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  paid_at DATETIME NOT NULL
);

CREATE TABLE recipes (
  menu_id BIGINT UNSIGNED NOT NULL,
  ingredient_id BIGINT UNSIGNED NOT NULL,
  qty_used DECIMAL(10,3) NOT NULL,
  PRIMARY KEY (menu_id, ingredient_id)
);

A payment_id on each item enables split-by-item: each guest pays for their own items, and the order closes once every item has a payment. The recipes table links menu items to ingredients.

Deducting ingredients when an order closes

Wrap stock deduction in a database transaction so a failure never leaves stock half-updated:

use Illuminate\Support\Facades\DB;

DB::transaction(function () use ($order) {
    foreach ($order->items as $item) {
        foreach ($item->menu->recipes as $recipe) {
            Ingredient::whereKey($recipe->ingredient_id)
                ->decrement('stock', $recipe->qty_used * $item->qty);
        }
    }
    $order->update(['status' => 'closed', 'closed_at' => now()]);
});

For busy venues, push this into a queued job so the cashier never waits. See Laravel queues and jobs for heavy tasks.

Peak Hours and Best Sellers

These two reports are what owners use most for staff scheduling and purchasing. Both come from closed orders:

-- Busiest hours, last 30 days
SELECT HOUR(o.opened_at) AS hour, COUNT(DISTINCT o.id) AS orders
FROM orders o
WHERE o.status = 'closed' AND o.opened_at >= NOW() - INTERVAL 30 DAY
GROUP BY HOUR(o.opened_at)
ORDER BY orders DESC;

-- Top 10 menu items
SELECT m.name, SUM(oi.qty) AS sold, SUM(oi.qty * oi.unit_price) AS revenue
FROM order_items oi
JOIN orders o ON o.id = oi.order_id AND o.status = 'closed'
JOIN menus m ON m.id = oi.menu_id
GROUP BY m.name
ORDER BY sold DESC
LIMIT 10;

For example, suppose 3 to 5 pm is consistently quiet. You could test an afternoon snack bundle and compare the following week. Items that sit at the bottom for months deserve a review of price, portion, or whether they belong on the menu at all.

A Phased Rollout

  1. Week one: enter menu items, prices, and tables. Train servers to open and close tabs.
  2. Week two: connect the kitchen via display or printer and stop paper tickets.
  3. Week three: enable split checks and every payment method you accept.
  4. Month two: fill in recipes and opening ingredient stock, then reconcile against physical counts weekly.

Phasing reduces staff confusion and makes it easier to spot which part is not working yet.

Running It Locally

  1. Install PHP, Composer, and MySQL at the versions listed in the README.
  2. Run composer install.
  3. Copy .env.example to .env, set database credentials, and run php artisan key:generate.
  4. Run migrations and seeders, or import the bundled SQL.
  5. Start php artisan serve, log in as admin, and test the full flow: open a table, order, send to kitchen, split, close.

Evaluation Checklist

  • Items can be added after the first kitchen send without creating a new order.
  • Cooked items cannot be deleted without manager approval, which closes a common fraud gap.
  • Split by item and by person both work and totals still reconcile.
  • Prices are stored on order_items at order time, not re-read from the menu.
  • Sold-out items disappear from server screens immediately.
  • Best-seller and peak-hour reports exist or are easy to add.
  • Runs on a tablet browser with no special install.
  • Follows standard Laravel structure and ships with documentation.

Common Mistakes

  • Using retail POS in a restaurant: no table concept, so servers go back to paper.
  • Kitchen not connected: add-on orders get lost. At least use a kitchen printer.
  • Empty recipes: the ingredient module is useless. Start with your top 10 sellers.
  • Untracked voids: always log who voided what, when, and why.

Limitations

Thermal printer and payment terminal integrations depend on your hardware and provider, so expect adjustment work. Offline mode is rare in web-based POS; if your connection drops often, consider running the server on the local network. Backups and server security are your responsibility. For a framework to weigh this decision, read buy or build: choosing ready-to-use source code. GudangCode's catalog also includes Laravel POS apps you can compare.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Source Code Pos Restoran Source Code
Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now