Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .prettierignore
100644 → 100755
Empty file.
Empty file modified .prettierrc
100644 → 100755
Empty file.
File renamed without changes.
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,6 @@ docs: update installation instructions

## License

MIT — see [license.txt](license.txt) for full terms.
MIT — see [LICENSE](LICENSE) for full terms.

---

<div align="center">

Developed and maintained by **Ali Raza** — [ar.frappe.dev@gmail.com](mailto:ar.frappe.dev@gmail.com)

</div>
1 change: 1 addition & 0 deletions docs/features/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
| 23 | [Product Bundles](23-product-bundles.md) | Bundle items with component expansion |
| 24 | [POS Profile Configuration](24-pos-profile-config.md) | Complete POS Profile settings reference |
| 25 | [Keyboard Shortcuts](25-keyboard-shortcuts.md) | Full keyboard shortcut reference |
| 26 | [Cashier Settlement](26-cashier-settlement.md) | Create bills at the terminal, collect payment at a separate cashier |

---

Expand Down
1 change: 1 addition & 0 deletions docs/features/07-draft-orders.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,4 @@ The dialog shows all held orders for the current shift:
- Draft invoices are automatically cleaned up based on your profile settings
- Use draft orders to manage busy periods efficiently
- Each draft shows the customer name, item count, and total for quick identification
- Held orders and [Cashier Settlement](26-cashier-settlement.md) bills are kept separate — bills sent to a cashier do not appear in the held-orders list, and vice versa
12 changes: 12 additions & 0 deletions docs/features/24-pos-profile-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ The POS Profile is the central configuration hub for X POS. It controls every as
| Use POS Invoice | Use POS Invoice instead of Sales Invoice |
| Allow Multi Currency | Enable multi-currency transactions |
| Block Sale Beyond Available Qty | Prevent selling beyond available stock |
| Enable Cashier Settlement | Create unsettled bills at the terminal for a cashier to settle later (see [Cashier Settlement](26-cashier-settlement.md)) |
| Print Backup Receipt | Print a non-genuine backup receipt at the terminal when a bill is sent to the cashier |

---

Expand Down Expand Up @@ -180,6 +182,16 @@ In addition to the X POS settings, the standard POS Profile provides:

---

## User Access (Applicable for Users)

The **Applicable for Users** table assigns users to the POS Profile. X POS adds one field to each row:

| Field | Description |
|---|---|
| Is Cashier | Allows this user to open the Cashier screen and settle bills. See [Cashier Settlement](26-cashier-settlement.md). |

---

## Configuration Tips

- Start with a minimal configuration and enable features as needed
Expand Down
13 changes: 13 additions & 0 deletions docs/features/25-keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ X POS is designed for fast operation with extensive keyboard support for power u

---

## Cashier Settlement

| Key | Action | Context |
|---|---|---|
| `Alt+0` | Open the Cashier screen | Anywhere (cashier users only) |
| `↑` (Up Arrow) | Highlight previous unsettled bill | Cashier screen |
| `↓` (Down Arrow) | Highlight next unsettled bill | Cashier screen |
| `Enter` | Open the payment dialog to settle the highlighted bill | Cashier screen |

See [Cashier Settlement](26-cashier-settlement.md) for the full workflow.

---

## Workflow Examples

### Fast Checkout (Keyboard Only)
Expand Down
113 changes: 113 additions & 0 deletions docs/features/26-cashier-settlement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Cashier Settlement

Cashier Settlement separates the act of **creating a bill** from the act of **collecting payment**. A POS terminal operator builds the order and sends it to a cashier, who collects the money and closes the bill from a dedicated screen — typically on a different PC at a central cash counter.

This mirrors how many pharmacies and high-traffic retail counters operate: sales staff ring up items at the terminal, while a single cashier handles all cash collection.

---

## How It Works

1. A POS Profile is configured with **Cashier Settlement** enabled.
2. At the terminal, the operator builds the cart as usual. The checkout button reads **Send to Cashier** instead of **Pay**.
3. Pressing **Send to Cashier** saves the order as an **unsettled draft invoice** — no payment is collected. The cart clears, ready for the next customer.
4. The unsettled invoice appears on the **Cashier** screen on any PC signed in to the same POS Profile. The list refreshes automatically.
5. The cashier selects an invoice, collects payment through the normal payment dialog, and submits it. The settled bill leaves the list, and the genuine tax invoice prints.

