diff --git a/.prettierignore b/.prettierignore old mode 100644 new mode 100755 diff --git a/.prettierrc b/.prettierrc old mode 100644 new mode 100755 diff --git a/license.txt b/LICENSE similarity index 100% rename from license.txt rename to LICENSE diff --git a/README.md b/README.md index 9390d0b..04984a2 100755 --- a/README.md +++ b/README.md @@ -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. --- - -
- -Developed and maintained by **Ali Raza** — [ar.frappe.dev@gmail.com](mailto:ar.frappe.dev@gmail.com) - -
diff --git a/docs/features/00-overview.md b/docs/features/00-overview.md index 97701fe..1ab76d1 100755 --- a/docs/features/00-overview.md +++ b/docs/features/00-overview.md @@ -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 | --- diff --git a/docs/features/07-draft-orders.md b/docs/features/07-draft-orders.md index 7a63093..1d3a69c 100755 --- a/docs/features/07-draft-orders.md +++ b/docs/features/07-draft-orders.md @@ -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 diff --git a/docs/features/24-pos-profile-config.md b/docs/features/24-pos-profile-config.md index c8a3a90..c1bfeb4 100755 --- a/docs/features/24-pos-profile-config.md +++ b/docs/features/24-pos-profile-config.md @@ -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 | --- @@ -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 diff --git a/docs/features/25-keyboard-shortcuts.md b/docs/features/25-keyboard-shortcuts.md index f6e5a1b..c9b497b 100755 --- a/docs/features/25-keyboard-shortcuts.md +++ b/docs/features/25-keyboard-shortcuts.md @@ -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) diff --git a/docs/features/26-cashier-settlement.md b/docs/features/26-cashier-settlement.md new file mode 100644 index 0000000..46a2826 --- /dev/null +++ b/docs/features/26-cashier-settlement.md @@ -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. diff --git a/frontend/.yarnrc b/frontend/.yarnrc old mode 100644 new mode 100755 diff --git a/frontend/electron/database/dbService.ts b/frontend/electron/database/dbService.ts index 12c0488..e9ea46d 100755 --- a/frontend/electron/database/dbService.ts +++ b/frontend/electron/database/dbService.ts @@ -245,6 +245,84 @@ async function runMigrations(): Promise { 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 => { + const [existing] = await db.execute( + "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 { diff --git a/frontend/electron/database/schema.sql b/frontend/electron/database/schema.sql index c874ab8..73e1c10 100755 --- a/frontend/electron/database/schema.sql +++ b/frontend/electron/database/schema.sql @@ -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`), diff --git a/frontend/src/components/MenuBar.vue b/frontend/src/components/MenuBar.vue index 159b026..96a61b6 100755 --- a/frontend/src/components/MenuBar.vue +++ b/frontend/src/components/MenuBar.vue @@ -26,7 +26,8 @@ class="absolute top-full start-0 min-w-[220px] bg-[#252526] dark:bg-[#252526] border border-[#454545] shadow-2xl py-1 z-[200]" >