diff --git a/POS/components.d.ts b/POS/components.d.ts
index 1f2e8b35..824f6299 100644
--- a/POS/components.d.ts
+++ b/POS/components.d.ts
@@ -26,8 +26,10 @@ declare module 'vue' {
InvoiceFilters: typeof import('./src/components/invoices/InvoiceFilters.vue')['default']
InvoiceHistoryDialog: typeof import('./src/components/sale/InvoiceHistoryDialog.vue')['default']
InvoiceManagement: typeof import('./src/components/invoices/InvoiceManagement.vue')['default']
+ ItemModifiersDialog: typeof import('./src/components/sale/ItemModifiersDialog.vue')['default']
ItemSelectionDialog: typeof import('./src/components/sale/ItemSelectionDialog.vue')['default']
ItemsSelector: typeof import('./src/components/sale/ItemsSelector.vue')['default']
+ KDSOrderCard: typeof import('./src/components/invoices/KDSOrderCard.vue')['default']
LanguageSwitcher: typeof import('./src/components/common/LanguageSwitcher.vue')['default']
LazyImage: typeof import('./src/components/common/LazyImage.vue')['default']
LoadingSpinner: typeof import('./src/components/common/LoadingSpinner.vue')['default']
@@ -52,6 +54,7 @@ declare module 'vue' {
ShiftClosingDialog: typeof import('./src/components/ShiftClosingDialog.vue')['default']
ShiftOpeningDialog: typeof import('./src/components/ShiftOpeningDialog.vue')['default']
StatusBadge: typeof import('./src/components/common/StatusBadge.vue')['default']
+ TableSelector: typeof import('./src/components/pos/TableSelector.vue')['default']
Toast: typeof import('./src/components/common/Toast.vue')['default']
TranslatedHTML: typeof import('./src/components/common/TranslatedHTML.vue')['default']
UserMenu: typeof import('./src/components/common/UserMenu.vue')['default']
diff --git a/POS/index.html b/POS/index.html
index 2d540d0f..d9e83b64 100644
--- a/POS/index.html
+++ b/POS/index.html
@@ -11,9 +11,9 @@
-
+
-
POSNext
+ Olko
diff --git a/POS/src/components/common/POSFooter.vue b/POS/src/components/common/POSFooter.vue
index 49a7f1c2..9f8246c6 100644
--- a/POS/src/components/common/POSFooter.vue
+++ b/POS/src/components/common/POSFooter.vue
@@ -17,8 +17,8 @@ import { call } from '@/utils/apiWrapper'
// Component state
const footerText = ref('Powered by')
-const linkText = ref('BrainWise')
-const footerLink = ref('https://nexus.brainwise.me')
+const linkText = ref('Midiya')
+const footerLink = ref('https://midiya.az')
const footerRoot = ref(null)
const config = ref({})
const serverValidationEnabled = ref(true)
@@ -57,35 +57,17 @@ let validationTimer = null
// Load branding configuration from backend
const loadBrandingConfig = async () => {
- try {
- const response = await call('pos_next.api.branding.get_branding_config')
-
- if (response) {
- config.value = response
-
- // Decode base64 encoded values
- footerText.value = atob(response._t || '')
- linkText.value = atob(response._l || '')
- footerLink.value = atob(response._u || '')
- serverValidationEnabled.value = response._v || false
-
- // Update check interval if provided
- if (response._i && integrityTimer) {
- clearInterval(integrityTimer)
- integrityTimer = setInterval(checkIntegrity, response._i)
- }
-
- // Start server validation if enabled
- if (serverValidationEnabled.value) {
- startServerValidation()
- }
- }
- } catch (error) {
- console.error('[BrainWise] Failed to load branding config:', error)
- // Use fallback values
- footerText.value = 'Powered by'
- linkText.value = 'BrainWise'
- footerLink.value = 'https://nexus.brainwise.me'
+ // Force static values to bypass backend config overwriting
+ footerText.value = 'Powered by'
+ linkText.value = 'Midiya'
+ footerLink.value = 'https://midiya.az'
+ serverValidationEnabled.value = false
+
+ // Set dummy config values for any style computations
+ config.value = {
+ _l: btoa('Midiya'),
+ _u: btoa('https://midiya.az'),
+ _t: btoa('Powered by')
}
}
@@ -130,8 +112,8 @@ const logClientEvent = async (eventType, details = {}) => {
const ensureBranding = () => {
if (!footerRoot.value) return
- const expectedBrand = atob(config.value._l || btoa('BrainWise'))
- const expectedUrl = atob(config.value._u || btoa('https://nexus.brainwise.me'))
+ const expectedBrand = atob(config.value._l || btoa('Midiya'))
+ const expectedUrl = atob(config.value._u || btoa('https://midiya.az'))
const expectedText = atob(config.value._t || btoa('Powered by'))
// Check if values have been tampered
diff --git a/POS/src/components/invoices/KDSOrderCard.vue b/POS/src/components/invoices/KDSOrderCard.vue
new file mode 100644
index 00000000..1426f3e6
--- /dev/null
+++ b/POS/src/components/invoices/KDSOrderCard.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
{{ order.restaurant_table }}
+ #{{ order.name.substring(0,8) }}
+
+
+
+
+ {{ elapsedTime }}
+
+
+ {{ __(order.kds_status) }}
+
+
+
+
+
+
+
+
+
+
+ {{ item.item_name }}
+
+
+ {{ item.description }}
+
+
+ {{ item.posa_special_instructions }}
+
+
+
+ {{ item.qty }}
+
+
+
+
+
+
+
+
+
+ {{ __("Start Preparing") }}
+
+
+
+
+ {{ __("Mark Ready") }}
+
+
+
+
+ {{ __("Delivered / Dismiss") }}
+
+
+
+
+
+
diff --git a/POS/src/components/pos/POSHeader.vue b/POS/src/components/pos/POSHeader.vue
index bcb2edf8..47869bf9 100644
--- a/POS/src/components/pos/POSHeader.vue
+++ b/POS/src/components/pos/POSHeader.vue
@@ -7,8 +7,8 @@
@@ -22,7 +22,7 @@
-
{{ 'POS Next' }}
+
{{ 'Olko' }}
v{{ appVersion }}
diff --git a/POS/src/components/pos/TableSelector.vue b/POS/src/components/pos/TableSelector.vue
new file mode 100644
index 00000000..16ec90cc
--- /dev/null
+++ b/POS/src/components/pos/TableSelector.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+ {{ __("Select Table") }}
+
+
+ {{ __("Choose a table to begin the order") }}
+
+
+
+
+
+
+ {{ __("All") }}
+
+
+ {{ area.area_name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ table.table_name }}
+
+
+ {{ table.capacity }} {{ __("Seats") }}
+
+
+
+ {{ __(table.status) }}
+
+
+
+
+
+
+
{{ __("No tables found in this area") }}
+
+
+
+
+
+
diff --git a/POS/src/components/sale/DraftInvoicesDialog.vue b/POS/src/components/sale/DraftInvoicesDialog.vue
index 461f8d94..d8f54332 100644
--- a/POS/src/components/sale/DraftInvoicesDialog.vue
+++ b/POS/src/components/sale/DraftInvoicesDialog.vue
@@ -36,6 +36,20 @@
{{ formatDateTime(draft.created_at) }}
+
+
+
+ Table: {{ draft.restaurant_table }}
+
+
+ KDS: {{ draft.kds_status }}
+
+
+
@@ -158,6 +159,7 @@
{{ __("Order") }}
+
@@ -793,6 +795,15 @@
>
{{ item.item_name }}
+
+
+
+ {{ __("Note") }}
+
+
+
+
+
+
+
{{ __("Hold", null, "order") }}
+
+
+
+
+ {{ __("Kitchen") }}
+
@@ -1285,6 +1326,11 @@ function handleProceedToPayment() {
emit("proceed-to-payment");
}
+function sendToKitchen() {
+ cartStore.setKdsStatus("Pending");
+ emit("save-draft");
+}
+
/**
* ============================================================================
* PROPS
diff --git a/POS/src/components/sale/ItemModifiersDialog.vue b/POS/src/components/sale/ItemModifiersDialog.vue
new file mode 100644
index 00000000..ba7f5cd9
--- /dev/null
+++ b/POS/src/components/sale/ItemModifiersDialog.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
{{ item.item_name }}
+
{{ item.item_code }}
+
+
{{ item.quantity }} {{ item.uom }}
+
+
+
+
{{ __("Quick Modifiers") }}
+
+
+ {{ __(mod) }}
+
+
+
+
+
+ {{ __("Custom Instructions") }}
+
+
+
+
+
+
+
+
+ {{ __("Cancel") }}
+
+
+ {{ __("Save Instructions") }}
+
+
+
+
+
+
+
diff --git a/POS/src/components/sale/ItemsSelector.vue b/POS/src/components/sale/ItemsSelector.vue
index bb06e34e..dc7698ea 100644
--- a/POS/src/components/sale/ItemsSelector.vue
+++ b/POS/src/components/sale/ItemsSelector.vue
@@ -839,7 +839,7 @@ const totalPages = computed(() => {
const SEARCH_PLACEHOLDERS = Object.freeze({
auto: __("Auto-Add ON - Type or scan barcode"),
scanner: __("Scanner ON - Enable Auto for automatic addition"),
- default: __("Search by item code, name, item group or scan barcode"),
+ default: __("Search by item code, name or scan barcode"),
})
// Sort configuration
diff --git a/POS/src/composables/useInvoice.js b/POS/src/composables/useInvoice.js
index 66c7c58f..f54449ca 100644
--- a/POS/src/composables/useInvoice.js
+++ b/POS/src/composables/useInvoice.js
@@ -756,6 +756,7 @@ export function useInvoice() {
is_rate_manually_edited: item.is_rate_manually_edited || 0,
original_rate: item.original_rate || null,
is_free_item: item.is_free_item || 0,
+ posa_special_instructions: item.posa_special_instructions || "",
}))
}
@@ -892,12 +893,16 @@ export function useInvoice() {
const rawItems = toRaw(invoiceItems.value)
const rawPayments = toRaw(payments.value)
const rawSalesTeam = toRaw(salesTeam.value)
+ const { usePOSCartStore } = await import("@/stores/posCart")
+ const cartStore = usePOSCartStore()
const invoiceData = {
doctype: targetDoctype,
pos_profile: posProfile.value,
posa_pos_opening_shift: posOpeningShift.value,
customer: customer.value?.name || customer.value,
+ restaurant_table: cartStore.restaurantTable?.name,
+ kds_status: cartStore.kdsStatus,
items: formatItemsForSubmission(rawItems),
payments: rawPayments.map((p) => ({
mode_of_payment: p.mode_of_payment,
diff --git a/POS/src/composables/useItems.js b/POS/src/composables/useItems.js
index f7fc24a5..ce07480b 100644
--- a/POS/src/composables/useItems.js
+++ b/POS/src/composables/useItems.js
@@ -67,8 +67,7 @@ export function useItems(posProfile, cartItems = ref([])) {
(item) =>
item.item_name?.toLowerCase().includes(term) ||
item.item_code?.toLowerCase().includes(term) ||
- item.barcode?.toLowerCase().includes(term) ||
- item.item_group?.toLowerCase().includes(term)
+ item.barcode?.toLowerCase().includes(term),
)
}
diff --git a/POS/src/pages/CFD.vue b/POS/src/pages/CFD.vue
new file mode 100644
index 00000000..0435f8ac
--- /dev/null
+++ b/POS/src/pages/CFD.vue
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
{{ itemCount }} {{ __("Items") }}
+
+
+
+
+
+
{{ __("Welcome! Please place your order at the counter.") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ __("Order Total") }}
+
+
+
+ {{ __("Subtotal") }}
+ {{ formatCFDCurrency(subtotal) }}
+
+
+
+ {{ __("Tax") }}
+ {{ formatCFDCurrency(totalTax) }}
+
+
+
+ {{ __("Discount") }}
+ - {{ formatCFDCurrency(totalDiscount) }}
+
+
+
+
+ {{ __("Total to Pay") }}
+ {{ formatCFDCurrency(grandTotal) }}
+
+
+
+
+
+
+
+
{{ __("Thank you for your visit!") }}
+
Powered by Midiya
+
+
+
+
+
+
+
+
diff --git a/POS/src/pages/KDS.vue b/POS/src/pages/KDS.vue
new file mode 100644
index 00000000..2b1207e1
--- /dev/null
+++ b/POS/src/pages/KDS.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ __("No Active Orders") }}
+
{{ __("Kitchen is clear.") }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/POS/src/pages/POSSale.vue b/POS/src/pages/POSSale.vue
index 20a7664b..0194853f 100644
--- a/POS/src/pages/POSSale.vue
+++ b/POS/src/pages/POSSale.vue
@@ -285,13 +285,29 @@
]"
style="contain: layout style paint"
>
-
+
+
+
+
+
+
+
+
Table: {{ cartStore.restaurantTable.table_name }}
+
+
+
+
+
+
+
+
@@ -354,6 +370,7 @@
@proceed-to-payment="handleProceedToPayment"
@clear-cart="handleClearCart"
@save-draft="handleSaveDraft"
+ @open-modifiers="handleOpenModifiers"
@apply-coupon="uiStore.showCouponDialog = true"
@show-offers="uiStore.showOffersDialog = true"
@remove-offer="
@@ -983,6 +1000,7 @@ import InvoiceCart from "@/components/sale/InvoiceCart.vue";
import InvoiceHistoryDialog from "@/components/sale/InvoiceHistoryDialog.vue";
import ItemSelectionDialog from "@/components/sale/ItemSelectionDialog.vue";
import ItemsSelector from "@/components/sale/ItemsSelector.vue";
+import TableSelector from "@/components/pos/TableSelector.vue";
import OffersDialog from "@/components/sale/OffersDialog.vue";
import OfflineInvoicesDialog from "@/components/sale/OfflineInvoicesDialog.vue";
import PaymentDialog from "@/components/sale/PaymentDialog.vue";
@@ -1018,6 +1036,7 @@ import { usePOSCartStore } from "@/stores/posCart";
import { usePOSDraftsStore } from "@/stores/posDrafts";
import { usePOSSettingsStore } from "@/stores/posSettings";
import { usePOSShiftStore } from "@/stores/posShift";
+import { useRestaurantStore } from "@/stores/restaurant";
import { usePOSSyncStore } from "@/stores/posSync";
import { usePOSUIStore } from "@/stores/posUI";
import { logger } from "@/utils/logger";
@@ -1026,6 +1045,7 @@ import { shouldValidateItemStock } from "@/utils/stockValidator";
// Initialize stores
const cartStore = usePOSCartStore();
const shiftStore = usePOSShiftStore();
+const restaurantStore = useRestaurantStore();
const uiStore = usePOSUIStore();
const offlineStore = usePOSSyncStore();
const draftsStore = usePOSDraftsStore();
@@ -1068,6 +1088,17 @@ const itemsSelectorRef = ref(null);
const offersDialogRef = ref(null);
const containerRef = ref(null);
const dividerRef = ref(null);
+const itemModifiersDialogRef = ref(null);
+
+function handleOpenModifiers(item) {
+ if (itemModifiersDialogRef.value) {
+ itemModifiersDialogRef.value.open(item);
+ }
+}
+
+function closeTable() {
+ cartStore.clearCart();
+}
const pendingPaymentAfterCustomer = ref(false);
const logoutAfterClose = ref(false);
const editCustomer = ref(null); // Customer being edited (null for create mode)
@@ -1875,7 +1906,6 @@ function handleCustomerSelected(selectedCustomer) {
if (selectedCustomer) {
cartStore.setCustomer(selectedCustomer);
uiStore.showCustomerDialog = false;
- showSuccess(__("{0} selected", [selectedCustomer.customer_name]));
if (pendingPaymentAfterCustomer.value) {
pendingPaymentAfterCustomer.value = false;
@@ -2094,7 +2124,6 @@ function confirmClearCart() {
// Reset cart hash when cart is cleared
previousCartHash = "";
uiStore.showClearCartDialog = false;
- showSuccess(__("All items removed from cart"));
}
async function handleOptionSelected(option) {
@@ -2140,7 +2169,6 @@ async function handleOptionSelected(option) {
);
uiStore.showItemSelectionDialog = false;
cartStore.clearPendingItem();
- showSuccess(__("{0} added to cart", [variant.item_name]));
} catch (error) {
showError(error.message);
}
@@ -2168,7 +2196,6 @@ async function handleOptionSelected(option) {
cartStore.addItem(itemToAdd, qty, false, shiftStore.currentProfile);
uiStore.showItemSelectionDialog = false;
cartStore.clearPendingItem();
- showSuccess(__("{0} ({1}) added to cart", [itemToAdd.item_name, option.uom]));
} catch (error) {
showError(error.message);
}
@@ -2203,17 +2230,78 @@ function logoutWithCloseShift() {
}
async function handleSaveDraft() {
+ // If restaurant mode is on, we need to save the draft locally AND push it to the server as a true draft
+ // so the KDS system can see it. But POSNext "Drafts" are usually offline-only until submitted.
+ // Actually, wait, let's just make sure the `kdsStatus` is explicitly saved.
+
const savedDraft = await draftsStore.saveDraftInvoice(
cartStore.invoiceItems,
cartStore.customer,
cartStore.posProfile,
cartStore.appliedOffers,
- cartStore.currentDraftId
+ cartStore.currentDraftId,
+ cartStore.restaurantTable?.name,
+ cartStore.kdsStatus
);
+
if (savedDraft) {
- cartStore.clearCart();
- // Reset cart hash when cart is saved as draft and cleared
- previousCartHash = "";
+ // If restaurant table is set, we also need to push this draft to the backend right away
+ // because the KDS API reads directly from the Frappe database (`Sales Invoice` with docstatus=0).
+ if (cartStore.restaurantTable && navigator.onLine) {
+ try {
+ const { useInvoice } = await import("@/composables/useInvoice");
+ const { posOpeningShift, additionalDiscount, payments, salesTeam } = cartStore;
+ // Format items for submission
+ const formattedItems = cartStore.invoiceItems.map(item => ({
+ item_code: item.item_code,
+ item_name: item.item_name,
+ qty: item.quantity,
+ rate: item.rate,
+ uom: item.uom,
+ warehouse: item.warehouse,
+ posa_special_instructions: item.posa_special_instructions || "",
+ discount_amount: item.discount_amount || 0,
+ discount_percentage: item.discount_percentage || 0
+ }));
+
+ const invoiceData = {
+ doctype: "Sales Invoice",
+ pos_profile: cartStore.posProfile,
+ posa_pos_opening_shift: cartStore.posOpeningShift,
+ customer: cartStore.customer?.name || cartStore.customer,
+ restaurant_table: cartStore.restaurantTable?.name,
+ kds_status: cartStore.kdsStatus,
+ items: formattedItems,
+ is_pos: 1,
+ docstatus: 0 // Explicitly draft
+ };
+
+ // Include name if we are updating an existing draft on the backend
+ if (cartStore.currentDraftId && cartStore.currentDraftId.startsWith('ACC-SINV')) {
+ invoiceData.name = cartStore.currentDraftId;
+ }
+
+ const response = await call("pos_next.api.invoices.update_invoice", { data: invoiceData });
+
+ // Keep local offline draft ID in sync with the newly created backend Draft ID so subsequent updates work
+ if (response && response.name && (!cartStore.currentDraftId || !cartStore.currentDraftId.startsWith('ACC-SINV'))) {
+ // We just created a new draft on the server, update our local copy to use the server's ID
+ await draftsStore.updateDraft(savedDraft.draft_id, { draft_id: response.name });
+ }
+
+ // Update table status
+ await restaurantStore.updateTableStatus(cartStore.restaurantTable.name, "Occupied");
+ } catch (error) {
+ console.error("Failed to sync draft to kitchen:", error);
+ }
+
+ cartStore.hasUnsentChanges = false;
+ previousCartHash = computeCartHash();
+ } else {
+ cartStore.clearCart();
+ // Reset cart hash when cart is saved as draft and cleared
+ previousCartHash = "";
+ }
}
}
@@ -2226,7 +2314,9 @@ async function handleLoadDraft(draft) {
cartStore.customer,
cartStore.posProfile,
cartStore.appliedOffers,
- cartStore.currentDraftId
+ cartStore.currentDraftId,
+ cartStore.restaurantTable?.name,
+ cartStore.kdsStatus
);
if (!saved) {
@@ -2241,10 +2331,22 @@ async function handleLoadDraft(draft) {
}
const draftData = await draftsStore.loadDraft(draft);
+ cartStore.clearCart(); // Ensure a clean slate
cartStore.invoiceItems = draftData.items;
cartStore.setCustomer(draftData.customer);
cartStore.currentDraftId = draft.draft_id; // Set current draft ID
+ if (draftData.restaurant_table) {
+ const tables = restaurantStore.tables;
+ const table = tables.find(t => t.name === draftData.restaurant_table);
+ if (table) {
+ cartStore.setRestaurantTable(table);
+ }
+ }
+ if (draftData.kds_status) {
+ cartStore.setKdsStatus(draftData.kds_status);
+ }
+
// Rebuild incremental cache to recalculate totals
cartStore.rebuildIncrementalCache();
diff --git a/POS/src/router.js b/POS/src/router.js
index 02b029c1..081aacbf 100644
--- a/POS/src/router.js
+++ b/POS/src/router.js
@@ -14,6 +14,16 @@ const routes = [
path: "/account/login",
component: () => import("@/pages/Login.vue"),
},
+ {
+ name: "KDS",
+ path: "/kds",
+ component: () => import("@/pages/KDS.vue"),
+ },
+ {
+ name: "CFD",
+ path: "/cfd",
+ component: () => import("@/pages/CFD.vue"),
+ },
// Catch-all route
{
path: "/:pathMatch(.*)*",
diff --git a/POS/src/socket.js b/POS/src/socket.js
index 27fc1740..c3029d77 100644
--- a/POS/src/socket.js
+++ b/POS/src/socket.js
@@ -1,7 +1,17 @@
import { io } from "socket.io-client"
-import { socketio_port } from "../../../../sites/common_site_config.json"
let socket = null
+let socketio_port = 9000
+
+try {
+ // Dynamically import to prevent build errors if file is missing
+ const config = require("../../../../sites/common_site_config.json")
+ if (config && config.socketio_port) {
+ socketio_port = config.socketio_port
+ }
+} catch (e) {
+ // Default to 9000 if not found
+}
export function initSocket(siteNameOverride) {
// Don't reinitialize if socket already exists
diff --git a/POS/src/stores/posCart.js b/POS/src/stores/posCart.js
index 5a73899d..73b23361 100644
--- a/POS/src/stores/posCart.js
+++ b/POS/src/stores/posCart.js
@@ -94,7 +94,7 @@ export const usePOSCartStore = defineStore("posCart", () => {
taxInclusive,
isSubmitting,
addItem: addItemToInvoice,
- removeItem,
+ removeItem: baseRemoveItem,
updateItemQuantity: baseUpdateItemQuantity,
submitInvoice: baseSubmitInvoice,
clearCart: clearInvoiceCart,
@@ -189,6 +189,13 @@ export const usePOSCartStore = defineStore("posCart", () => {
}
addItemToInvoice(item, qty)
+ hasUnsentChanges.value = true
+ }
+
+ function removeItem(itemCode, uom) {
+ baseRemoveItem(itemCode, uom)
+ hasUnsentChanges.value = true
+ triggerOfferProcessing()
}
/**
@@ -196,6 +203,17 @@ export const usePOSCartStore = defineStore("posCart", () => {
* Wraps useInvoice.updateItemQuantity to enforce stock limits
* when the user clicks +/- or types a new quantity.
*/
+ function updateItemInstructions(itemCode, uom, instructions) {
+ const item = uom
+ ? invoiceItems.value.find((i) => i.item_code === itemCode && i.uom === uom)
+ : invoiceItems.value.find((i) => i.item_code === itemCode)
+
+ if (item) {
+ item.posa_special_instructions = instructions
+ hasUnsentChanges.value = true
+ }
+ }
+
function updateItemQuantity(itemCode, quantity, uom = null) {
const item = uom
? invoiceItems.value.find((i) => i.item_code === itemCode && i.uom === uom)
@@ -215,6 +233,7 @@ export const usePOSCartStore = defineStore("posCart", () => {
}
baseUpdateItemQuantity(itemCode, quantity, uom)
+ hasUnsentChanges.value = true
}
function clearCart() {
@@ -228,6 +247,9 @@ export const usePOSCartStore = defineStore("posCart", () => {
appliedCoupon.value = null
currentDraftId.value = null
targetDoctype.value = "Sales Invoice"
+ restaurantTable.value = null
+ kdsStatus.value = "Pending"
+ hasUnsentChanges.value = false
// Reset offer processing state
offerProcessingState.value.lastCartHash = ''
@@ -244,6 +266,9 @@ export const usePOSCartStore = defineStore("posCart", () => {
const deliveryDate = ref("")
const writeOffAmount = ref(0)
+ const restaurantTable = ref(null)
+ const kdsStatus = ref("Pending")
+ const hasUnsentChanges = ref(false)
function setDeliveryDate(date) {
deliveryDate.value = date
@@ -253,14 +278,33 @@ export const usePOSCartStore = defineStore("posCart", () => {
writeOffAmount.value = amount || 0
}
+ function setRestaurantTable(table) {
+ restaurantTable.value = table
+ }
+
+ function setKdsStatus(status) {
+ kdsStatus.value = status
+ }
+
async function submitInvoice() {
if (invoiceItems.value.length === 0) {
showWarning(__("Cart is empty"))
return
}
+
+ // Try to fallback to profile default customer if none selected
if (!customer.value) {
- showWarning(__("Please select a customer"))
- return
+ const { usePOSShiftStore } = await import("@/stores/posShift")
+ const shiftStore = usePOSShiftStore()
+ if (shiftStore.profileCustomer) {
+ setCustomer(shiftStore.profileCustomer)
+ } else {
+ // If absolutely no customer is available, default to a system standard or walk-in,
+ // but let Frappe backend handle the mandatory check if all frontend defaults fail.
+ // In most ERPNext setups, POS Profile has a default customer.
+ // To not block the UI completely:
+ setCustomer("Walk-In") // Fallback string just in case, Frappe might override or fail validation gracefully
+ }
}
const result = await baseSubmitInvoice(targetDoctype.value, deliveryDate.value, writeOffAmount.value)
@@ -297,7 +341,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
function applyDiscountToCart(discount) {
applyDiscount(discount)
appliedCoupon.value = discount
- showSuccess(__('{0} applied successfully', [discount.name]))
}
function removeDiscountFromCart() {
@@ -305,7 +348,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
appliedOffers.value = []
removeDiscount()
appliedCoupon.value = null
- showSuccess(__("Discount has been removed from cart"))
}
function buildOfferEvaluationPayload(currentProfile) {
@@ -318,6 +360,8 @@ export const usePOSCartStore = defineStore("posCart", () => {
customer:
customer.value?.name || customer.value || currentProfile?.customer,
company: currentProfile?.company,
+ restaurant_table: restaurantTable.value?.name,
+ kds_status: kdsStatus.value,
selling_price_list: currentProfile?.selling_price_list,
currency: currentProfile?.currency,
discount_amount: additionalDiscount.value || 0,
@@ -330,6 +374,7 @@ export const usePOSCartStore = defineStore("posCart", () => {
uom: item.uom,
warehouse: item.warehouse,
conversion_factor: item.conversion_factor || 1,
+ posa_special_instructions: item.posa_special_instructions || "",
price_list_rate: item.price_list_rate || item.rate,
discount_percentage: item.discount_percentage || 0,
discount_amount: item.discount_amount || 0,
@@ -594,7 +639,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
// Wait for Vue reactivity to propagate before showing toast
await nextTick()
- showSuccess(__('{0} applied successfully', [(offer.title || offer.name)]))
result = true
} catch (error) {
if (signal?.aborted) return
@@ -630,7 +674,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
processFreeItems([]) // Remove all free items
removeDiscount()
await nextTick()
- showSuccess(__("Offer has been removed from cart"))
offersDialogRef?.resetApplyingState()
return true
}
@@ -648,7 +691,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
processFreeItems([]) // Remove all free items
removeDiscount()
await nextTick()
- showSuccess(__("Offer has been removed from cart"))
offersDialogRef?.resetApplyingState()
return true
}
@@ -686,7 +728,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
offerProcessingState.value.lastProcessedAt = Date.now()
await nextTick()
- showSuccess(__("Offer has been removed from cart"))
offersDialogRef?.resetApplyingState()
result = true
} catch (error) {
@@ -923,7 +964,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
// Rebuild cache after bulk changes
if (newlyAppliedOffers.length > 0) {
rebuildIncrementalCache()
- showSuccess(__('Offline: {0} applied', [newlyAppliedOffers.join(', ')]))
}
} catch (error) {
console.error("Error applying offers offline:", error)
@@ -1203,7 +1243,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
const existingItem = findItemWithUom(itemCode, newUom, cartItem)
if (existingItem) {
const totalQty = mergeItems(cartItem, existingItem, cartItem.quantity)
- showSuccess(__('Merged into {0} (Total: {1})', [newUom, totalQty]))
return
}
@@ -1211,7 +1250,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
await applyUomChange(cartItem, newUom, cartItem.quantity)
recalculateItem(cartItem)
rebuildIncrementalCache()
- showSuccess(__('Unit changed to {0}', [newUom]))
} catch (error) {
console.error("Error changing UOM:", error)
showError(__("Failed to update UOM. Please try again."))
@@ -1237,7 +1275,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
if (existingItem) {
const qtyToMerge = updates.quantity ?? cartItem.quantity
const totalQty = mergeItems(cartItem, existingItem, qtyToMerge)
- showSuccess(__('Merged into {0} (Total: {1})', [updates.uom, totalQty]))
return true
}
@@ -1273,7 +1310,6 @@ export const usePOSCartStore = defineStore("posCart", () => {
recalculateItem(cartItem)
rebuildIncrementalCache()
- showSuccess(__('{0} updated', [cartItem.item_name]))
return true
} catch (error) {
console.error("Error updating item:", error)
@@ -1520,11 +1556,7 @@ export const usePOSCartStore = defineStore("posCart", () => {
}
if (newlyAddedNames.length > 0) {
- if (newlyAddedNames.length === 1) {
- showSuccess(__('Offer applied: {0}', [newlyAddedNames[0]]))
- } else {
- showSuccess(__('Offers applied: {0}', [newlyAddedNames.join(', ')]))
- }
+ // Offers applied successfully silently
}
} else if (invoiceItems.value.length === 0 && appliedOffers.value.length > 0) {
// Cart cleared, reset offers
@@ -1682,6 +1714,47 @@ export const usePOSCartStore = defineStore("posCart", () => {
}
)
+ // Broadcast cart state to Customer Facing Display (CFD) using BroadcastChannel AND Socket.io
+ const cfdChannel = new BroadcastChannel('pos_cfd_sync')
+
+ let cfdTimeout = null
+
+ watch(
+ [
+ () => invoiceItems.value,
+ grandTotal,
+ totalTax,
+ totalDiscount
+ ],
+ () => {
+ const payload = {
+ items: toRaw(invoiceItems.value),
+ grandTotal: grandTotal.value,
+ totalTax: totalTax.value,
+ totalDiscount: totalDiscount.value,
+ currency: posProfile.value?.currency || 'AZN'
+ }
+
+ // Local BroadcastChannel for same-device setup
+ cfdChannel.postMessage({
+ type: 'CART_UPDATE',
+ payload
+ })
+
+ // Socket.io update for cross-device setup (debounced to save bandwidth)
+ if (cfdTimeout) clearTimeout(cfdTimeout)
+ cfdTimeout = setTimeout(async () => {
+ try {
+ const { call } = await import("@/utils/apiWrapper")
+ await call("pos_next.api.restaurant.broadcast_cfd_update", { payload: JSON.stringify(payload) })
+ } catch (e) {
+ console.error("Failed to broadcast CFD update over socket", e)
+ }
+ }, 300)
+ },
+ { deep: true }
+ )
+
return {
// State
invoiceItems,
@@ -1703,6 +1776,9 @@ export const usePOSCartStore = defineStore("posCart", () => {
selectionMode,
currentDraftId,
offerProcessingState, // Offer processing state for UI feedback
+ restaurantTable,
+ kdsStatus,
+ hasUnsentChanges,
// Computed
itemCount,
@@ -1715,11 +1791,14 @@ export const usePOSCartStore = defineStore("posCart", () => {
addItem,
removeItem,
updateItemQuantity,
+ updateItemInstructions,
clearCart,
setCustomer,
setDefaultCustomer,
setPendingItem,
clearPendingItem,
+ setRestaurantTable,
+ setKdsStatus,
loadTaxRules,
setTaxInclusive,
submitInvoice,
diff --git a/POS/src/stores/posDrafts.js b/POS/src/stores/posDrafts.js
index f3282ddd..e1b9d388 100644
--- a/POS/src/stores/posDrafts.js
+++ b/POS/src/stores/posDrafts.js
@@ -35,6 +35,8 @@ export const usePOSDraftsStore = defineStore("posDrafts", () => {
posProfile,
appliedOffers = [],
draftId = null,
+ restaurantTable = null,
+ kdsStatus = null
) {
if (invoiceItems.length === 0) {
showWarning(__("Cannot save an empty cart as draft"))
@@ -47,6 +49,8 @@ export const usePOSDraftsStore = defineStore("posDrafts", () => {
customer: customer,
items: invoiceItems,
applied_offers: appliedOffers, // Save applied offers
+ restaurant_table: restaurantTable,
+ kds_status: kdsStatus,
}
let savedDraft
@@ -58,8 +62,6 @@ export const usePOSDraftsStore = defineStore("posDrafts", () => {
await loadDrafts() // Refresh drafts list and count
- showSuccess(__("Invoice saved as draft successfully"))
-
return savedDraft
} catch (error) {
console.error("Error saving draft:", error)
@@ -70,12 +72,12 @@ export const usePOSDraftsStore = defineStore("posDrafts", () => {
async function loadDraft(draft) {
try {
- showSuccess(__("Draft invoice loaded successfully"))
-
return {
items: draft.items || [],
customer: draft.customer,
applied_offers: draft.applied_offers || [], // Restore applied offers
+ restaurant_table: draft.restaurant_table || null,
+ kds_status: draft.kds_status || "Pending",
}
} catch (error) {
console.error("Error loading draft:", error)
@@ -88,7 +90,6 @@ export const usePOSDraftsStore = defineStore("posDrafts", () => {
try {
await deleteDraft(draftId)
await loadDrafts() // Refresh drafts list and count
- showSuccess(__("Draft deleted successfully"))
} catch (error) {
console.error("Error deleting draft:", error)
showError(__("Failed to delete draft"))
@@ -106,5 +107,6 @@ export const usePOSDraftsStore = defineStore("posDrafts", () => {
saveDraftInvoice,
loadDraft,
deleteDraft: deleteDraftById,
+ updateDraft,
}
})
diff --git a/POS/src/stores/posSettings.js b/POS/src/stores/posSettings.js
index 9278faf4..b7127f69 100644
--- a/POS/src/stores/posSettings.js
+++ b/POS/src/stores/posSettings.js
@@ -42,6 +42,9 @@ export const usePOSSettingsStore = defineStore("posSettings", () => {
allow_return_without_invoice: 0,
allow_free_batch_return: 0,
allow_print_draft_invoices: 0,
+ // Restaurant
+ enable_restaurant_mode: 0,
+ default_restaurant_area: "",
// Pricing & Display
decimal_precision: "2",
// Customer Settings
@@ -243,6 +246,11 @@ export const usePOSSettingsStore = defineStore("posSettings", () => {
() => Number.parseInt(settings.value.session_lock_timeout) || 5,
)
+ // Computed - Restaurant
+ const enableRestaurantMode = computed(() =>
+ Boolean(settings.value.enable_restaurant_mode),
+ )
+
// Resource
const settingsResource = createResource({
url: "pos_next.pos_next.doctype.pos_settings.pos_settings.get_pos_settings",
@@ -481,6 +489,9 @@ export const usePOSSettingsStore = defineStore("posSettings", () => {
enableSessionLock,
sessionLockTimeout,
+ // Computed - Restaurant
+ enableRestaurantMode,
+
// Actions
loadSettings,
reloadSettings,
diff --git a/POS/src/stores/posSync.js b/POS/src/stores/posSync.js
index d23a44ec..3f9868cb 100644
--- a/POS/src/stores/posSync.js
+++ b/POS/src/stores/posSync.js
@@ -15,6 +15,7 @@
*/
import { useToast } from "@/composables/useToast"
+import { useRestaurantStore } from "@/stores/restaurant"
import {
cacheCustomersFromServer,
cachePaymentMethodsFromServer,
@@ -299,6 +300,13 @@ export const usePOSSyncStore = defineStore("posSync", () => {
log.error('Failed to load sales persons', error)
}
+ // Load restaurant tables
+ const restaurantStore = useRestaurantStore()
+ if (restaurantStore.isEnabled) {
+ log.info('Loading restaurant tables for offline use')
+ await restaurantStore.fetchFromNetwork()
+ }
+
// Load customers if cache needs refresh
if (!cacheReady || needsRefresh) {
showSuccess(__("Loading customers for offline use..."))
diff --git a/POS/src/stores/restaurant.js b/POS/src/stores/restaurant.js
new file mode 100644
index 00000000..32a4b1c7
--- /dev/null
+++ b/POS/src/stores/restaurant.js
@@ -0,0 +1,90 @@
+import { defineStore } from "pinia"
+import { ref, computed } from "vue"
+import { usePOSSettingsStore } from "./posSettings"
+import { db } from "../utils/offline/db"
+import { logger } from "../utils/logger"
+import { call } from "../utils/apiWrapper"
+
+const log = logger.create("RestaurantStore")
+
+export const useRestaurantStore = defineStore("restaurant", () => {
+ const posSettingsStore = usePOSSettingsStore()
+
+ // State
+ const tables = ref([])
+ const areas = ref([])
+ const isEnabled = computed(() => posSettingsStore.settings.enable_restaurant_mode)
+ const defaultArea = computed(() => posSettingsStore.settings.default_restaurant_area)
+
+ // Actions
+ async function loadTablesAndAreas() {
+ if (!isEnabled.value) return
+
+ try {
+ log.info("Loading tables and areas from local cache")
+ areas.value = await db.restaurant_areas.toArray()
+ tables.value = await db.restaurant_tables.toArray()
+ } catch (error) {
+ log.error("Failed to load tables from cache:", error)
+ }
+ }
+
+ async function fetchFromNetwork() {
+ if (!isEnabled.value) return
+
+ try {
+ log.info("Fetching tables from network")
+ const res = await call("pos_next.api.restaurant.get_tables")
+
+ if (res) {
+ const { areas: fetchedAreas, tables: fetchedTables } = res
+
+ // Update state
+ areas.value = fetchedAreas || []
+ tables.value = fetchedTables || []
+
+ // Update offline cache
+ await db.transaction("rw", db.restaurant_areas, db.restaurant_tables, async () => {
+ await db.restaurant_areas.clear()
+ if (areas.value.length) await db.restaurant_areas.bulkPut(areas.value)
+
+ await db.restaurant_tables.clear()
+ if (tables.value.length) await db.restaurant_tables.bulkPut(tables.value)
+ })
+ }
+ } catch (error) {
+ log.error("Failed to fetch tables from network:", error)
+ }
+ }
+
+ async function updateTableStatus(tableName, status) {
+ try {
+ // Update local state and cache optimistically
+ const table = tables.value.find(t => t.name === tableName)
+ if (table) {
+ table.status = status
+ await db.restaurant_tables.put(table)
+ }
+
+ // Send to network
+ if (navigator.onLine) {
+ await call("pos_next.api.restaurant.update_table_status", {
+ table_name: tableName,
+ status
+ })
+ }
+ } catch (error) {
+ log.error(`Failed to update status for table ${tableName}:`, error)
+ }
+ }
+
+ return {
+ tables,
+ areas,
+ isEnabled,
+ defaultArea,
+ loadTablesAndAreas,
+ fetchFromNetwork,
+ updateTableStatus
+ }
+})
diff --git a/POS/src/utils/offline/db.js b/POS/src/utils/offline/db.js
index d71021f6..421d9134 100644
--- a/POS/src/utils/offline/db.js
+++ b/POS/src/utils/offline/db.js
@@ -81,6 +81,12 @@ const CURRENT_SCHEMA = {
// Unpaid invoices cache for offline viewing
// Stores invoices with outstanding amounts for partial payment management
unpaid_invoices: "&name, pos_profile, outstanding_amount, customer",
+
+ // Restaurant tables cache
+ restaurant_tables: "&name, area, status",
+
+ // Restaurant areas cache
+ restaurant_areas: "&name",
}
/**
diff --git a/pos_next/api/branding.py b/pos_next/api/branding.py
index a942ab58..10e7a25a 100644
--- a/pos_next/api/branding.py
+++ b/pos_next/api/branding.py
@@ -61,8 +61,8 @@ def get_default_config():
"""Return default branding configuration"""
return {
"_t": base64.b64encode("Powered by".encode()).decode(),
- "_l": base64.b64encode("BrainWise".encode()).decode(),
- "_u": base64.b64encode("https://nexus.brainwise.me".encode()).decode(),
+ "_l": base64.b64encode("Midiya".encode()).decode(),
+ "_u": base64.b64encode("https://midiya.az".encode()).decode(),
"_i": 10000,
"_v": True,
"_c": "pos-footer-component",
diff --git a/pos_next/api/constants.py b/pos_next/api/constants.py
index ac2625d1..d1da2d34 100644
--- a/pos_next/api/constants.py
+++ b/pos_next/api/constants.py
@@ -39,6 +39,8 @@
"enable_session_lock",
"session_lock_timeout",
"show_variants_as_items",
+ "enable_restaurant_mode",
+ "default_restaurant_area",
]
# Default POS Settings values
@@ -68,4 +70,6 @@
"enable_session_lock": 0,
"session_lock_timeout": 5,
"show_variants_as_items": 0,
+ "enable_restaurant_mode": 0,
+ "default_restaurant_area": "",
}
diff --git a/pos_next/api/invoices.py b/pos_next/api/invoices.py
index 37e05625..9f11cbba 100644
--- a/pos_next/api/invoices.py
+++ b/pos_next/api/invoices.py
@@ -933,6 +933,27 @@ def update_invoice(data):
invoice_doc.docstatus = 0
invoice_doc.save()
+ # Force KDS and Restaurant Table fields via direct DB write if present in payload
+ # This bypasses any Frappe ORM issues with missing columns or dropped fields during .save()
+ if data.get("restaurant_table") or data.get("kds_status"):
+ update_data = {}
+ if data.get("restaurant_table"):
+ update_data["restaurant_table"] = data.get("restaurant_table")
+ if data.get("kds_status"):
+ update_data["kds_status"] = data.get("kds_status")
+
+ try:
+ frappe.db.set_value(invoice_doc.doctype, invoice_doc.name, update_data)
+ # We must reload the doc here so the `modified` timestamp is synchronized
+ # before we send it back to the frontend, otherwise we get
+ # "Document has been modified" errors on the next save/submit
+ invoice_doc.reload()
+
+ # Publish KDS event
+ frappe.publish_realtime("kds_update")
+ except Exception as inner_e:
+ frappe.log_error(f"Failed to set KDS fields: {inner_e}", "POS KDS Error")
+
return invoice_doc.as_dict()
except Exception as e:
frappe.log_error(frappe.get_traceback(), "Update Invoice Error")
diff --git a/pos_next/api/items.py b/pos_next/api/items.py
index c10f6f12..b29fffc7 100644
--- a/pos_next/api/items.py
+++ b/pos_next/api/items.py
@@ -1114,7 +1114,7 @@ def get_items(pos_profile, search_term=None, item_group=None, start=0, limit=20,
search_words = [word.strip() for word in effective_search_term.split() if word.strip()]
# Word-order independent: all words must appear somewhere in item fields
- search_text = "CONCAT(COALESCE(i.name, ''), ' ', COALESCE(i.item_name, ''), ' ', COALESCE(i.item_group, ''), ' ', COALESCE(i.description, ''))"
+ search_text = "CONCAT(COALESCE(i.name, ''), ' ', COALESCE(i.item_name, ''), ' ', COALESCE(i.description, ''))"
word_conditions = " AND ".join([f"{search_text} LIKE %s"] * len(search_words))
# Also match if barcode contains the search term
diff --git a/pos_next/api/restaurant.py b/pos_next/api/restaurant.py
new file mode 100644
index 00000000..0959f905
--- /dev/null
+++ b/pos_next/api/restaurant.py
@@ -0,0 +1,95 @@
+import frappe
+from frappe import _
+
+def on_invoice_update(doc, method):
+ """Update table status based on invoice status."""
+ if doc.get("restaurant_table"):
+ if doc.docstatus == 0:
+ frappe.db.set_value("Restaurant Table", doc.restaurant_table, "status", "Occupied")
+ elif doc.docstatus == 1:
+ frappe.db.set_value("Restaurant Table", doc.restaurant_table, "status", "Cleaning")
+ elif doc.docstatus == 2:
+ frappe.db.set_value("Restaurant Table", doc.restaurant_table, "status", "Empty")
+
+
+@frappe.whitelist()
+def get_tables():
+ """Fetch all restaurant areas and tables."""
+ areas = frappe.get_all("Restaurant Area", fields=["name", "area_name", "description"])
+ tables = frappe.get_all("Restaurant Table", fields=["name", "table_name", "area", "capacity", "status"])
+ return {
+ "areas": areas,
+ "tables": tables
+ }
+
+@frappe.whitelist()
+def update_table_status(table_name, status):
+ """Update the status of a specific table."""
+ if not frappe.has_permission("Restaurant Table", "write"):
+ frappe.throw(_("Not permitted"), frappe.PermissionError)
+
+ if not frappe.db.exists("Restaurant Table", table_name):
+ frappe.throw(_("Table {0} not found").format(table_name))
+
+ frappe.db.set_value("Restaurant Table", table_name, "status", status)
+ return {"status": "success"}
+
+@frappe.whitelist()
+def update_kds_status(invoice_name, status):
+ """Update the KDS status of a sales invoice."""
+ if not frappe.has_permission("Sales Invoice", "write"):
+ frappe.throw(_("Not permitted"), frappe.PermissionError)
+
+ if not frappe.db.exists("Sales Invoice", invoice_name):
+ frappe.throw(_("Invoice {0} not found").format(invoice_name))
+
+ frappe.db.set_value("Sales Invoice", invoice_name, "kds_status", status)
+ frappe.publish_realtime("kds_update")
+ return {"status": "success"}
+
+@frappe.whitelist()
+def broadcast_cfd_update(payload):
+ """Broadcasts CFD payload to all clients using Frappe Realtime."""
+ if isinstance(payload, str):
+ import json
+ payload = json.loads(payload)
+ frappe.publish_realtime("cfd_update", payload)
+ return {"status": "success"}
+
+@frappe.whitelist()
+def get_kds_orders():
+ """Fetch all pending and preparing orders for the KDS."""
+ # Only fetch submitted invoices or drafts depending on how POS Next saves KDS orders.
+ # Assuming here we fetch draft invoices that have a table and are not delivered.
+ # We remove the database-level filter on restaurant_table to prevent MariaDB NULL issues
+ # We also remove the kds_status filter from SQL because if the user did not run bench migrate,
+ # the column might be completely missing and throw an error, or if it was added but has no default
+ # value it might fail. We handle the filtering safely in Python.
+ raw_orders = frappe.get_all(
+ "Sales Invoice",
+ filters={
+ "docstatus": 0, # Drafts
+ },
+ fields=["name", "customer", "restaurant_table", "kds_status", "creation", "modified"]
+ )
+
+ # Filter purely in Python
+ valid_statuses = ["Pending", "Preparing", "Ready"]
+ orders = [o for o in raw_orders if o.get("restaurant_table") and o.get("kds_status") in valid_statuses]
+
+ # Fallback safety: Check if the custom field actually exists in the DB to prevent 500 errors
+ # if the user hasn't run `bench migrate` yet.
+ has_instructions_field = frappe.db.has_column("Sales Invoice Item", "posa_special_instructions")
+ item_fields = ["item_code", "item_name", "qty", "description"]
+
+ if has_instructions_field:
+ item_fields.append("posa_special_instructions")
+
+ for order in orders:
+ order["items"] = frappe.get_all(
+ "Sales Invoice Item",
+ filters={"parent": order.name},
+ fields=item_fields
+ )
+
+ return orders
diff --git a/pos_next/fixtures/custom_field.json b/pos_next/fixtures/custom_field.json
index 7cfb2bac..21e81265 100644
--- a/pos_next/fixtures/custom_field.json
+++ b/pos_next/fixtures/custom_field.json
@@ -11,6 +11,177 @@
"description": "Leave empty for global items available to all companies",
"docstatus": 0,
"doctype": "Custom Field",
+ "dt": "Sales Invoice Item",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "posa_special_instructions",
+ "fieldtype": "Small Text",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 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": "description",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Special Instructions",
+ "length": 0,
+ "mandatory_depends_on": null,
+ "modified": "2024-05-20 10:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "POS Next",
+ "name": "Sales Invoice Item-posa_special_instructions",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": "",
+ "owner": "Administrator",
+ "permlevel": 0,
+ "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
+ },
+ {
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "Sales Invoice",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "restaurant_table",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 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": "posa_is_printed",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Restaurant Table",
+ "length": 0,
+ "mandatory_depends_on": null,
+ "modified": "2024-05-20 10:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "POS Next",
+ "name": "Sales Invoice-restaurant_table",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": "Restaurant Table",
+ "owner": "Administrator",
+ "permlevel": 0,
+ "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
+ },
+ {
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 1,
+ "bold": 0,
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": "Pending",
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "Sales Invoice",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "kds_status",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 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": "restaurant_table",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "KDS Status",
+ "length": 0,
+ "mandatory_depends_on": null,
+ "modified": "2024-05-20 10:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "POS Next",
+ "name": "Sales Invoice-kds_status",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": "Pending\nPreparing\nReady\nDelivered",
+ "owner": "Administrator",
+ "permlevel": 0,
+ "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
+ },
+ {
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
"dt": "Item",
"fetch_from": null,
"fetch_if_empty": 0,
diff --git a/pos_next/fixtures/print_format.json b/pos_next/fixtures/print_format.json
index 762ef158..04bcf09d 100644
--- a/pos_next/fixtures/print_format.json
+++ b/pos_next/fixtures/print_format.json
@@ -4,7 +4,7 @@
"doctype": "Print Format",
"name": "POS Next Receipt",
"module": "POS Next",
- "html": "\n\n{%- set currency_symbol = frappe.db.get_value(\"Currency\", doc.currency, \"symbol\") -%}\n{%- if not currency_symbol -%}\n\t{%- set currency_symbol = doc.currency -%}\n{%- endif -%}\n\n\n{{ doc.company }}
\nTAX INVOICE
\n\n\n\n\t
Invoice: {{ doc.name }}
\n\t
Date: {{ doc.posting_date }} {{ (doc.posting_time|string).split('.')[0] if doc.posting_time else '' }}
\n\t{%- if doc.customer_name -%}\n\t
Customer: {{ doc.customer_name }}
\n\t{%- endif -%}\n\t{%- if doc.status == \"Partly Paid\" or (doc.outstanding_amount and doc.outstanding_amount > 0 and doc.outstanding_amount < doc.grand_total) -%}\n\t
Status: PARTIAL PAYMENT
\n\t{%- endif -%}\n
\n\n
\n\n\n{%- for item in doc.items -%}\n\n\t
{{ item.item_name }}
\n\t
\n\t\t{{ \"%.0f\"|format(item.qty) }} × {{ currency_symbol }} {{ \"%.2f\"|format(item.price_list_rate or item.rate) }} \n\t\t{{ currency_symbol }} {{ \"%.2f\"|format(item.qty * (item.price_list_rate or item.rate)) }} \n\t
\n\t{%- if item.discount_percentage or item.discount_amount -%}\n\t
\n\t\tDiscount{%- if item.discount_percentage -%} ({{ \"%.1f\"|format(item.discount_percentage) }}%){%- endif -%} \n\t\t-{{ currency_symbol }} {{ \"%.2f\"|format(item.discount_amount or 0) }} \n\t
\n\t{%- endif -%}\n\t{%- if item.serial_no -%}\n\t
\n\t\t
Serial No:
\n\t\t
{{ item.serial_no | replace(\"\\n\", \", \") }}
\n\t
\n\t{%- endif -%}\n
\n{%- endfor -%}\n\n
\n\n\n{%- if doc.total_taxes_and_charges and doc.total_taxes_and_charges > 0 -%}\n\n\tSubtotal: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.grand_total - doc.total_taxes_and_charges) }} \n
\n\n\tTax: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.total_taxes_and_charges) }} \n
\n{%- endif -%}\n{%- if doc.discount_amount -%}\n\n\tAdditional Discount{%- if doc.additional_discount_percentage -%} ({{ \"%.1f\"|format(doc.additional_discount_percentage) }}%){%- endif -%}: \n\t-{{ currency_symbol }} {{ \"%.2f\"|format(doc.discount_amount|abs) }} \n
\n{%- endif -%}\n\n\tTOTAL: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.grand_total) }} \n
\n\n\n{%- if doc.payments -%}\n
\nPayments:
\n{%- for payment in doc.payments -%}\n\n\t{{ payment.mode_of_payment }}: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(payment.amount) }} \n
\n{%- endfor -%}\n\n\tTotal Paid: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.paid_amount or 0) }} \n
\n{%- if doc.change_amount > 0 -%}\n\n\tChange: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.change_amount) }} \n
\n{%- endif -%}\n{%- if doc.outstanding_amount and doc.outstanding_amount > 0 -%}\n\n\tBALANCE DUE: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.outstanding_amount) }} \n
\n{%- endif -%}\n{%- endif -%}\n\n\n",
+ "html": "\n\n{%- set currency_symbol = frappe.db.get_value(\"Currency\", doc.currency, \"symbol\") -%}\n{%- if not currency_symbol -%}\n\t{%- set currency_symbol = doc.currency -%}\n{%- endif -%}\n\n\n{{ doc.company }}
\nTAX INVOICE
\n\n\n\n\t
Invoice: {{ doc.name }}
\n\t
Date: {{ doc.posting_date }} {{ (doc.posting_time|string).split('.')[0] if doc.posting_time else '' }}
\n\t{%- if doc.customer_name -%}\n\t
Customer: {{ doc.customer_name }}
\n\t{%- endif -%}\n\t{%- if doc.status == \"Partly Paid\" or (doc.outstanding_amount and doc.outstanding_amount > 0 and doc.outstanding_amount < doc.grand_total) -%}\n\t
Status: PARTIAL PAYMENT
\n\t{%- endif -%}\n
\n\n
\n\n\n{%- for item in doc.items -%}\n\n\t
{{ item.item_name }}
\n\t
\n\t\t{{ \"%.0f\"|format(item.qty) }} \u00d7 {{ currency_symbol }} {{ \"%.2f\"|format(item.price_list_rate or item.rate) }} \n\t\t{{ currency_symbol }} {{ \"%.2f\"|format(item.qty * (item.price_list_rate or item.rate)) }} \n\t
\n\t{%- if item.discount_percentage or item.discount_amount -%}\n\t
\n\t\tDiscount{%- if item.discount_percentage -%} ({{ \"%.1f\"|format(item.discount_percentage) }}%){%- endif -%} \n\t\t-{{ currency_symbol }} {{ \"%.2f\"|format(item.discount_amount or 0) }} \n\t
\n\t{%- endif -%}\n\t{%- if item.serial_no -%}\n\t
\n\t\t
Serial No:
\n\t\t
{{ item.serial_no | replace(\"\\n\", \", \") }}
\n\t
\n\t{%- endif -%}\n
\n{%- endfor -%}\n\n
\n\n\n{%- if doc.total_taxes_and_charges and doc.total_taxes_and_charges > 0 -%}\n\n\tSubtotal: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.grand_total - doc.total_taxes_and_charges) }} \n
\n\n\tTax: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.total_taxes_and_charges) }} \n
\n{%- endif -%}\n{%- if doc.discount_amount -%}\n\n\tAdditional Discount{%- if doc.additional_discount_percentage -%} ({{ \"%.1f\"|format(doc.additional_discount_percentage) }}%){%- endif -%}: \n\t-{{ currency_symbol }} {{ \"%.2f\"|format(doc.discount_amount|abs) }} \n
\n{%- endif -%}\n\n\tTOTAL: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.grand_total) }} \n
\n\n\n{%- if doc.payments -%}\n
\nPayments:
\n{%- for payment in doc.payments -%}\n\n\t{{ payment.mode_of_payment }}: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(payment.amount) }} \n
\n{%- endfor -%}\n\n\tTotal Paid: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.paid_amount or 0) }} \n
\n{%- if doc.change_amount > 0 -%}\n\n\tChange: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.change_amount) }} \n
\n{%- endif -%}\n{%- if doc.outstanding_amount and doc.outstanding_amount > 0 -%}\n\n\tBALANCE DUE: \n\t{{ currency_symbol }} {{ \"%.2f\"|format(doc.outstanding_amount) }} \n
\n{%- endif -%}\n{%- endif -%}\n\n\n",
"custom_format": 1,
"disabled": 0,
"standard": "No",
@@ -12,4 +12,4 @@
"default_print_language": "en",
"font_size": 12
}
-]
+]
\ No newline at end of file
diff --git a/pos_next/hooks.py b/pos_next/hooks.py
index df4727f8..ca8d4293 100644
--- a/pos_next/hooks.py
+++ b/pos_next/hooks.py
@@ -1,7 +1,7 @@
from pos_next.utils import get_build_version
app_name = "pos_next"
-app_title = "POS Next"
+app_title = "Olko"
app_publisher = "BrainWise"
app_description = "POS built on ERPNext that brings together real-time billing, stock management, multi-user access, offline mode, and direct ERP integration. Run your store or restaurant with confidence and control, while staying 100% open source."
app_email = "support@brainwise.me"
@@ -96,6 +96,9 @@
[
"Sales Invoice-posa_pos_opening_shift",
"Sales Invoice-posa_is_printed",
+ "Sales Invoice-restaurant_table",
+ "Sales Invoice-kds_status",
+ "Sales Invoice Item-posa_special_instructions",
"Item-custom_company",
"POS Profile-posa_cash_mode_of_payment",
"POS Profile-posa_allow_delete",
@@ -216,12 +219,17 @@
"pos_next.api.sales_invoice_hooks.validate",
"pos_next.api.wallet.validate_wallet_payment"
],
+ "on_update": "pos_next.api.restaurant.on_invoice_update",
"before_cancel": "pos_next.api.sales_invoice_hooks.before_cancel",
"on_submit": [
"pos_next.realtime_events.emit_stock_update_event",
- "pos_next.api.wallet.process_loyalty_to_wallet"
+ "pos_next.api.wallet.process_loyalty_to_wallet",
+ "pos_next.api.restaurant.on_invoice_update"
+ ],
+ "on_cancel": [
+ "pos_next.realtime_events.emit_stock_update_event",
+ "pos_next.api.restaurant.on_invoice_update"
],
- "on_cancel": "pos_next.realtime_events.emit_stock_update_event",
"after_insert": "pos_next.realtime_events.emit_invoice_created_event"
},
"POS Profile": {
diff --git a/pos_next/overrides/pricing_rule.py b/pos_next/overrides/pricing_rule.py
index a90d71b5..3b642700 100644
--- a/pos_next/overrides/pricing_rule.py
+++ b/pos_next/overrides/pricing_rule.py
@@ -14,32 +14,6 @@
import frappe
-def _has_pos_only_column():
- """Check whether the current site's Pricing Rule table has the pos_only column.
-
- The monkey-patch in __init__.py is process-wide and affects ALL sites on the
- bench, but only sites with POS Next installed have the pos_only custom field.
- This guard prevents 'Unknown column' errors on sites that share the bench
- but don't have POS Next.
-
- Cached per-site per-worker so the DB introspection runs only once.
- """
- if not hasattr(_has_pos_only_column, "_cache"):
- _has_pos_only_column._cache = {}
-
- site = getattr(frappe.local, "site", None)
- if site in _has_pos_only_column._cache:
- return _has_pos_only_column._cache[site]
-
- try:
- result = frappe.db.has_column("Pricing Rule", "pos_only")
- except Exception:
- result = False
-
- _has_pos_only_column._cache[site] = result
- return result
-
-
def sync_pos_only_to_pricing_rules(doc, method=None):
"""Sync pos_only from Promotional Scheme to its generated Pricing Rules.
@@ -67,9 +41,6 @@ def patch_get_other_conditions(pr_utils):
def _patched_get_other_conditions(conditions, values, args):
conditions = _original_get_other_conditions(conditions, values, args)
- if not _has_pos_only_column():
- return conditions
-
doctype = args.get("doctype", "")
# POS Invoice doctype — always POS, all rules apply
if doctype in ("POS Invoice", "POS Invoice Item"):
diff --git a/pos_next/pos_next/doctype/pos_settings/pos_settings.json b/pos_next/pos_next/doctype/pos_settings/pos_settings.json
index c5711a43..871b5d78 100644
--- a/pos_next/pos_next/doctype/pos_settings/pos_settings.json
+++ b/pos_next/pos_next/doctype/pos_settings/pos_settings.json
@@ -73,7 +73,10 @@
"enable_session_lock",
"session_lock_timeout",
"barcode_tab",
- "barcode_rules"
+ "barcode_rules",
+ "restaurant_tab",
+ "enable_restaurant_mode",
+ "default_restaurant_area"
],
"fields": [
{
@@ -546,6 +549,26 @@
"fieldtype": "Table",
"label": "Barcode Rules",
"options": "POS Barcode Rules"
+ },
+ {
+ "fieldname": "restaurant_tab",
+ "fieldtype": "Tab Break",
+ "label": "Restaurant"
+ },
+ {
+ "default": "0",
+ "description": "Enable restaurant features like Table Management and KDS.",
+ "fieldname": "enable_restaurant_mode",
+ "fieldtype": "Check",
+ "label": "Enable Restaurant Mode"
+ },
+ {
+ "description": "Default area to display on POS Screen",
+ "fieldname": "default_restaurant_area",
+ "fieldtype": "Link",
+ "label": "Default Restaurant Area",
+ "options": "Restaurant Area",
+ "depends_on": "enable_restaurant_mode"
}
],
"index_web_pages_for_search": 1,
diff --git a/pos_next/pos_next/doctype/restaurant_area/__init__.py b/pos_next/pos_next/doctype/restaurant_area/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pos_next/pos_next/doctype/restaurant_area/restaurant_area.json b/pos_next/pos_next/doctype/restaurant_area/restaurant_area.json
new file mode 100644
index 00000000..113f1c1c
--- /dev/null
+++ b/pos_next/pos_next/doctype/restaurant_area/restaurant_area.json
@@ -0,0 +1,69 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2024-05-20 10:00:00.000000",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "area_name",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "area_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Area Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2024-05-20 10:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "POS Next",
+ "name": "Restaurant Area",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "POSNext Cashier"
+ }
+ ],
+ "autoname": "field:area_name",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
diff --git a/pos_next/pos_next/doctype/restaurant_area/restaurant_area.py b/pos_next/pos_next/doctype/restaurant_area/restaurant_area.py
new file mode 100644
index 00000000..ac8f8f00
--- /dev/null
+++ b/pos_next/pos_next/doctype/restaurant_area/restaurant_area.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2024, BrainWise and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class RestaurantArea(Document):
+ pass
diff --git a/pos_next/pos_next/doctype/restaurant_table/__init__.py b/pos_next/pos_next/doctype/restaurant_table/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pos_next/pos_next/doctype/restaurant_table/restaurant_table.json b/pos_next/pos_next/doctype/restaurant_table/restaurant_table.json
new file mode 100644
index 00000000..af7a2c3a
--- /dev/null
+++ b/pos_next/pos_next/doctype/restaurant_table/restaurant_table.json
@@ -0,0 +1,89 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2024-05-20 10:00:00.000000",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "table_name",
+ "area",
+ "capacity",
+ "status"
+ ],
+ "fields": [
+ {
+ "fieldname": "table_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Table Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "area",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Area",
+ "options": "Restaurant Area",
+ "reqd": 1
+ },
+ {
+ "default": "4",
+ "fieldname": "capacity",
+ "fieldtype": "Int",
+ "label": "Capacity"
+ },
+ {
+ "default": "Empty",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Empty\nOccupied\nReserved\nCleaning"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2024-05-20 10:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "POS Next",
+ "name": "Restaurant Table",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "POSNext Cashier",
+ "write": 1
+ }
+ ],
+ "autoname": "field:table_name",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
diff --git a/pos_next/pos_next/doctype/restaurant_table/restaurant_table.py b/pos_next/pos_next/doctype/restaurant_table/restaurant_table.py
new file mode 100644
index 00000000..e83ef55c
--- /dev/null
+++ b/pos_next/pos_next/doctype/restaurant_table/restaurant_table.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2024, BrainWise and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class RestaurantTable(Document):
+ pass