Under the hood this reuses the existing draft-invoice workflow. Unsettled bills are drafts (`docstatus = 0`) flagged with a `POS Awaiting Settlement` field, which keeps them separate from ordinary [held orders](07-draft-orders.md). When a bill is settled, the invoice is re-associated with the **cashier's** open shift, so the collected amount is reconciled in the cashier's shift, not the terminal operator's.

---

## Enabling the Feature

Two settings on the **POS Profile** control the terminal behavior:

| Setting | Description |
|---|---|
| Enable Cashier Settlement | Turns the terminal's checkout into **Send to Cashier**. No payment is collected at the terminal; the order is saved for a cashier to settle. |
| Print Backup Receipt | When enabled, the terminal prints a non-genuine **backup receipt** for the customer at the moment the order is sent. The genuine tax invoice is printed later by the cashier after settlement. (Only available when Cashier Settlement is enabled.) |

---

## Cashier Access Control

Not every user is a cashier. Access is controlled per user on the POS Profile's **Applicable for Users** table:

| Field | Description |
|---|---|
| Is Cashier | When checked, this user may open the Cashier screen and settle (close) bills. |

Access rules:

- Only users with **Is Cashier** checked can open the Cashier screen or settle a bill.
- **Administrator** and any user with the **System Manager** role always have access, so managers/admins are never locked out.
- Enforcement is on the **server**, not just the UI:
- The Cashier route redirects non-cashiers back to the POS screen.
- The Cashier nav item is hidden for non-cashiers.
- The settlement and "list unsettled invoices" APIs reject non-cashiers, so a bill cannot be closed by an unauthorized user even outside the UI.

---

## Terminal Workflow (Sales Operator)

1. Open a shift and build the cart (items, customer, discounts) as normal.
2. The checkout button shows **Send to Cashier**.
3. Press it to save the unsettled bill. If **Print Backup Receipt** is enabled, a backup copy prints for the customer.
4. The cart clears immediately for the next sale.

Returns are unaffected — return mode still uses the normal payment flow even when Cashier Settlement is enabled.

---

## Cashier Workflow (Settlement Screen)

Open the **Cashier** screen from the sidebar, or press `Alt + 0`.

The screen lists every unsettled invoice for the POS Profile — regardless of which terminal or operator created it — and refreshes on its own every few seconds so new bills appear and settled bills disappear without manual reloading.

Each row shows the customer, invoice reference, date/time, item count, and grand total.

### Settling a Bill (Keyboard or Mouse)

| Action | Result |
|---|---|
| `↑` / `↓` | Move the highlight up/down the list |
| `Enter` | Open the payment dialog for the highlighted bill |
| Mouse click | Open the payment dialog for that bill |

In the payment dialog the cashier collects payment exactly like a normal sale. On **Save & Print**, the invoice is submitted, the genuine tax invoice prints, and the bill drops off the unsettled list.

A manual **Refresh** button is available if needed.

---

## Sales Invoice and POS Invoice Support

Cashier Settlement works whether the system is configured for **Sales Invoice** or **POS Invoice** (set in POS Settings).

ERPNext requires at least one mode of payment on a POS Invoice, even as a draft. Because no payment is collected at the terminal, X POS automatically seeds a zero-amount payment row on the draft so it can be saved; the cashier replaces it with the real amounts at settlement time.

---

## Use Cases

### Pharmacy / Retail Cash Counter
1. The counter staff scan items and hand the customer a backup receipt.
2. The customer walks to the cash counter and pays.
3. The cashier finds the bill on the Cashier screen, collects payment, and prints the official invoice.

### Multiple Terminals, One Cashier
Several sales terminals can send bills to a single Cashier screen. The cashier works through the queue using the keyboard, clearing each bill as payment is collected.

### Manager-Controlled Closing
By granting **Is Cashier** only to trusted staff, businesses ensure that bill creation (open to many operators) and money collection (restricted) are handled by different people.

---

## Tips

