diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index f107462..f674eea 100755 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -25,7 +25,7 @@ {{ __("Main") }}

{ window.removeEventListener("xpos:toggle-sidebar", handleToggleSidebar); }); -const mainNavItems = [ - { route: "/pos", label: __("POS"), icon: LayoutGrid }, - { route: "/orders", label: __("Orders"), icon: FileText }, - { route: "/reports", label: __("Reports"), icon: BarChart3 }, -]; +const mainNavItems = computed(() => [ + { route: "/pos", label: __("POS"), icon: LayoutGrid, show: true }, + { route: "/orders", label: __("Orders"), icon: FileText, show: true }, + { + route: "/cashier", + label: __("Cashier"), + icon: Banknote, + show: posStore.enableCashierSettlement && posStore.isCashier, + }, + { route: "/reports", label: __("Reports"), icon: BarChart3, show: true }, +]); const purchaseNavItems = [ { route: "/purchase-order", label: __("Purchase Order"), icon: ClipboardList }, diff --git a/frontend/src/components/cart/CartSummary.vue b/frontend/src/components/cart/CartSummary.vue index 54c9582..4e21f94 100755 --- a/frontend/src/components/cart/CartSummary.vue +++ b/frontend/src/components/cart/CartSummary.vue @@ -332,7 +332,7 @@ : 'bg-gradient-to-r from-primary to-primary/90 hover:from-primary/90 hover:to-primary/80 shadow-primary/25' " :disabled="cartStore.isEmpty || !cartStore.customer" - @click="cartStore.openPaymentDialog()" + @click="handleCheckout()" > {{ payButtonLabel }} @@ -349,6 +349,7 @@ import { call, showSuccess, showError } from "@/services/api"; import { __ } from "@/lib/translate"; import { isElectron } from "@/services/electronBridge"; import { useOfflineStore } from "@/stores/offlineStore"; +import { usePrintInvoice } from "@/composables/usePrintInvoice"; import { Button } from "@/components/ui/button"; import { TooltipWrapper } from "@/components/ui/tooltip"; import { Input } from "@/components/ui/input"; @@ -373,6 +374,7 @@ const posStore = usePosStore(); const cartStore = useCartStore(); const offerStore = useOfferStore(); const offlineStore = useOfflineStore(); +const { printInvoice, printInvoiceLocal } = usePrintInvoice(); const showDiscount = ref(false); const showCoupon = ref(false); @@ -432,10 +434,19 @@ const discountValue = computed(() => { const payButtonLabel = computed(() => { if (cartStore.isEmpty) return __("Add items to pay"); if (!cartStore.customer) return __("Select customer first"); + if (posStore.enableCashierSettlement && !cartStore.isReturnMode) return __("Send to Cashier"); const amt = `${posStore.currencySymbol}${formatPrice(Math.abs(cartStore.grandTotal))}`; return cartStore.isReturnMode ? __("Process Return {0}", [amt]) : __("Pay {0}", [amt]); }); +function handleCheckout() { + if (posStore.enableCashierSettlement && !cartStore.isReturnMode) { + sendToCashier(); + return; + } + cartStore.openPaymentDialog(); +} + function applyDiscount() { cartStore.setDiscount(discountType.value as "percentage" | "amount", discountInput.value || 0); } @@ -666,6 +677,83 @@ async function holdOrder() { } } +async function sendToCashier() { + if (cartStore.isEmpty) return; + + const profileName = posStore.profileName; + const shiftName = posStore.posOpeningShift?.name || ""; + + if (!profileName) { + showError(__("POS Profile is not set. Please close and reopen the shift.")); + return; + } + + if (!shiftName) { + showError(__("No open shift found. Please open a shift first.")); + return; + } + + try { + const data = cartStore.getInvoiceData(profileName, shiftName); + data.pos_awaiting_settlement = true; + + if (!data.customer) { + const bootCustomer = (window.xpos?.boot as Record)?.sysdefaults as + | Record + | undefined; + data.customer = bootCustomer?.customer || ""; + } + + if (!data.customer) { + showError(__("Please select a customer before sending to cashier.")); + return; + } + + if (!data.items || data.items.length === 0) { + showError(__("No items in cart to send.")); + return; + } + + if (isElectron() && window.electronAPI?.db) { + const result = await window.electronAPI.db.addPendingInvoice({ + data: { ...data, is_draft: true, pos_opening_shift_local_id: shiftName }, + customer_name: cartStore.customerName || data.customer, + grand_total: cartStore.grandTotal || 0, + }); + if (posStore.printBackupReceipt && result?.id) { + await printInvoiceLocal(result.id); + } + cartStore.clearAll(); + showSuccess(__("Sent to cashier")); + } else { + const serverData = { ...data }; + if (serverData.pos_opening_shift && /^\d+$/.test(String(serverData.pos_opening_shift))) { + delete serverData.pos_opening_shift; + } + if (offlineStore.isOnline) { + const result = await call<{ name: string }>("xpos.api.invoices.save_draft_invoice", { + data: JSON.stringify(serverData), + }); + if (posStore.printBackupReceipt && result?.name) { + await printInvoice(result.name); + } + cartStore.clearAll(); + showSuccess(__("Sent to cashier")); + } else { + await offlineStore.saveOffline( + { ...serverData, is_draft: true } as Parameters[0], + cartStore.customerName || data.customer, + cartStore.grandTotal || 0, + ); + cartStore.clearAll(); + showSuccess(__("No connection – will sync to cashier when back online")); + } + } + } catch (error: unknown) { + showError(__("Failed to send to cashier: {0}", [extractErrorMessage(error)])); + } +} + function formatPrice(price: number | string) { return parseFloat(String(price) || "0").toFixed(2); } diff --git a/frontend/src/components/dialogs/PaymentDialog.vue b/frontend/src/components/dialogs/PaymentDialog.vue index 44b5fd3..21bf198 100755 --- a/frontend/src/components/dialogs/PaymentDialog.vue +++ b/frontend/src/components/dialogs/PaymentDialog.vue @@ -551,6 +551,7 @@ import { usePaymentStore } from "@/stores/paymentStore"; import { call, showSuccess, showError, showInfo, isNetworkError } from "@/services/api"; import { useOfflineStore } from "@/stores/offlineStore"; import { __ } from "@/lib/translate"; +import { usePrintInvoice } from "@/composables/usePrintInvoice"; import { isElectron } from "@/services/electronBridge"; import { Dialog, @@ -592,6 +593,7 @@ import { } from "@/components/dialogs/paymentDialogShortcuts"; const posStore = usePosStore(); +const { printInvoice, printInvoiceLocal } = usePrintInvoice(); const cartStore = useCartStore(); const paymentStore = usePaymentStore(); const offlineStore = useOfflineStore(); @@ -1140,65 +1142,6 @@ async function submitPayment(withPrint: boolean = true) { } } -async function printInvoice(invoiceName: string) { - try { - const printFormat = posStore?.defaultPrintFormat || "XPOS Thermal Receipt"; - const letterHead = posStore.printSettings?.letter_head || ""; - - const usePosInvoice = xpos.boot?.pos_settings?.invoice_type === "POS Invoice"; - const doctype = usePosInvoice ? "POS Invoice" : "Sales Invoice"; - - const baseUrl = window.location.origin; - const printUrl = `${baseUrl}/printview?doctype=${doctype}&name=${invoiceName}&format=${printFormat}&no_letterhead=${letterHead ? "0" : "1"}`; - const printWindow = window.open(printUrl, "_blank"); - - if (printWindow) { - printWindow.onload = () => { - printWindow.onafterprint = () => { - printWindow.close(); - }; - setTimeout(() => { - printWindow.print(); - }, 500); - }; - } else { - window.open(printUrl, "_blank"); - } - } catch (error) { - console.error("Print error:", error); - showError(__("Failed to print invoice")); - } -} - -async function printInvoiceLocal(localId: number) { - try { - if (!window.electronAPI?.db || !window.electronAPI?.print) { - showError(__("Print not available")); - return; - } - - const invoice = await window.electronAPI.db.getPendingInvoice(localId); - if (!invoice) { - showError(__("Invoice not found for printing")); - return; - } - - await window.electronAPI.print.printInvoice({ - localId, - data: invoice.data, - customerName: invoice.customer_name || "", - grandTotal: invoice.grand_total, - isReturn: invoice.is_return, - printFormat: posStore.printSettings?.print_format || "POS Invoice", - letterHead: posStore.printSettings?.letter_head || "", - companyName: posStore.posProfile?.company || "", - }); - } catch (error) { - console.error("Local print error:", error); - showError(__("Failed to print invoice locally")); - } -} - function close() { cartStore.closePaymentDialog(); } diff --git a/frontend/src/composables/useKeyboardShortcuts.ts b/frontend/src/composables/useKeyboardShortcuts.ts index d78f8dd..9f139ae 100755 --- a/frontend/src/composables/useKeyboardShortcuts.ts +++ b/frontend/src/composables/useKeyboardShortcuts.ts @@ -183,6 +183,16 @@ export function useKeyboardShortcuts() { router.push("/reports"); }, }, + { + id: "goto-cashier", + keys: ["alt", "0"], + description: "Go to Cashier", + category: "Navigation", + global: true, + action: () => { + router.push("/cashier"); + }, + }, { id: "focus-cart", keys: ["f3"], diff --git a/frontend/src/composables/usePrintInvoice.ts b/frontend/src/composables/usePrintInvoice.ts new file mode 100644 index 0000000..2f83db5 --- /dev/null +++ b/frontend/src/composables/usePrintInvoice.ts @@ -0,0 +1,85 @@ +import { usePosStore } from "@/stores/posStore"; +import { showError } from "@/services/api"; +import { __ } from "@/lib/translate"; + +export interface PrintInvoiceOptions { + /** Override the print format (defaults to the POS profile's default thermal receipt). */ + format?: string; + /** Override the resolved doctype. */ + doctype?: "Sales Invoice" | "POS Invoice"; +} + +/** + * Shared invoice printing helpers used by the payment dialog (genuine receipt), + * the terminal backup receipt, and the cashier settlement screen. + */ +export function usePrintInvoice() { + const posStore = usePosStore(); + + function resolveDoctype(): "Sales Invoice" | "POS Invoice" { + return xpos.boot?.pos_settings?.invoice_type === "POS Invoice" + ? "POS Invoice" + : "Sales Invoice"; + } + + async function printInvoice(invoiceName: string, options: PrintInvoiceOptions = {}) { + try { + const printFormat = options.format || posStore?.defaultPrintFormat || "XPOS Thermal Receipt"; + const letterHead = posStore.printSettings?.letter_head || ""; + const doctype = options.doctype || resolveDoctype(); + + const baseUrl = window.location.origin; + const printUrl = `${baseUrl}/printview?doctype=${encodeURIComponent(doctype)}&name=${encodeURIComponent( + invoiceName, + )}&format=${encodeURIComponent(printFormat)}&no_letterhead=${letterHead ? "0" : "1"}`; + const printWindow = window.open(printUrl, "_blank"); + + if (printWindow) { + printWindow.onload = () => { + printWindow.onafterprint = () => { + printWindow.close(); + }; + setTimeout(() => { + printWindow.print(); + }, 500); + }; + } else { + window.open(printUrl, "_blank"); + } + } catch (error) { + console.error("Print error:", error); + showError(__("Failed to print invoice")); + } + } + + async function printInvoiceLocal(localId: number) { + try { + if (!window.electronAPI?.db || !window.electronAPI?.print) { + showError(__("Print not available")); + return; + } + + const invoice = await window.electronAPI.db.getPendingInvoice(localId); + if (!invoice) { + showError(__("Invoice not found for printing")); + return; + } + + await window.electronAPI.print.printInvoice({ + localId, + data: invoice.data, + customerName: invoice.customer_name || "", + grandTotal: invoice.grand_total, + isReturn: invoice.is_return, + printFormat: posStore.printSettings?.print_format || "POS Invoice", + letterHead: posStore.printSettings?.letter_head || "", + companyName: posStore.posProfile?.company || "", + }); + } catch (error) { + console.error("Local print error:", error); + showError(__("Failed to print invoice locally")); + } + } + + return { printInvoice, printInvoiceLocal }; +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 6b4c66d..42d8fc5 100755 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -2,19 +2,15 @@ import { createRouter, createWebHistory, createWebHashHistory, type Router, type import { useAuthStore } from "@/stores/authStore"; import { isElectron } from "@/services/electronBridge"; import routes from "./routes"; +import { usePosStore } from "@/stores/posStore"; -// Electron uses hash-based routing (file:// protocol, no server for SPA fallback). -// Browser/PWA uses history-based routing with /xpos base path. -const history = isElectron() - ? createWebHashHistory() - : createWebHistory("/xpos"); +const history = isElectron() ? createWebHashHistory() : createWebHistory("/xpos"); export const router: Router = createRouter({ history, routes, }); -// Track first-run status (only checked once) let _firstRunChecked = false; let _isFirstRun = false; @@ -28,21 +24,19 @@ async function checkFirstRun(): Promise { try { _isFirstRun = await window.electronAPI!.isFirstRun(); } catch { - // If the IPC call fails, assume first run (show setup wizard) _isFirstRun = true; } _firstRunChecked = true; return _isFirstRun; } -/** Call after setup wizard completes to skip future redirects. */ export function markSetupComplete(): void { _isFirstRun = false; _firstRunChecked = true; } router.beforeEach(async (to, _from, next) => { - // First-run check: redirect to setup wizard if needed + const firstRun = await checkFirstRun(); if (firstRun && to.meta.isSetupPage !== true) { next({ name: "setup" }); @@ -58,6 +52,7 @@ router.beforeEach(async (to, _from, next) => { } const authStore = useAuthStore(); + const posStore = usePosStore(); if (!authStore.isAuthenticated && !authStore.isLoading) { await authStore.checkAuth(); @@ -83,6 +78,10 @@ router.beforeEach(async (to, _from, next) => { next({ name: "pos" }); return; } + if (to.name === "cashier" && (!posStore.enableCashierSettlement || !posStore.isCashier)) { + next({ name: "pos" }); + return; + } if (to.meta.title) { document.title = `${to.meta.title} | X POS`; diff --git a/frontend/src/router/routes.ts b/frontend/src/router/routes.ts index 0a9c480..95ff910 100755 --- a/frontend/src/router/routes.ts +++ b/frontend/src/router/routes.ts @@ -1,5 +1,6 @@ const PosView = () => import("@/views/PosView.vue"); const OrdersView = () => import("@/views/OrdersView.vue"); +const CashierView = () => import("@/views/CashierView.vue"); const ReportsIndexView = () => import("@/views/ReportsIndexView.vue"); const ReportViewerView = () => import("@/views/ReportViewerView.vue"); const SettingsView = () => import("@/views/SettingsView.vue"); @@ -51,6 +52,12 @@ const routes: RouteRecordRaw[] = [ component: OrdersView, meta: { title: "Orders", requiresAuth: true }, }, + { + path: "/cashier", + name: "cashier", + component: CashierView, + meta: { title: "Cashier", requiresAuth: true }, + }, { path: "/reports", name: "reports", diff --git a/frontend/src/stores/posStore.ts b/frontend/src/stores/posStore.ts index 42ec7c2..f4db54b 100755 --- a/frontend/src/stores/posStore.ts +++ b/frontend/src/stores/posStore.ts @@ -20,6 +20,7 @@ import { isOnline } from "@/utils"; export const usePosStore = defineStore("pos", () => { const isLoading = ref(true); const isReady = ref(false); + const isCashier = ref(true); const currentView = ref("pos"); const posOpeningShift = ref(null); const posProfile = ref(null); @@ -158,6 +159,10 @@ export const usePosStore = defineStore("pos", () => { const allowPrintDraftInvoices = computed(() => !!posProfile.value?.allow_print_draft_invoices); + const enableCashierSettlement = computed(() => !!posProfile.value?.enable_cashier_settlement); + + const printBackupReceipt = computed(() => !!posProfile.value?.print_backup_receipt); + const cashModeOfPayment = computed(() => posProfile.value?.cash_mode_of_payment || "Cash"); const purchaseTaxes = computed(() => posProfile.value?.purchase_taxes || []); @@ -172,6 +177,7 @@ export const usePosStore = defineStore("pos", () => { disableRoundedTotal.value = false; printSettings.value = null; isReady.value = false; + isCashier.value = true; printFormats.value = []; lastInvoiceName.value = ""; } @@ -185,6 +191,7 @@ export const usePosStore = defineStore("pos", () => { taxInclusiveMode.value = !!result.tax_inclusive; disableRoundedTotal.value = !!result.disable_rounded_total; printSettings.value = result.print_settings || null; + isCashier.value = result.is_cashier ?? true; showOpeningDialog.value = false; isReady.value = true; } @@ -465,6 +472,7 @@ export const usePosStore = defineStore("pos", () => { return { isLoading, isReady, + isCashier, currentView, posOpeningShift, posProfile, @@ -524,6 +532,8 @@ export const usePosStore = defineStore("pos", () => { useCustomerCredit, applyCustomerDiscount, allowPrintDraftInvoices, + enableCashierSettlement, + printBackupReceipt, cashModeOfPayment, purchaseTaxes, hideImages, diff --git a/frontend/src/types/pos.types.ts b/frontend/src/types/pos.types.ts index 6ec2075..eaebaae 100755 --- a/frontend/src/types/pos.types.ts +++ b/frontend/src/types/pos.types.ts @@ -47,6 +47,8 @@ export interface POSProfile { display_item_code?: boolean; allow_zero_rated_items?: boolean; allow_print_draft_invoices?: boolean; + enable_cashier_settlement?: boolean; + print_backup_receipt?: boolean; auto_set_batch?: boolean; search_serial_no?: boolean; tax_inclusive?: boolean; @@ -392,6 +394,7 @@ export interface InvoiceData { currency?: string; conversion_rate?: number; is_credit_sale?: boolean; + pos_awaiting_settlement?: boolean; pos_delivery_charges?: string; pos_delivery_charges_rate?: number; } @@ -633,6 +636,7 @@ export interface ShiftCheckResult { tax_inclusive?: number; print_settings?: PrintSettings; disable_rounded_total?: number; + is_cashier?: boolean; } export interface OpeningData { diff --git a/frontend/src/views/CashierView.vue b/frontend/src/views/CashierView.vue new file mode 100644 index 0000000..780309e --- /dev/null +++ b/frontend/src/views/CashierView.vue @@ -0,0 +1,271 @@ + + + diff --git a/package.json b/package.json index 3077073..e83f60a 100755 --- a/package.json +++ b/package.json @@ -26,6 +26,6 @@ "scripts": { "dev": "cd frontend && yarn dev", "build": "cd frontend && yarn install && yarn build", - "prepare": "pre-commit install && pre-commit install --hook-type commit-msg" + "prepare": "command -v pre-commit > /dev/null 2>&1 && pre-commit install && pre-commit install --hook-type commit-msg || true" } } diff --git a/xpos/api/invoices.py b/xpos/api/invoices.py index dc20996..94a3c8b 100755 --- a/xpos/api/invoices.py +++ b/xpos/api/invoices.py @@ -9,7 +9,7 @@ from frappe.utils import cint, flt, getdate, now_datetime, nowdate from frappe.utils.background_jobs import enqueue -from xpos.api.utilities import get_invoice_type, get_profile_setting +from xpos.api.utilities import get_invoice_type, get_profile_setting, is_pos_cashier def _get_item_rate_precision(): @@ -222,6 +222,11 @@ def create_invoice(data: str | dict): if invoice_doc.docstatus != 0: frappe.throw(_("Only draft invoices can be updated and submitted")) + if invoice_doc.get("pos_awaiting_settlement") and not is_pos_cashier( + frappe.session.user, pos_profile + ): + frappe.throw(_("Only a cashier can settle this invoice."), frappe.PermissionError) + is_existing_draft = True invoice_doc.set("items", []) invoice_doc.set("payments", []) @@ -671,7 +676,7 @@ def save_draft_invoice(data: str | dict): }, ) - _ensure_pos_invoice_payment_row(invoice_doc, pos, get_invoice_type() == "POS Invoice") + _ensure_pos_invoice_payment_row(invoice_doc, pos, doctype == "POS Invoice") if pos_opening_shift: try: @@ -679,6 +684,9 @@ def save_draft_invoice(data: str | dict): except Exception: pass + if data.get("pos_awaiting_settlement") and frappe.db.has_column(doctype, "pos_awaiting_settlement"): + invoice_doc.pos_awaiting_settlement = 1 + _apply_invoice_delivery_charge_fields(invoice_doc, data) if is_update: @@ -725,6 +733,47 @@ def get_draft_invoices(pos_opening_shift: str): return invoices +@frappe.whitelist() +def get_unsettled_invoices(pos_profile: str | None = None): + """Get draft invoices awaiting cashier settlement for a POS profile. + + These are unsubmitted invoices created by a terminal in cashier-settlement + mode (``pos_awaiting_settlement = 1``). They are listed on the Cashier screen + regardless of which terminal, operator or shift created them. Once settled + (submitted), they drop out of this list via the ``docstatus = 0`` filter. + """ + doctype = get_invoice_type() + + if not frappe.db.has_column(doctype, "pos_awaiting_settlement"): + return [] + + if not is_pos_cashier(frappe.session.user, pos_profile): + frappe.throw(_("You are not permitted to settle invoices."), frappe.PermissionError) + + filters = {"docstatus": 0, "is_pos": 1, "pos_awaiting_settlement": 1} + if pos_profile: + filters["pos_profile"] = pos_profile + + return frappe.get_list( + doctype, + filters=filters, + fields=[ + "name", + "customer", + "customer_name", + "posting_date", + "posting_time", + "grand_total", + "total_qty", + "currency", + "creation", + "modified", + ], + limit_page_length=0, + order_by="modified desc", + ) + + @frappe.whitelist() def get_past_orders( pos_profile: str = "", diff --git a/xpos/api/shifts.py b/xpos/api/shifts.py index 57a5042..5970a0d 100755 --- a/xpos/api/shifts.py +++ b/xpos/api/shifts.py @@ -6,7 +6,7 @@ import frappe from frappe import Any, _ from frappe.utils import cint, flt, now_datetime, nowdate -from xpos.api.utilities import get_invoice_type +from xpos.api.utilities import get_invoice_type, is_pos_cashier def _row_value(row: dict | object, key: str, default: Any | None = None): @@ -391,6 +391,7 @@ def _enrich_shift_data(data: dict, pos_profile: str): profile = frappe.get_doc("POS Profile", pos_profile) data["pos_profile"] = profile.as_dict() data["company"] = frappe.get_cached_doc("Company", profile.company).as_dict() + data["is_cashier"] = is_pos_cashier(frappe.session.user, pos_profile) allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock") or 0) data["stock_settings"] = {"allow_negative_stock": bool(allow_negative_stock)} diff --git a/xpos/api/utilities.py b/xpos/api/utilities.py index c9beafb..03ab6da 100755 --- a/xpos/api/utilities.py +++ b/xpos/api/utilities.py @@ -168,4 +168,38 @@ def get_default_warehouse(company: str | None = None): def get_invoice_type(): """Returns the invoice type based on POS settings.""" - return frappe.get_single_value("POS Settings", "invoice_type") or "Sales Invoice" \ No newline at end of file + return frappe.get_single_value("POS Settings", "invoice_type") or "Sales Invoice" + + +def is_pos_cashier(user: str | None = None, pos_profile: str | None = None) -> bool: + """Return whether ``user`` may settle (close) bills on the Cashier screen. + + A user qualifies if their row in the POS Profile's ``applicable_for_users`` + child table has ``is_cashier`` checked. Administrators and System Managers + always qualify so they are never locked out. Until the ``is_cashier`` custom + field is deployed (pre-migration) the restriction is inactive and everyone + qualifies, so existing behaviour is preserved. + """ + user = user or frappe.session.user + + if user == "Administrator" or "System Manager" in frappe.get_roles(user): + return True + + if not frappe.db.has_column("POS Profile User", "is_cashier"): + return True + + if not pos_profile: + pos_profile = frappe.db.get_value( + "POS Profile User", {"user": user}, "parent" + ) or frappe.db.get_single_value("POS Settings", "pos_profile") + + if not pos_profile: + return False + + return bool( + frappe.db.get_value( + "POS Profile User", + {"parent": pos_profile, "parenttype": "POS Profile", "user": user}, + "is_cashier", + ) + ) \ No newline at end of file diff --git a/xpos/x_pos/custom/pos_invoice.json b/xpos/x_pos/custom/pos_invoice.json index 4ac941e..a96427b 100644 --- a/xpos/x_pos/custom/pos_invoice.json +++ b/xpos/x_pos/custom/pos_invoice.json @@ -321,6 +321,70 @@ "unique": 0, "width": null }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-05-28 00:00:00.000000", + "default": "0", + "depends_on": null, + "description": "Set by X POS when the terminal sends an order to the cashier for settlement. The cashier collects payment and submits the invoice from the Cashier screen.", + "docstatus": 0, + "dt": "POS Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "pos_awaiting_settlement", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 113, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "pos_opening_shift", + "is_system_generated": 0, + "is_virtual": 0, + "label": "POS Awaiting Settlement", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-28 00:00:00.000000", + "modified_by": "Administrator", + "module": "X POS", + "name": "POS Invoice-pos_awaiting_settlement", + "no_copy": 1, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, { "_assign": null, "_comments": null, diff --git a/xpos/x_pos/custom/pos_profile.json b/xpos/x_pos/custom/pos_profile.json index 35d4962..eeab5ea 100755 --- a/xpos/x_pos/custom/pos_profile.json +++ b/xpos/x_pos/custom/pos_profile.json @@ -924,6 +924,138 @@ "unique": 0, "width": null }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "alignment": "", + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "button_color": "", + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-05-28 00:00:00.000000", + "default": null, + "depends_on": null, + "description": "When enabled, the POS terminal creates an unsettled invoice instead of collecting payment. A cashier settles it later from the Cashier screen.", + "docstatus": 0, + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "enable_cashier_settlement", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "allow_print_draft_invoices", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Enable cashier settlement", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-28 00:00:00.000000", + "modified_by": "Administrator", + "module": "X POS", + "name": "POS Profile-enable_cashier_settlement", + "no_copy": 0, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "alignment": "", + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "button_color": "", + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-05-28 00:00:00.000000", + "default": null, + "depends_on": "eval:doc.enable_cashier_settlement", + "description": "Print a non-genuine backup receipt for the customer at the terminal. No payment is collected; the genuine invoice is printed by the cashier after settlement.", + "docstatus": 0, + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "print_backup_receipt", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "enable_cashier_settlement", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Print backup receipt", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-28 00:00:00.000000", + "modified_by": "Administrator", + "module": "X POS", + "name": "POS Profile-print_backup_receipt", + "no_copy": 0, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, { "_assign": null, "_comments": null, @@ -6575,7 +6707,7 @@ "property": "field_order", "property_type": "Data", "row_name": null, - "value": "[\"company\", \"customer\", \"country\", \"disabled\", \"column_break_9\", \"warehouse\", \"company_address\", \"section_break_15\", \"applicable_for_users\", \"section_break_11\", \"payments\", \"section_break_14\", \"hide_images\", \"hide_unavailable_items\", \"auto_add_item_to_cart\", \"validate_stock_on_save\", \"print_receipt_on_order_complete\", \"action_on_new_invoice\", \"column_break_16\", \"update_stock\", \"ignore_pricing_rule\", \"allow_rate_change\", \"allow_discount_change\", \"set_grand_total_to_default_mop\", \"allow_partial_payment\", \"section_break_23\", \"item_groups\", \"column_break_25\", \"customer_groups\", \"section_break_16\", \"print_format\", \"letter_head\", \"column_break0\", \"tc_name\", \"select_print_heading\", \"section_break_19\", \"selling_price_list\", \"currency\", \"write_off_account\", \"write_off_cost_center\", \"write_off_limit\", \"account_for_change_amount\", \"disable_rounded_total\", \"column_break_23\", \"income_account\", \"expense_account\", \"taxes_and_charges\", \"tax_category\", \"apply_discount_on\", \"accounting_dimensions_section\", \"cost_center\", \"dimension_col_break\", \"project\", \"utm_analytics_section\", \"utm_source\", \"column_break_tvls\", \"utm_campaign\", \"column_break_xygw\", \"utm_medium\", \"pos_settings\", \"settings_section\", \"auto_delete_draft_invoice\", \"allow_user_to_edit_additional_discount\", \"allow_change_posting_date\", \"allow_credit_sale\", \"allow_return\", \"allow_return_without_invoice\", \"allow_delete\", \"allow_sales_order\", \"allow_free_batch_return\", \"allow_multi_currency\", \"apply_customer_discount\", \"allow_duplicate_customer_names\", \"tax_inclusive\", \"allow_zero_rated_items\", \"hide_expected_amount\", \"display_additional_notes\", \"allow_write_off_change\", \"hide_variants_items\", \"force_price_from_customer_price_list\", \"column_break_zfglz\", \"use_cashback\", \"use_customer_credit\", \"hide_closing_shift\", \"display_item_code\", \"display_items_in_stock\", \"show_template_items\", \"input_qty\", \"auto_set_batch\", \"search_serial_no\", \"search_batch_no\", \"use_limit_search\", \"force_reload_items\", \"use_server_cache\", \"allow_print_draft_invoices\", \"use_delivery_charges\", \"auto_set_delivery_charges\", \"show_customer_balance\", \"column_break_aebm5\", \"max_discount_percentage_allowed\", \"default_view\", \"server_cache_duration\", \"item_search_limit\", \"pos_payments_section\", \"default_pos_expense_account\", \"cash_mode_of_payment\", \"back_office_cash_account\", \"default_source_account\", \"cash_movement_max_amount\", \"allowed_source_accounts\", \"allowed_expense_accounts\", \"column_break_gw92o\", \"allow_pos_expense\", \"allow_source_account_override\", \"require_cash_movement_remarks\", \"allow_delete_cancelled_cash_movement\", \"allow_cancel_submitted_cash_movement\", \"enable_cash_movement\", \"allow_make_new_payments\", \"allow_reconcile_payments\", \"use_pos_payments\", \"allow_cash_deposit\", \"section_break_tjwtr\", \"allow_print_last_invoice\", \"default_print_format\", \"print_format_rules\", \"print_discount_amount\", \"referral_settings\", \"auto_create_referral_for_new_customers\", \"auto_fetch_coupons_gifts\", \"fbr_integration_section\", \"enable_fbr_integration\", \"fbr_environment\", \"fbr_pos_id\", \"column_break_fbr\", \"fbr_bearer_token\", \"fbr_api_url\", \"fbr_skip_ssl_verification\", \"selling\", \"allowed_sales_persons\", \"column_break_pvbpj\", \"use_offline_mode\", \"fetch_items_directly_from_server\", \"block_sale_beyond_available_qty\", \"allow_submissions_in_background_job\", \"allow_delete_offline_invoice\", \"allow_delete_draft_invoices\", \"enable_return_validity\", \"return_validity_days\", \"column_break_tgaeu\", \"purchasing\", \"allow_create_purchase_items\", \"allow_create_purchase_suppliers\", \"allow_purchase_receipt\", \"allow_purchase_order\", \"update_selling_price\", \"update_buying_price\", \"default_purchase_uom\", \"purchase_taxes\", \"column_break_cnwjh\", \"display_authorization_code\"]" + "value": "[\"company\", \"customer\", \"country\", \"disabled\", \"column_break_9\", \"warehouse\", \"company_address\", \"section_break_15\", \"applicable_for_users\", \"section_break_11\", \"payments\", \"section_break_14\", \"hide_images\", \"hide_unavailable_items\", \"auto_add_item_to_cart\", \"validate_stock_on_save\", \"print_receipt_on_order_complete\", \"action_on_new_invoice\", \"column_break_16\", \"update_stock\", \"ignore_pricing_rule\", \"allow_rate_change\", \"allow_discount_change\", \"set_grand_total_to_default_mop\", \"allow_partial_payment\", \"section_break_23\", \"item_groups\", \"column_break_25\", \"customer_groups\", \"section_break_16\", \"print_format\", \"letter_head\", \"column_break0\", \"tc_name\", \"select_print_heading\", \"section_break_19\", \"selling_price_list\", \"currency\", \"write_off_account\", \"write_off_cost_center\", \"write_off_limit\", \"account_for_change_amount\", \"disable_rounded_total\", \"column_break_23\", \"income_account\", \"expense_account\", \"taxes_and_charges\", \"tax_category\", \"apply_discount_on\", \"accounting_dimensions_section\", \"cost_center\", \"dimension_col_break\", \"project\", \"utm_analytics_section\", \"utm_source\", \"column_break_tvls\", \"utm_campaign\", \"column_break_xygw\", \"utm_medium\", \"pos_settings\", \"settings_section\", \"auto_delete_draft_invoice\", \"allow_user_to_edit_additional_discount\", \"allow_change_posting_date\", \"allow_credit_sale\", \"allow_return\", \"allow_return_without_invoice\", \"allow_delete\", \"allow_sales_order\", \"allow_free_batch_return\", \"allow_multi_currency\", \"apply_customer_discount\", \"allow_duplicate_customer_names\", \"tax_inclusive\", \"allow_zero_rated_items\", \"hide_expected_amount\", \"display_additional_notes\", \"allow_write_off_change\", \"hide_variants_items\", \"force_price_from_customer_price_list\", \"column_break_zfglz\", \"use_cashback\", \"use_customer_credit\", \"hide_closing_shift\", \"display_item_code\", \"display_items_in_stock\", \"show_template_items\", \"input_qty\", \"auto_set_batch\", \"search_serial_no\", \"search_batch_no\", \"use_limit_search\", \"force_reload_items\", \"use_server_cache\", \"allow_print_draft_invoices\", \"enable_cashier_settlement\", \"print_backup_receipt\", \"use_delivery_charges\", \"auto_set_delivery_charges\", \"show_customer_balance\", \"column_break_aebm5\", \"max_discount_percentage_allowed\", \"default_view\", \"server_cache_duration\", \"item_search_limit\", \"pos_payments_section\", \"default_pos_expense_account\", \"cash_mode_of_payment\", \"back_office_cash_account\", \"default_source_account\", \"cash_movement_max_amount\", \"allowed_source_accounts\", \"allowed_expense_accounts\", \"column_break_gw92o\", \"allow_pos_expense\", \"allow_source_account_override\", \"require_cash_movement_remarks\", \"allow_delete_cancelled_cash_movement\", \"allow_cancel_submitted_cash_movement\", \"enable_cash_movement\", \"allow_make_new_payments\", \"allow_reconcile_payments\", \"use_pos_payments\", \"allow_cash_deposit\", \"section_break_tjwtr\", \"allow_print_last_invoice\", \"default_print_format\", \"print_format_rules\", \"print_discount_amount\", \"referral_settings\", \"auto_create_referral_for_new_customers\", \"auto_fetch_coupons_gifts\", \"fbr_integration_section\", \"enable_fbr_integration\", \"fbr_environment\", \"fbr_pos_id\", \"column_break_fbr\", \"fbr_bearer_token\", \"fbr_api_url\", \"fbr_skip_ssl_verification\", \"selling\", \"allowed_sales_persons\", \"column_break_pvbpj\", \"use_offline_mode\", \"fetch_items_directly_from_server\", \"block_sale_beyond_available_qty\", \"allow_submissions_in_background_job\", \"allow_delete_offline_invoice\", \"allow_delete_draft_invoices\", \"enable_return_validity\", \"return_validity_days\", \"column_break_tgaeu\", \"purchasing\", \"allow_create_purchase_items\", \"allow_create_purchase_suppliers\", \"allow_purchase_receipt\", \"allow_purchase_order\", \"update_selling_price\", \"update_buying_price\", \"default_purchase_uom\", \"purchase_taxes\", \"column_break_cnwjh\", \"display_authorization_code\"]" }, { "_assign": null, diff --git a/xpos/x_pos/custom/pos_profile_user.json b/xpos/x_pos/custom/pos_profile_user.json new file mode 100644 index 0000000..e640be9 --- /dev/null +++ b/xpos/x_pos/custom/pos_profile_user.json @@ -0,0 +1,73 @@ +{ + "custom_fields": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-05-29 00:00:00.000000", + "default": "0", + "depends_on": null, + "description": "If checked, this user is a cashier and may open the Cashier screen to settle (close) bills sent from POS terminals.", + "docstatus": 0, + "dt": "POS Profile User", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "is_cashier", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "user", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Is Cashier", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-29 00:00:00.000000", + "modified_by": "Administrator", + "module": "X POS", + "name": "POS Profile User-is_cashier", + "no_copy": 0, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + } + ], + "custom_perms": [], + "doctype": "POS Profile User", + "links": [], + "property_setters": [], + "sync_on_migrate": 1 +} diff --git a/xpos/x_pos/custom/sales_invoice.json b/xpos/x_pos/custom/sales_invoice.json index e3beee9..c5c95ca 100755 --- a/xpos/x_pos/custom/sales_invoice.json +++ b/xpos/x_pos/custom/sales_invoice.json @@ -321,6 +321,70 @@ "unique": 0, "width": null }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-05-28 00:00:00.000000", + "default": "0", + "depends_on": null, + "description": "Set by X POS when the terminal sends an order to the cashier for settlement. The cashier collects payment and submits the invoice from the Cashier screen.", + "docstatus": 0, + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "pos_awaiting_settlement", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 113, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "pos_opening_shift", + "is_system_generated": 0, + "is_virtual": 0, + "label": "POS Awaiting Settlement", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-05-28 00:00:00.000000", + "modified_by": "Administrator", + "module": "X POS", + "name": "Sales Invoice-pos_awaiting_settlement", + "no_copy": 1, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, { "_assign": null, "_comments": null,