- The collected amount reconciles in the **cashier's** shift, so make sure the cashier has an open shift before settling.
- Use **Print Backup Receipt** so customers have something to present at the cash counter; it is clearly a draft/backup, not the tax invoice.
- The Cashier screen refreshes automatically, but the **Refresh** button forces an immediate reload.
- Ordinary [held orders](07-draft-orders.md) do **not** appear on the Cashier screen — only bills explicitly sent for settlement.
- See [POS Profile Configuration](24-pos-profile-config.md) for the related settings and [Keyboard Shortcuts](25-keyboard-shortcuts.md) for the full navigation reference.
Empty file modified frontend/.yarnrc
100644 → 100755
Empty file.
78 changes: 78 additions & 0 deletions frontend/electron/database/dbService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,84 @@ async function runMigrations(): Promise<void> {
log.warn(`Migration for pos_profiles.${col} failed`, err);
}
}

const posUserMigrations: [string, string][] = [
["close_bill", "TINYINT(1) DEFAULT 1"],
["close_shift", "TINYINT(1) DEFAULT 0"],
["allow_reprint_invoice", "TINYINT(1) DEFAULT 0"],
["shift_report", "TINYINT(1) DEFAULT 0"],
["allow_cancel_invoice", "TINYINT(1) DEFAULT 0"],
["unsettled_invoices", "TINYINT(1) DEFAULT 0"],
["apply_additional_discount", "TINYINT(1) DEFAULT 0"],
["apply_standard_discount", "TINYINT(1) DEFAULT 0"],
["show_edit_discount_field", "TINYINT(1) DEFAULT 0"],
["edit_tax_template", "TINYINT(1) DEFAULT 0"],
["allow_change_price", "TINYINT(1) DEFAULT 0"],
["quotation", "TINYINT(1) DEFAULT 0"],
["sale_return", "TINYINT(1) DEFAULT 0"],
["local_purchase", "TINYINT(1) DEFAULT 0"],
["purchase_order", "TINYINT(1) DEFAULT 0"],
["purchase_invoice", "TINYINT(1) DEFAULT 0"],
["stock_adjustment", "TINYINT(1) DEFAULT 0"],
["stock_entry", "TINYINT(1) DEFAULT 0"],
["near_expiry_items", "TINYINT(1) DEFAULT 0"],
["expense", "TINYINT(1) DEFAULT 0"],
["bank_drop", "TINYINT(1) DEFAULT 0"],
["list_of_invoices", "TINYINT(1) DEFAULT 1"],
["list_of_cancelled_invoices", "TINYINT(1) DEFAULT 0"],
["list_of_errors", "TINYINT(1) DEFAULT 0"],
["list_of_purchase_invoices", "TINYINT(1) DEFAULT 0"],
["list_of_quotations", "TINYINT(1) DEFAULT 0"],
["list_of_stock_entries", "TINYINT(1) DEFAULT 0"],
["list_of_local_purchases", "TINYINT(1) DEFAULT 0"],
["list_of_stock_adjustments", "TINYINT(1) DEFAULT 0"],
["list_of_expense", "TINYINT(1) DEFAULT 0"],
["list_of_bank_drops", "TINYINT(1) DEFAULT 0"],
["invoice_settlement_report", "TINYINT(1) DEFAULT 0"],
["sales_report_by_time", "TINYINT(1) DEFAULT 0"],
["sales_summary_by_hour", "TINYINT(1) DEFAULT 0"],
["current_stock_by_brand", "TINYINT(1) DEFAULT 0"],
["stock_register", "TINYINT(1) DEFAULT 0"],
["current_stock_report", "TINYINT(1) DEFAULT 0"],
["discount_limit", "DECIMAL(18,6) DEFAULT 100"],
];

const posUserColumnExists = async (col: string): Promise<boolean> => {
const [existing] = await db.execute<RowDataPacket[]>(
"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pos_users' AND COLUMN_NAME = ?",
[col],
);
return (existing as RowDataPacket[]).length > 0;
};

for (const [col, typedef] of posUserMigrations) {
try {
if (!(await posUserColumnExists(col))) {
await db.execute(`ALTER TABLE \`pos_users\` ADD COLUMN \`${col}\` ${typedef}`);
log.info(`Migration: added pos_users.${col}`);
}
} catch (err) {
log.warn(`Migration for pos_users.${col} failed`, err);
}
}

const posUserRenames: [string, string][] = [
["allow_return", "sale_return"],
["allow_expense", "expense"],
["allow_bank_drop", "bank_drop"],
["show_edit_item_tax_template", "edit_tax_template"],
];
for (const [oldCol, newCol] of posUserRenames) {
try {
if (await posUserColumnExists(oldCol)) {
await db.execute(`UPDATE \`pos_users\` SET \`${newCol}\` = \`${oldCol}\``);
await db.execute(`ALTER TABLE \`pos_users\` DROP COLUMN \`${oldCol}\``);
log.info(`Migration: renamed pos_users.${oldCol} -> ${newCol}`);
}
} catch (err) {
log.warn(`Migration for pos_users rename ${oldCol} -> ${newCol} failed`, err);
}
}
}

async function executeSchemaFile(filePath: string): Promise<void> {
Expand Down
42 changes: 33 additions & 9 deletions frontend/electron/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -473,20 +473,44 @@ CREATE TABLE IF NOT EXISTS `pos_users` (
`company` VARCHAR(255) DEFAULT NULL,
`theme` VARCHAR(50) DEFAULT 'Default',
`enabled` TINYINT(1) DEFAULT 1,
`discount_limit` DECIMAL(18,6) DEFAULT 100,
`close_bill` TINYINT(1) DEFAULT 1,
`close_shift` TINYINT(1) DEFAULT 0,
`allow_reprint_invoice` TINYINT(1) DEFAULT 0,
`stock_adjustment` TINYINT(1) DEFAULT 0,
`quotation` TINYINT(1) DEFAULT 0,
`shift_report` TINYINT(1) DEFAULT 0,
`allow_cancel_invoice` TINYINT(1) DEFAULT 0,
`unsettled_invoices` TINYINT(1) DEFAULT 0,
`apply_additional_discount` TINYINT(1) DEFAULT 0,
`apply_standard_discount` TINYINT(1) DEFAULT 0,
`allow_change_price` TINYINT(1) DEFAULT 0,
`show_edit_discount_field` TINYINT(1) DEFAULT 0,
`show_edit_item_tax_template` TINYINT(1) DEFAULT 0,
`allow_return` TINYINT(1) DEFAULT 0,
`allow_cancel_invoice` TINYINT(1) DEFAULT 0,
`allow_expense` TINYINT(1) DEFAULT 0,
`allow_bank_drop` TINYINT(1) DEFAULT 0,
`discount_limit` DECIMAL(18,6) DEFAULT 100,
`edit_tax_template` TINYINT(1) DEFAULT 0,
`allow_change_price` TINYINT(1) DEFAULT 0,
`quotation` TINYINT(1) DEFAULT 0,
`sale_return` TINYINT(1) DEFAULT 0,
`local_purchase` TINYINT(1) DEFAULT 0,
`purchase_order` TINYINT(1) DEFAULT 0,
`purchase_invoice` TINYINT(1) DEFAULT 0,
`stock_adjustment` TINYINT(1) DEFAULT 0,
`stock_entry` TINYINT(1) DEFAULT 0,
`near_expiry_items` TINYINT(1) DEFAULT 0,
`expense` TINYINT(1) DEFAULT 0,
`bank_drop` TINYINT(1) DEFAULT 0,
`list_of_invoices` TINYINT(1) DEFAULT 1,
`list_of_cancelled_invoices` TINYINT(1) DEFAULT 0,
`list_of_errors` TINYINT(1) DEFAULT 0,
`list_of_purchase_invoices` TINYINT(1) DEFAULT 0,
`list_of_quotations` TINYINT(1) DEFAULT 0,
`list_of_stock_entries` TINYINT(1) DEFAULT 0,
`list_of_local_purchases` TINYINT(1) DEFAULT 0,
`list_of_stock_adjustments` TINYINT(1) DEFAULT 0,
`list_of_expense` TINYINT(1) DEFAULT 0,
`list_of_bank_drops` TINYINT(1) DEFAULT 0,
`invoice_settlement_report` TINYINT(1) DEFAULT 0,
`sales_report_by_time` TINYINT(1) DEFAULT 0,
`sales_summary_by_hour` TINYINT(1) DEFAULT 0,
`current_stock_by_brand` TINYINT(1) DEFAULT 0,
`stock_register` TINYINT(1) DEFAULT 0,
`current_stock_report` TINYINT(1) DEFAULT 0,
`modified` DATETIME DEFAULT NULL,
`synced_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX `idx_username` (`username`),
Expand Down
Loading
Loading