diff --git a/README.md b/README.md index 7a8136797..e2bc1ce01 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ before making a package-level licence statement. [![Latest release](https://img.shields.io/github/v/release/Posnic/POS?include_prereleases&label=latest&color=blue)](https://github.com/Posnic/POS/releases/latest) [![Tests](https://img.shields.io/badge/tests-9%2C000%2B%20passing-brightgreen)](docs/DEVELOPMENT.md#running-the-tests) [![Coverage](https://img.shields.io/badge/coverage-69%25%20statements-yellow)](docs/DEVELOPMENT.md#running-the-tests) -[![API](https://img.shields.io/badge/REST%20API-897%20endpoints-blue)](docs/API.md) +[![API](https://img.shields.io/badge/REST%20API-900%20endpoints-blue)](docs/API.md) [![Source licence](https://img.shields.io/badge/source%20licence-AGPL--3.0-blue)](LICENSE) [![Package notices](https://img.shields.io/badge/package%20notices-component%20licences-informational)](THIRD-PARTY-NOTICES.md) [![Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)](https://github.com/Posnic/POS/releases/latest) @@ -235,7 +235,7 @@ the free edition worse. This is written down in [GOVERNANCE.md](docs/GOVERNANCE. | [Contributor quickstart](docs/CONTRIBUTOR_QUICKSTART.md) | Local setup, test commands, issue map, PR flow and safety rules | | [Developer guide](docs/DEVELOPMENT.md) | Setup, tests, conventions, good first issues | | [Architecture](docs/ARCHITECTURE.md) | How it fits together, and the parts that bite | -| [REST API](docs/API.md) | 897 endpoints, generated from the routes | +| [REST API](docs/API.md) | 900 endpoints, generated from the routes | | [Hardware](docs/HARDWARE_MATRIX.md) | Printers, scanners, drawers, scales — and how far each claim is checked | | [India e-invoicing](docs/INDIA_EINVOICING_DESIGN.md) | Research, readiness inventory and design for GST e-invoicing as an optional feature; no live IRP submission is built | | [India GST return gaps](docs/INDIA_GST_RETURNS_GAPS.md) | What the GST reports compute today, measured against GSTR-1, 3B and 9 | diff --git a/api/src/helpers/bill-payload.js b/api/src/helpers/bill-payload.js index 9da6183df..19c105ffa 100644 --- a/api/src/helpers/bill-payload.js +++ b/api/src/helpers/bill-payload.js @@ -1,5 +1,6 @@ 'use strict'; +const Money = require('../utils/currency'); const { isWalkIn } = require('../utils/walk-in'); const { fieldAvailable } = require('./receipt-design'); @@ -633,6 +634,14 @@ function buildBillPayload(sale = {}, branch = {}, options = {}) { subTotal, taxes: taxRows(sale, branch, allocation), + charges: allocation + ? Object.entries(allocation.components) + .filter(([key]) => key.startsWith('charge:')) + .map(([key, minor]) => ({ + name: key.slice(7), + amount: Money.fromMinor(minor, Money.policy(branch)), + })) + : (sale.charges || []).map((c) => ({ name: c.name, amount: num(c.amount) })), discount: num(sale.discount), roundOff: num(sale.round_off || sale.sales_round_off), total: num(sale.sales_total != null ? sale.sales_total : sale.total), diff --git a/api/src/models/branch.model.js b/api/src/models/branch.model.js index de8b6a032..42feefe6a 100644 --- a/api/src/models/branch.model.js +++ b/api/src/models/branch.model.js @@ -25,8 +25,10 @@ const branchSchema = new Schema( mobile_pos: { type: Schema.Types.Mixed }, branch_upi_id: { type: String }, branch_upi_name: { type: String }, + captain_allow_unassigned: { type: Boolean, default: false }, captain_table_cleaning: { type: Boolean, default: false }, captain_payments: { type: Schema.Types.Mixed }, + captain_attendance: { type: Schema.Types.Mixed }, module_mobile_pos_enable: { type: Boolean }, module_billing_outlets_enable: { type: Boolean }, payment_settings: { type: Schema.Types.Mixed }, diff --git a/api/src/repositories/sale.repository.js b/api/src/repositories/sale.repository.js index b27adc198..cc184d4c0 100644 --- a/api/src/repositories/sale.repository.js +++ b/api/src/repositories/sale.repository.js @@ -8744,6 +8744,43 @@ class SalesRepository { }; } + // Preserve idempotent recovery, but never create a new dine-in ticket + // without a destination, including requests from older handsets. + const requestedService = String(order || dine_type || data.fulfilment || 'Dine-in') + .trim() + .toLowerCase() + .replace(/[\s_-]+/g, ''); + const requestedTable = kiosk_table_no ?? table; + const hasTable = + ['string', 'number'].includes(typeof requestedTable) && + String(requestedTable).trim() !== ''; + if ( + staffOrder && + !['takeaway', 'pickup', 'delivery'].includes(requestedService) && + !hasTable && + !venue && + branchDoc?.captain_allow_unassigned !== true + ) { + return { + status: false, + message: 'Choose a table first.', + data: { code: 'TABLE_REQUIRED' }, + }; + } + + if (staffOrder && requestedService === 'room') { + const scoped = await new (require('./settings.repository'))().resolveGroup('channels', { + branchId: branchObjectId, + licenseId: branchDoc.license, + }); + if (scoped.status !== true) + return { + status: false, + message: 'Room settings could not be loaded. Try again.', + data: null, + }; + venues = partnerVenues.normalizeVenues(scoped.data?.values?.partner_venues); + } const servicePoint = partnerVenues.resolveServicePoint( { table: String(kiosk_table_no || table || '').trim(), venue, unit }, venues @@ -8754,6 +8791,22 @@ class SalesRepository { message: 'Venue pricing is unavailable. Refresh the menu or contact the restaurant.', data: { state: 'pricing_rule_unavailable' }, }; + const staffRoom = staffOrder && requestedService === 'room'; + if ( + staffRoom && + (!servicePoint.venue?.staff_orders_enabled || + !String(unit || '').trim() || + String(unit).trim().length > 40 || + Array.from(String(unit)).some((ch) => ch.charCodeAt(0) < 32) || + servicePoint.label.length > 200) + ) + return { + status: false, + message: 'Choose an enabled venue and enter a room number (up to 40 characters).', + data: { code: 'ROOM_REQUIRED' }, + }; + // Staff room menus use house prices; web venue markup remains a separate policy. + if (staffRoom) servicePoint.venue = { ...servicePoint.venue, price_adjust_percent: 0 }; /* Where the food actually goes, as the customer confirmed it. Null for the shop's own floor, which needs no address. */ const deliverTo = partnerVenues.confirmDestination(servicePoint, destination || {}); @@ -8800,7 +8853,7 @@ class SalesRepository { * string reading in item.repository.js is for SETTINGS values, which are * a different shape from a different writer. */ - const runsTableService = branchDoc.table_options === true; + const runsTableService = branchDoc.table_options === true && !staffRoom; const openTableLimit = Number(branchDoc.table_order_limit ?? 1); const wantsTable = String(servicePoint.label || kiosk_table_no || table || '').trim(); const seating = require('../services/seating-claims'); @@ -8973,8 +9026,19 @@ class SalesRepository { offer on the food, and a shop that ends up paying somebody to collect an order has been given a rule it did not write. */ const afterCoupon = Math.max(0, round(foodTotal - couponDiscount)); + const roomCharge = staffRoom + ? await require('../services/room-charge').create( + db, + servicePoint.venue, + data.room_charge_accepted, + Math.max(0, afterCoupon - totalTax), + branchDoc, + data.room_charge_quote + ) + : null; + const roomChargeTotal = roomCharge ? roomCharge.amount + roomCharge.tax_amount : 0; const rounded = require('../services/sale-header').roundSaleTotal( - round(afterCoupon + deliveryFee), + round(afterCoupon + deliveryFee + roomChargeTotal), branchDoc.roundOff, monetary.currencyDigits ); @@ -9248,8 +9312,9 @@ class SalesRepository { A hotel room says the hotel and the room; the shop's own table says the table, exactly as it always did. */ table_number: servicePoint.label || kiosk_table_no || '', - table_id: kiosk_table_id || '', + table_id: staffRoom ? '' : kiosk_table_id || '', venue: deliverTo, + ...(roomCharge ? { charges: [roomCharge] } : {}), venue_commission: venueCommission, delivery_fee: deliveryFee, person_count: person_count || 0, @@ -9260,7 +9325,7 @@ class SalesRepository { round_off: rounded.roundOff, sales_round_off: rounded.roundOff, sales_sub_total: subtotal, - tax: totalTax, + tax: totalTax + (roomCharge?.tax_amount || 0), discount: round(itemDiscountTotal + discountAmt + couponDiscount), extra_discount: discountAmt, /* @@ -9496,7 +9561,8 @@ class SalesRepository { items: saleItems, subtotal, discount: round(itemDiscountTotal + discountAmt), - tax: totalTax, + tax: totalTax + (roomCharge?.tax_amount || 0), + ...(roomCharge ? { charges: [roomCharge] } : {}), delivery_fee: deliveryFee, total: finalTotal, /* Echoed back so the confirmation screen can say "we will bring it @@ -10583,6 +10649,11 @@ class SalesRepository { item_transfer: true, total_amount: doc.sales_total || doc.total || 0, subtotal: doc.sales_sub_total || doc.subtotal || 0, + charges: (doc.charges || []).map((c) => ({ + name: c.name, + amount: c.amount, + tax_amount: c.tax_amount, + })), tax: doc.tax || 0, discount: doc.discount || 0, pricing_preview: true, @@ -10738,13 +10809,21 @@ class SalesRepository { if (charges !== undefined && preview) throw new Error('Charges cannot be changed in an item preview.'); const savedCharges = Array.isArray(orderDoc.charges) ? orderDoc.charges : []; - const normalizedCharges = + let normalizedCharges = charges === undefined ? savedCharges : [ - ...savedCharges.filter((c) => c.source === 'outlet'), + ...savedCharges.filter( + (c) => + c.source === 'outlet' || + (c.source === 'room' && + Array.isArray(charges) && + charges.some((v) => v?.source === 'room')) + ), ...(await require('../services/sale-charges').normalizeSaleCharges( - Array.isArray(charges) ? charges.filter((c) => c?.source !== 'outlet') : charges, + Array.isArray(charges) + ? charges.filter((c) => !['outlet', 'room'].includes(c?.source)) + : charges, savedCharges.filter((c) => c.source !== 'outlet'), { branchSettings: shop || {}, @@ -11319,6 +11398,16 @@ class SalesRepository { } if (extraDiscountAmount > itemsSub) extraDiscountAmount = itemsSub; + if (!orderDoc.captain_transfer_allocation) + normalizedCharges = normalizedCharges.map((c) => + c.source === 'room' + ? require('../services/room-charge').recalculate( + c, + status === 'cancelled' ? 0 : Math.max(0, itemsSub - taxTotal - extraDiscountAmount), + shop + ) + : c + ); const chargeTotal = normalizedCharges.reduce( (sum, charge) => sum + Number(charge.amount || 0), 0 diff --git a/api/src/repositories/shift.repository.js b/api/src/repositories/shift.repository.js index 888981d58..997283965 100644 --- a/api/src/repositories/shift.repository.js +++ b/api/src/repositories/shift.repository.js @@ -49,6 +49,8 @@ class ShiftRepository { * behaviour, e.g. the 409 on double clock-in). */ async _autoCloseIfStale(collection, shift, now) { + // Mobile attendance requires an explicit clock-out or manager correction. + if (shift.source === 'captain') return null; const clockIn = shift.clock_in instanceof Date ? shift.clock_in : new Date(shift.clock_in); const ageMin = Math.round((now.getTime() - clockIn.getTime()) / 60000); if (!(ageMin > STALE_SHIFT_AFTER_MINUTES)) return null; diff --git a/api/src/routes/captain-access.routes.js b/api/src/routes/captain-access.routes.js index 015743a40..ea85d5c73 100644 --- a/api/src/routes/captain-access.routes.js +++ b/api/src/routes/captain-access.routes.js @@ -52,6 +52,10 @@ router.get( }) ); router.use(protect); +const attendance = require('../services/captain-attendance'); +router.get('/attendance', wrap(attendance.current)); +router.post('/attendance', limit, wrap(attendance.record)); +router.post('/attendance/settings', limit, wrap(attendance.settings)); // Only publish the signed-in branch's addresses. A hint never grants access // to the peer server; Captain still verifies its session/route proof there. router.post( @@ -351,7 +355,10 @@ router.get( '/table-settings', wrap(async (req) => { const c = await require('../utils/branch-access').context(req); - return { cleaningEnabled: require('../services/table-cleaning').enabled(c.branch) }; + return { + tableRequired: c.branch?.captain_allow_unassigned !== true, + cleaningEnabled: require('../services/table-cleaning').enabled(c.branch), + }; }) ); router.post( @@ -360,14 +367,22 @@ router.post( const { allowed, context, fail } = require('../utils/branch-access'); if (!allowed(req.user, 'settings')) fail('Settings permission is required.', 403); if (typeof req.body?.cleaningEnabled !== 'boolean') fail('Choose a table status.'); + if (req.body.tableRequired !== undefined && typeof req.body.tableRequired !== 'boolean') + fail('Choose a table requirement.'); const c = await context(req), cleaningEnabled = req.body.cleaningEnabled; - await req.db - .collection('branches') - .updateOne( - { _id: c.branchId, license: c.license }, - { $set: { captain_table_cleaning: cleaningEnabled, updated_date: new Date() } } - ); + await req.db.collection('branches').updateOne( + { _id: c.branchId, license: c.license }, + { + $set: { + captain_table_cleaning: cleaningEnabled, + ...(req.body.tableRequired === undefined + ? {} + : { captain_allow_unassigned: !req.body.tableRequired }), + updated_date: new Date(), + }, + } + ); if (!cleaningEnabled) await req.db.collection('tableorder').updateMany( { branch_id: c.branchId, license: c.license, service_state: 'cleaning' }, diff --git a/api/src/services/captain-access.js b/api/src/services/captain-access.js index 83737f259..6ce39d212 100644 --- a/api/src/services/captain-access.js +++ b/api/src/services/captain-access.js @@ -253,7 +253,7 @@ async function verifySession(req, user) { .replace(/^\/api/, '') .replace(/\/$/, ''); if ( - !/^\/(authorizations\/verify-pin|captain\/v1\/(session|connections|logout|profile(?:\/(?:phone|email)\/(?:start|verify))?|password|branch-details|orders\/edit\/preview|bill(?:\/reprint)?|kitchen-ready|kitchen-audio\/(?:start|cancel|voice|status|archive|recordings|playback)|tables(?:\/(?:state|close|guests(?:\/status)?|merge\/prepare|transfer\/(?:preview|status|complete)|move\/(?:prepare|complete|cancel)))?|paper-orders\/(?:options|upload|recognize|reference|photos\/[a-fA-F0-9-]{36})|payment-options|payments\/(table|record|release))|items\/(accessQr|aiAvailability|soldOut|instanceItemInsert|instantItemTax)|sales\/(qrOrder|myDay|guestBills\/(table|latest|print)|requestBillPrint|serveKitchenItems|fireKitchenItems|kitchenDeliveryStatus|handoverStaff|handoverOrder|getTablesWithActiveOrders|getOrderHistory|updateOrder|getFrequentItems|pendingOnlineOrders|getListKot|[a-fA-F0-9]{24}\/(approval|print)|waiterCalls\/[^/]+\/seen|transcribe|voiceIntent))$/.test( + !/^\/(authorizations\/verify-pin|captain\/v1\/(session|connections|logout|attendance(?:\/settings)?|profile(?:\/(?:phone|email)\/(?:start|verify))?|password|branch-details|orders\/edit\/preview|bill(?:\/reprint)?|kitchen-ready|kitchen-audio\/(?:start|cancel|voice|status|archive|recordings|playback)|tables(?:\/(?:state|close|guests(?:\/status)?|merge\/prepare|transfer\/(?:preview|status|complete)|move\/(?:prepare|complete|cancel)))?|paper-orders\/(?:options|upload|recognize|reference|photos\/[a-fA-F0-9-]{36})|payment-options|payments\/(table|record|release))|items\/(accessQr|aiAvailability|soldOut|instanceItemInsert|instantItemTax)|sales\/(qrOrder|myDay|guestBills\/(table|latest|print)|requestBillPrint|serveKitchenItems|fireKitchenItems|kitchenDeliveryStatus|handoverStaff|handoverOrder|getTablesWithActiveOrders|getOrderHistory|updateOrder|getFrequentItems|pendingOnlineOrders|getListKot|[a-fA-F0-9]{24}\/(approval|print)|waiterCalls\/[^/]+\/seen|transcribe|voiceIntent))$/.test( path ) ) diff --git a/api/src/services/captain-attendance.js b/api/src/services/captain-attendance.js new file mode 100644 index 000000000..4f76cff92 --- /dev/null +++ b/api/src/services/captain-attendance.js @@ -0,0 +1,157 @@ +'use strict'; +const { ObjectId } = require('mongodb'); +const { isIP } = require('node:net'); +const { context, allowed } = require('../utils/branch-access'); +const { fail } = require('./captain-access'); +const { isMultiTenant } = require('../db/tenant-context'); +const { getRequestDeviceId } = require('../utils/device-id.util'); + +// Attendance stays on the till. Do not trust X-Forwarded-For or a phone's +// claim that Wi-Fi is enabled; hosted servers must never attest LAN presence. +function localRequest(req) { + const ip = String(req.socket?.remoteAddress || '').replace(/^::ffff:/, ''); + return ( + process.env.POSNIC_DESKTOP === '1' && + !isMultiTenant() && + !req.headers?.forwarded && + !req.headers?.['x-forwarded-for'] && + isIP(ip) === 4 && + (/^10\./.test(ip) || /^192\.168\./.test(ip) || /^172\.(1[6-9]|2\d|3[01])\./.test(ip)) + ); +} +async function scope(req) { + if (!localRequest(req)) + fail('ATTENDANCE_WIFI_REQUIRED', 'Connect to the shop Wi-Fi to record your shift.', 409); + const c = await context(req); + if ([false, 0, '0', 'false'].includes(c.branch.module_captain_enable)) + fail('CAPTAIN_DISABLED', 'Captain is disabled.'); + if (!ObjectId.isValid(String(req.user?._id))) fail('AUTH_REQUIRED', 'Sign in first.', 401); + return { ...c, userId: new ObjectId(String(req.user._id)) }; +} +const enabled = (branch) => + branch.captain_attendance?.enabled === true && + ![false, 'false', 0, '0'].includes(branch.staff_shifts_enable); +const filter = (c) => ({ license: c.license, user_id: c.userId }); +function visible(s) { + if (!s) return null; + return { + id: String(s._id), + branchId: String(s.branch_id), + status: s.status, + clockIn: s.clock_in, + clockOut: s.clock_out, + workedMinutes: s.worked_minutes, + breakMinutes: s.break_minutes || 0, + }; +} +async function current(req) { + const c = await scope(req), + col = req.db.collection('shifts'); + const shift = await col.findOne({ ...filter(c), status: 'open' }); + return { + enabled: enabled(c.branch), + canManage: allowed(req.user, 'settings'), + branchId: String(c.branchId), + shift: visible(shift), + }; +} +async function settings(req) { + const c = await scope(req); + if (!allowed(req.user, 'settings')) fail('MANAGER_REQUIRED', 'Settings permission is required.'); + if (typeof req.body?.enabled !== 'boolean') + fail('INVALID_SETTINGS', 'Choose whether to enable attendance.', 422); + if (req.body.enabled && [false, 'false', 0, '0'].includes(c.branch.staff_shifts_enable)) + fail('SHIFTS_DISABLED', 'Enable staff shifts in POS first.', 409); + await req.db + .collection('branches') + .updateOne( + { _id: c.branchId, license: c.license }, + { $set: { captain_attendance: { enabled: req.body.enabled }, updated_date: new Date() } } + ); + return { saved: true, enabled: req.body.enabled }; +} +async function record(req) { + const c = await scope(req), + body = req.body || {}, + col = req.db.collection('shifts'); + if (!['start', 'end'].includes(body.action)) fail('INVALID_ACTION', 'Invalid shift action.', 422); + if (!/^[a-f0-9-]{36}$/i.test(body.requestId || '')) + fail('INVALID_REQUEST', 'A request ID is required.', 422); + // Unique partial index protects concurrent phones, as well as desktop clock-in. + // Existing duplicate open shifts intentionally fail closed for manager review. + await col.createIndex( + { license: 1, user_id: 1 }, + { + name: 'one_open_staff_shift', + unique: true, + partialFilterExpression: { status: 'open' }, + } + ); + let shift; + if (body.action === 'start') { + shift = await col.findOne({ ...filter(c), mobile_request_id: body.requestId }); + if (shift) return { confirmed: true, shift: visible(shift) }; + if (!enabled(c.branch)) + fail('ATTENDANCE_DISABLED', 'Ask your manager to enable mobile attendance.', 403); + shift = await col.findOne({ ...filter(c), status: 'open' }); + if (shift) + fail('SHIFT_ALREADY_OPEN', 'You already have an open shift. Refresh to see it.', 409); + const now = new Date(); + shift = { + _id: new ObjectId(), + ...filter(c), + branch_id: c.branchId, + user_name: req.user.username || req.user.name || req.user.email || '', + status: 'open', + clock_in: now, + clock_out: null, + worked_minutes: 0, + break_minutes: 0, + created_date: now, + updated_date: now, + device_id: getRequestDeviceId(req), + mobile_request_id: body.requestId, + source: 'captain', + attendance_events: [{ action: 'start', at: now, request_id: body.requestId }], + }; + try { + await col.insertOne(shift); + } catch (error) { + if (error.code !== 11000) throw error; + const replay = await col.findOne({ ...filter(c), mobile_request_id: body.requestId }); + if (replay) return { confirmed: true, shift: visible(replay) }; + fail('SHIFT_ALREADY_OPEN', 'You already have an open shift. Refresh to see it.', 409); + } + } else { + if (!ObjectId.isValid(String(body.shiftId))) + fail('INVALID_SHIFT', 'Refresh your shift first.', 422); + const query = { ...filter(c), _id: new ObjectId(String(body.shiftId)), branch_id: c.branchId }; + shift = await col.findOne(query); + if (!shift) fail('SHIFT_NOT_FOUND', 'This shift belongs to another shop or staff member.', 404); + // End remains available after a manager disables new mobile clock-ins. + if (shift.status !== 'closed') { + const now = new Date(); + const worked = Math.max( + 0, + Math.round((now - new Date(shift.clock_in)) / 60000) - (Number(shift.break_minutes) || 0) + ); + await col.updateOne( + { ...query, status: 'open' }, + { + $set: { status: 'closed', clock_out: now, worked_minutes: worked, updated_date: now }, + $push: { + attendance_events: { + action: 'end', + at: now, + request_id: body.requestId, + device_id: getRequestDeviceId(req), + }, + }, + } + ); + shift = await col.findOne(query); + } + } + return { confirmed: true, shift: visible(shift) }; +} +module.exports = { current, settings, record, localRequest, enabled }; diff --git a/api/src/services/captain-bill.js b/api/src/services/captain-bill.js index 98268e314..8ca62d3b8 100644 --- a/api/src/services/captain-bill.js +++ b/api/src/services/captain-bill.js @@ -18,7 +18,7 @@ async function read(req) { if ( typeof table !== 'string' || !table.trim() || - table.length > 40 || + table.length > 200 || Array.from(table).some((ch) => ch.charCodeAt(0) < 32) ) fail('Choose a table.'); diff --git a/api/src/services/captain-payments.js b/api/src/services/captain-payments.js index a26542dcd..37fee3a8a 100644 --- a/api/src/services/captain-payments.js +++ b/api/src/services/captain-payments.js @@ -59,7 +59,7 @@ function tableOf(req) { if ( !table || table.startsWith('takeaway:') || - table.length > 40 || + table.length > 200 || Array.from(table).some((c) => c.charCodeAt(0) < 32) ) fail('Choose a table.'); @@ -211,7 +211,7 @@ function paymentReceipt(plan, c, payment) { receipt_settled: true, // Discounts, taxes and adjustments are already allocated in this bill. sale_extra_discount: 0, - charges: [], + charges: payload.charges || [], tax: payload.taxes.reduce((sum, row) => sum + row.amount, 0), items: [], payment_mode: payment.method, diff --git a/api/src/services/captain-tables.js b/api/src/services/captain-tables.js index 557a269e0..090946663 100644 --- a/api/src/services/captain-tables.js +++ b/api/src/services/captain-tables.js @@ -87,9 +87,29 @@ async function list(req) { 'channels', { branchId: c.branchId, licenseId: c.license } ); + const roomVenues = require('../utils/partner-venues') + .normalizeVenues(feedback.status === true ? feedback.data?.values?.partner_venues : []) + .filter((v) => v.enabled && v.staff_orders_enabled); + if (roomVenues.some((v) => v.room_charge.enabled && v.room_charge.taxed)) { + const tax = ObjectId.isValid(String(c.branch?.default_tax || '')) + ? await req.db.collection('grouptax').findOne({ + _id: new ObjectId(String(c.branch.default_tax)), + branch_id: c.branchId, + license: c.license, + }) + : null; + for (const venue of roomVenues) + if (venue.room_charge.taxed) { + venue.room_charge.tax_rate = tax ? Number(tax.rate) : null; + venue.room_charge.tax_available = + !!tax && Number.isFinite(Number(tax.rate)) && Number(tax.rate) >= 0; + } + } return { orderFeedback: feedback.status === true && feedback.data?.values?.restaurant_playful_feedback === true, + tableRequired: c.branch?.captain_allow_unassigned !== true, + roomVenues, cleaningEnabled: require('./table-cleaning').enabled(c.branch), canManage: allowed(req.user, 'settings'), canMerge: allowed(req.user, 'sales', 'merge'), diff --git a/api/src/services/captain-transfer-projection.js b/api/src/services/captain-transfer-projection.js index 8ef0e0d55..0b46d6a08 100644 --- a/api/src/services/captain-transfer-projection.js +++ b/api/src/services/captain-transfer-projection.js @@ -39,6 +39,15 @@ function applyMoney(sale, view, branch, side) { round_off: amount(components.adjustment), sales_total: amount(side.totalMinor), }); + if (Array.isArray(sale.charges)) + view.charges = Object.entries(components) + .filter(([key]) => key.startsWith('charge:')) + .map(([key, minor]) => ({ + name: key.slice(7), + amount: amount(minor), + tax_amount: 0, + source: 'allocated', + })); view.items_subtotal = view.sales_sub_total; view.items_total = amount(side.totalMinor - (components.adjustment || 0)); if ('subtotal' in sale) view.subtotal = view.sales_sub_total; diff --git a/api/src/services/guest-bill.service.js b/api/src/services/guest-bill.service.js index 409e2bb70..d72071eef 100644 --- a/api/src/services/guest-bill.service.js +++ b/api/src/services/guest-bill.service.js @@ -46,6 +46,7 @@ function snapshotFrom(sales, branch, table, { allowZero = false } = {}) { }); for (const key of Object.keys(allocation.components)) if (key.startsWith('tax:')) labels[key] = key.slice(4); + else if (key.startsWith('charge:')) labels[key] = key.slice(7); totalMinor += allocation.totalMinor; continue; } @@ -74,6 +75,13 @@ function snapshotFrom(sales, branch, table, { allowZero = false } = {}) { labels[key] = tax.label; components.push({ key, amount: minor(tax.amount), weights: taxBasis }); } + for (const charge of payload.charges || []) { + const key = 'charge:' + charge.name; + labels[key] = charge.name; + const existing = components.find((c) => c.key === key); + if (existing) existing.amount += minor(charge.amount); + else components.push({ key, amount: minor(charge.amount), weights }); + } const total = minor(payload.total); if (total < 0) throw problem('Refunds cannot be split as unpaid guest bills.', 422); components.push({ @@ -156,6 +164,9 @@ function billForGuest(snapshot, guest, branch, sale, batchId, options = {}) { })), subTotal: value('base'), discount: -value('discount'), + charges: Object.entries(guest.components) + .filter(([key]) => key.startsWith('charge:')) + .map(([key, n]) => ({ name: snapshot.labels[key] || key.slice(7), amount: n / factor })), roundOff: value('adjustment'), total: guest.totalMinor / factor, taxes: Object.entries(guest.components) @@ -197,7 +208,7 @@ function createService(deps = {}) { !/^[a-f\d]{24}$/i.test(branchId) || !table || (!saleId && table.startsWith('takeaway:')) || - table.length > 40 || + table.length > 200 || Array.from(table).some((character) => character.charCodeAt(0) < 32) ) throw problem('Choose a branch and table.'); diff --git a/api/src/services/room-charge.js b/api/src/services/room-charge.js new file mode 100644 index 000000000..23da78fc1 --- /dev/null +++ b/api/src/services/room-charge.js @@ -0,0 +1,48 @@ +'use strict'; +const { ObjectId } = require('mongodb'); +const { BadRequestError } = require('../utils/appError'); +const Money = require('../utils/currency'); +function recalculate(charge, basis, branch) { + const policy = Money.policy(branch || {}); + const round = (n) => Money.fromMinor(Money.toMinor(n, policy), policy); + const amount = round((Math.max(0, Number(basis) || 0) * charge.percent) / 100); + return { ...charge, amount, tax_amount: round((amount * (charge.tax_rate || 0)) / 100) }; +} +async function create(db, venue, accepted, basis, branch, quote) { + const rule = venue?.room_charge; + if (!rule?.enabled || !(rule.percent > 0) || accepted !== true) return null; + let tax; + if (rule.taxed) { + if (!ObjectId.isValid(String(branch.default_tax || ''))) + throw new BadRequestError('Configure a default tax before taxing room charges.'); + tax = await db.collection('grouptax').findOne({ + _id: new ObjectId(String(branch.default_tax)), + branch_id: branch._id, + license: branch.license, + }); + if (!tax || !Number.isFinite(Number(tax.rate)) || Number(tax.rate) < 0) + throw new BadRequestError('The room charge tax is unavailable. Review the tax settings.'); + } + if ( + !quote || + Number(quote.percent) !== rule.percent || + quote.taxed !== rule.taxed || + Number(quote.tax_rate || 0) !== Number(tax?.rate || 0) + ) + throw new BadRequestError('Settings changed. Refresh and try again.'); + return recalculate( + { + source: 'room', + name: rule.name, + percent: rule.percent, + taxed: rule.taxed, + tax_name: tax?.name || '', + tax_rate: Number(tax?.rate || 0), + venue_code: venue.code, + accepted: true, + }, + basis, + branch + ); +} +module.exports = { create, recalculate }; diff --git a/api/src/services/sale.service.js b/api/src/services/sale.service.js index 8fc17a29f..49844c11b 100644 --- a/api/src/services/sale.service.js +++ b/api/src/services/sale.service.js @@ -2236,6 +2236,7 @@ const getTablesWithActiveOrders = async (branchId) => { let hasTakeaway = false; let takeaway = null; const takeawayOrders = []; + const unassignedOrders = []; const remember = (key, res) => { const was = detail.get(key); @@ -2274,6 +2275,15 @@ const getTablesWithActiveOrders = async (branchId) => { } else if (tNum !== '') { tables.push(tNum); remember(tNum, res); + } else { + for (const sale of res.sales || []) + unassignedOrders.push({ + sale_id: String(sale.id), + number: String(sale.number || sale.id), + since: sale.since ? new Date(sale.since).toISOString() : null, + amount: Number(sale.amount) || 0, + payment_status: sale.payment_status, + }); } }); takeaway = detail.get('__takeaway__') || null; @@ -2309,6 +2319,7 @@ const getTablesWithActiveOrders = async (branchId) => { data: { tables: uniqueTables, has_takeaway: hasTakeaway, + unassigned_orders: unassignedOrders, takeaway_orders: takeawayOrders.sort( (a, b) => String(a.since || '').localeCompare(String(b.since || '')) || diff --git a/api/src/sync/collections.json b/api/src/sync/collections.json index dffefadbc..ffe3e5381 100644 --- a/api/src/sync/collections.json +++ b/api/src/sync/collections.json @@ -131,7 +131,8 @@ "library_releases": "Private package grants, artifacts and single-use tokens belong to the registry authority, not shop-data replication.", "registry_accounts": "Registry identities, request budgets and sessions remain with their issuing registry; they must not be inherited by POS tills.", "registry_rate_limits": "Registry identities, request budgets and sessions remain with their issuing registry; they must not be inherited by POS tills.", - "registry_sessions": "Registry identities, request budgets and sessions remain with their issuing registry; they must not be inherited by POS tills." + "registry_sessions": "Registry identities, request budgets and sessions remain with their issuing registry; they must not be inherited by POS tills.", + "shifts": "Attendance and cash-shift records belong to the recording till. Mobile attendance requires that local till; generic replication cannot arbitrate concurrent open shifts." }, "undecided": { "_note": "Shop data that today stays where it was written. Each needs a decision and, if synced, a check that rows carry branch_id and license and that deletes leave a tombstone. Until decided, a till and the cloud can disagree on these exactly as they did on tables.", diff --git a/api/src/utils/partner-venues.js b/api/src/utils/partner-venues.js index 328575270..e5d08ce4d 100644 --- a/api/src/utils/partner-venues.js +++ b/api/src/utils/partner-venues.js @@ -96,6 +96,16 @@ function normalizeVenue(input) { skip the ones that matter. */ ask_floor: input.ask_floor === true, + staff_orders_enabled: input.staff_orders_enabled === true, + room_charge: { + enabled: input.room_charge?.enabled === true, + name: + String(input.room_charge?.name || 'Room service charge') + .trim() + .slice(0, 60) || 'Room service charge', + percent: clampPercent(input.room_charge?.percent, 0), + taxed: input.room_charge?.taxed === true, + }, enabled: input.enabled !== false, }; } diff --git a/api/tests/captain-access.integration.cjs b/api/tests/captain-access.integration.cjs index eef0e66bb..f9fa0f7d1 100644 --- a/api/tests/captain-access.integration.cjs +++ b/api/tests/captain-access.integration.cjs @@ -497,6 +497,12 @@ test('Captain scoped sessions cannot access Mobile POS or administration', async originalUrl: '/api/sales/getListKot', }; await access.verifySession(request, staff); + await access.verifySession({ ...request, originalUrl: '/api/captain/v1/attendance' }, staff); + await access.verifySession({ ...request, originalUrl: '/api/captain/v1/attendance/settings' }, staff); + await assert.rejects( + () => access.verifySession({ ...request, originalUrl: '/api/captain/v1/attendance/report' }, staff), + (e) => e.code === 'CAPTAIN_SCOPE' + ); await assert.rejects( () => access.verifySession({ ...request, originalUrl: '/api/mobile/v1/sales' }, staff), (e) => e.code === 'CAPTAIN_SCOPE' diff --git a/api/tests/unit/repositories/combined-seating-order.test.js b/api/tests/unit/repositories/combined-seating-order.test.js index 6e654f46f..e773f674e 100644 --- a/api/tests/unit/repositories/combined-seating-order.test.js +++ b/api/tests/unit/repositories/combined-seating-order.test.js @@ -515,3 +515,129 @@ test('repository save keeps the expected capacity revision when the document pub seating_capacity_revision: 'cover-edit-test-permit', }); }); + +test.each(['', ' ', null, undefined])( + 'dine-in refuses a missing table (%s) without saving a sale', + async (value) => { + const result = await submit({ + kiosk_table_no: value, + kiosk_table_id: '', + seating_request_id: undefined, + dine_type: 'Dine-in', + }); + expect(result).toMatchObject({ status: false, data: { code: 'TABLE_REQUIRED' } }); + expect(await db.collection('sales').countDocuments()).toBe(0); + } +); + +test('takeaway does not require a table', async () => { + const result = await submit({ + kiosk_table_no: '', + kiosk_table_id: '', + seating_request_id: undefined, + dine_type: 'Take away', + order: 'Take away', + }); + expect(result.status).toBe(true); + expect(await db.collection('sales').countDocuments()).toBe(1); +}); + +test('explicit branch opt-in allows a dine-in order without table or pax', async () => { + await db + .collection('branches') + .updateOne({ _id: branch }, { $set: { captain_allow_unassigned: true } }); + const result = await submit({ + kiosk_table_no: '', + kiosk_table_id: '', + seating_request_id: undefined, + dine_type: 'Dine-in', + person_count: 0, + }); + expect(result.status).toBe(true); + expect(await db.collection('sales').countDocuments()).toBe(1); +}); + +function configureRooms(enabled = true) { + const venue = { + code: 'hotel', + name: 'Our hotel', + staff_orders_enabled: enabled, + price_adjust_percent: 20, + room_charge: { enabled: true, name: 'Room service', percent: 5 }, + }; + jest + .spyOn(require('../../../src/repositories/settings.repository').prototype, 'resolveGroup') + .mockResolvedValue({ status: true, data: { values: { partner_venues: [venue] } } }); +} +const roomRequest = (extra) => ({ + seating_request_id: undefined, + kiosk_table_no: '', + kiosk_table_id: '', + person_count: 0, + order: 'Room', + dine_type: 'Room', + venue: 'hotel', + unit: '35B', + room_charge_accepted: true, + room_charge_quote: { percent: 5, taxed: false, tax_rate: 0 }, + ...extra, +}); +test('staff room orders store a free room number and separate charge, using house prices', async () => { + configureRooms(); + const result = await submit(roomRequest()); + if (!result.status) throw new Error(result.message); + const sale = await db.collection('sales').findOne({ _id: new ObjectId(result.data.sale_id) }); + expect(sale.table_number).toBe('Our hotel - 35B'); + expect(sale.venue.unit).toBe('35B'); + expect(sale.sales_total).toBe(105); + expect(sale.charges[0]).toMatchObject({ source: 'room', amount: 5, percent: 5, tax_amount: 0 }); + const edited = await repo.updateOrderModel( + String(sale._id), + sale.items.map((v) => ({ ...v, quantity: 2, item_quantity: 2 })), + 210, + 'modified', + null, + null, + null, + undefined, + undefined, + undefined, + {} + ); + if (!edited.status) throw new Error(edited.message); + const changed = await db.collection('sales').findOne({ _id: sale._id }); + expect(changed.charges[0].amount).toBe(10); + expect(changed.sales_total).toBe(210); + const waived = await repo.updateOrderModel( + String(sale._id), + changed.items, + 200, + 'modified', + null, + null, + null, + undefined, + undefined, + undefined, + { charges: [] } + ); + if (!waived.status) throw new Error(waived.message); + expect((await db.collection('sales').findOne({ _id: sale._id })).sales_total).toBe(200); +}); +test('room charge requires agreement and room entry is required even when tables are optional', async () => { + configureRooms(); + await db + .collection('branches') + .updateOne({ _id: branch }, { $set: { captain_allow_unassigned: true } }); + expect((await submit(roomRequest({ unit: ' ' }))).status).toBe(false); + const result = await submit(roomRequest({ room_charge_accepted: false })); + if (!result.status) throw new Error(result.message); + const sale = await db.collection('sales').findOne({ _id: new ObjectId(result.data.sale_id) }); + expect(sale.sales_total).toBe(100); + expect(sale.charges || []).toEqual([]); +}); +test('staff room ordering is opt-in', async () => { + configureRooms(false); + expect((await submit(roomRequest())).status).toBe(false); + expect(await db.collection('sales').countDocuments({})).toBe(0); +}); diff --git a/api/tests/unit/repositories/sale.repository.test.js b/api/tests/unit/repositories/sale.repository.test.js index cb41c27e0..a9817d429 100644 --- a/api/tests/unit/repositories/sale.repository.test.js +++ b/api/tests/unit/repositories/sale.repository.test.js @@ -1312,6 +1312,7 @@ describe('SalesRepository', () => { const result = await salesRepository.createOnlineOrder( { branch: FAKE_BRANCH, + kiosk_table_no: 'T1', items: [{ item_id: FAKE_ITEM, item_quantity: 2, item_price: 45 }], }, { staffOrder: true } @@ -1352,6 +1353,7 @@ describe('SalesRepository', () => { const result = await salesRepository.createOnlineOrder( { branch: FAKE_BRANCH, + kiosk_table_no: 'T1', items: [{ item_id: FAKE_ITEM, item_quantity: 2, item_price: entered }], }, { staffOrder: true } diff --git a/api/tests/unit/repositories/shift.repository.test.js b/api/tests/unit/repositories/shift.repository.test.js index 81f509dc8..01ed68361 100644 --- a/api/tests/unit/repositories/shift.repository.test.js +++ b/api/tests/unit/repositories/shift.repository.test.js @@ -44,6 +44,18 @@ function makeRepo(collection) { beforeEach(() => jest.clearAllMocks()); describe('clockIn', () => { + test('mobile shifts are never closed at a guessed time, even after overnight', async () => { + const col = makeCollection(); + col.findOne.mockResolvedValue({ + _id: 'mobile', + source: 'captain', + status: SHIFT_STATUS.OPEN, + clock_in: new Date(Date.now() - 30 * 60 * 60 * 1000), + }); + expect((await makeRepo(col).clockIn({})).statusCode).toBe(409); + expect(col.updateOne).not.toHaveBeenCalled(); + expect(col.insertOne).not.toHaveBeenCalled(); + }); test('creates an open shift when none is open', async () => { const col = makeCollection(); const repo = makeRepo(col); diff --git a/api/tests/unit/services/a-coupon-works-on-the-ordering-page.test.js b/api/tests/unit/services/a-coupon-works-on-the-ordering-page.test.js index f2a5d704e..701050ddf 100644 --- a/api/tests/unit/services/a-coupon-works-on-the-ordering-page.test.js +++ b/api/tests/unit/services/a-coupon-works-on-the-ordering-page.test.js @@ -169,7 +169,7 @@ describe('the order is where the money is decided', () => { test('and it cannot make the shop pay the customer', () => { expect(order).toMatch(/Math\.max\(0, round\(foodTotal - couponDiscount\)\)/); /* On the food, not on the delivery fee. */ - expect(order).toMatch(/roundSaleTotal\(\s*round\(afterCoupon \+ deliveryFee\)/); + expect(order).toMatch(/roundSaleTotal\(\s*round\(afterCoupon \+ deliveryFee \+ roomChargeTotal\)/); }); test('the redemption is recorded only after the order exists', () => { diff --git a/api/tests/unit/services/captain-attendance.test.js b/api/tests/unit/services/captain-attendance.test.js new file mode 100644 index 000000000..2d76a87d8 --- /dev/null +++ b/api/tests/unit/services/captain-attendance.test.js @@ -0,0 +1,126 @@ +'use strict'; +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { ObjectId } = require('mongodb'); +const mongoose = require('mongoose'); +const { randomUUID } = require('node:crypto'); +jest.mock('../../../src/services/captain-access', () => ({ + fail(code, message, status = 403) { + throw Object.assign(new Error(message), { code, status }); + }, +})); +jest.mock('../../../src/db/tenant-context', () => ({ isMultiTenant: () => false })); +const service = require('../../../src/services/captain-attendance'); +let mem, db, req; +const previousDesktop = process.env.POSNIC_DESKTOP; +beforeAll(async () => { + mem = await MongoMemoryServer.create(); + await mongoose.connect(mem.getUri('attendance')); + db = mongoose.connection.db; +}, 60000); +afterAll(async () => { + await mongoose.disconnect(); + await mem?.stop(); + if (previousDesktop === undefined) delete process.env.POSNIC_DESKTOP; + else process.env.POSNIC_DESKTOP = previousDesktop; +}); +beforeEach(async () => { + process.env.POSNIC_DESKTOP = '1'; + await db.dropDatabase(); + const branchId = new ObjectId(), + licenseId = new ObjectId(); + req = { + db, + tenantContext: { branchId, licenseId }, + user: { _id: new ObjectId(), username: 'Staff' }, + socket: { remoteAddress: '::ffff:192.168.1.23' }, + headers: {}, + body: {}, + }; + await db.collection('branches').insertOne({ _id: branchId, license: licenseId }); +}); +async function enable() { + await db + .collection('branches') + .updateOne( + { _id: req.tenantContext.branchId }, + { $set: { captain_attendance: { enabled: true } } } + ); +} +const start = () => ({ action: 'start', requestId: randomUUID() }); +test('unset is off and only manager can opt in', async () => { + expect((await service.current(req)).enabled).toBe(false); + req.body = start(); + await expect(service.record(req)).rejects.toMatchObject({ code: 'ATTENDANCE_DISABLED' }); + req.body = { enabled: true }; + await expect(service.settings(req)).rejects.toMatchObject({ code: 'MANAGER_REQUIRED' }); + req.user.role = 'manager'; + await service.settings(req); + expect((await service.current(req)).enabled).toBe(true); +}); +test('server timestamps, duplicate request recovery, end replay and no automatic finish', async () => { + await enable(); + req.body = { ...start(), clock_in: '2000-01-01' }; + const first = await service.record(req); + expect(new Date(first.shift.clockIn).getFullYear()).toBe(new Date().getFullYear()); + expect((await service.record(req)).shift.id).toBe(first.shift.id); + await db + .collection('shifts') + .updateOne({}, { $set: { clock_in: new Date(Date.now() - 28 * 3600000) } }); + req.body = start(); + await expect(service.record(req)).rejects.toMatchObject({ code: 'SHIFT_ALREADY_OPEN' }); + expect((await service.current(req)).shift.status).toBe('open'); + req.body = { action: 'end', shiftId: first.shift.id, requestId: randomUUID() }; + const ended = await service.record(req); + const again = await service.record(req); + expect(again.shift.clockOut).toEqual(ended.shift.clockOut); + expect(ended.shift.workedMinutes).toBe(1680); + expect((await db.collection('shifts').findOne({})).attendance_events).toHaveLength(2); +}); +test('two phones cannot create two open shifts', async () => { + await enable(); + const results = await Promise.allSettled([ + service.record({ ...req, body: start() }), + service.record({ ...req, body: start() }), + ]); + expect(results.filter((r) => r.status === 'fulfilled')).toHaveLength(1); + expect(await db.collection('shifts').countDocuments({ status: 'open' })).toBe(1); +}); +test('staff cannot end another staff member or branch shift', async () => { + await enable(); + req.body = start(); + const result = await service.record(req); + req.body = { action: 'end', shiftId: result.shift.id, requestId: randomUUID() }; + const original = req.user._id; + req.user._id = new ObjectId(); + await expect(service.record(req)).rejects.toMatchObject({ code: 'SHIFT_NOT_FOUND' }); + req.user._id = original; + req.tenantContext.branchId = new ObjectId(); + await db + .collection('branches') + .insertOne({ _id: req.tenantContext.branchId, license: req.tenantContext.licenseId }); + await expect(service.record(req)).rejects.toMatchObject({ code: 'SHIFT_NOT_FOUND' }); +}); +test('disabling blocks new shifts but still allows finishing an existing one', async () => { + await enable(); + req.body = start(); + const result = await service.record(req); + await db + .collection('branches') + .updateOne({}, { $set: { captain_attendance: { enabled: false } } }); + req.body = { action: 'end', shiftId: result.shift.id, requestId: randomUUID() }; + expect((await service.record(req)).shift.status).toBe('closed'); +}); +test.each(['203.0.113.1', '127.0.0.1', '::1', ''])( + 'rejects non-LAN or proxy address %s', + async (ip) => { + req.socket.remoteAddress = ip; + await expect(service.current(req)).rejects.toMatchObject({ code: 'ATTENDANCE_WIFI_REQUIRED' }); + } +); +test('cloud private IP and forwarded Wi-Fi claims are not attendance proof', async () => { + process.env.POSNIC_DESKTOP = '0'; + await expect(service.current(req)).rejects.toMatchObject({ code: 'ATTENDANCE_WIFI_REQUIRED' }); + process.env.POSNIC_DESKTOP = '1'; + req.headers['x-forwarded-for'] = '192.168.1.10'; + await expect(service.current(req)).rejects.toMatchObject({ code: 'ATTENDANCE_WIFI_REQUIRED' }); +}); diff --git a/api/tests/unit/services/captain-tables.test.js b/api/tests/unit/services/captain-tables.test.js index c255a408c..6a6424038 100644 --- a/api/tests/unit/services/captain-tables.test.js +++ b/api/tests/unit/services/captain-tables.test.js @@ -623,3 +623,11 @@ test('staff temporary labels are normalized, retry safe and do not alter configu }); expect(otherRow.id).not.toBe(rows[0].id); }); + +test('table is required unless this branch explicitly allows unassigned orders', async () => { + expect((await service.list(req())).tableRequired).toBe(true); + await db + .collection('branches') + .updateOne({ _id: branch }, { $set: { captain_allow_unassigned: true } }); + expect((await service.list(req())).tableRequired).toBe(false); +}); diff --git a/api/tests/unit/services/room-charge.test.js b/api/tests/unit/services/room-charge.test.js new file mode 100644 index 000000000..b1190b60d --- /dev/null +++ b/api/tests/unit/services/room-charge.test.js @@ -0,0 +1,70 @@ +const { create, recalculate } = require('../../../src/services/room-charge'); +const { normalizeVenue } = require('../../../src/utils/partner-venues'); +test('existing venues do not opt in on upgrade', () => { + const v = normalizeVenue({ code: 'hotel', name: 'Hotel' }); + expect(v.staff_orders_enabled).toBe(false); + expect(v.room_charge.enabled).toBe(false); +}); +test('charge uses saved rate, clamps negative basis and retains tax snapshot', () => { + const c = { source: 'room', percent: 5, tax_rate: 18, taxed: true }; + expect(recalculate(c, 123.45, {})).toMatchObject({ amount: 6.17, tax_amount: 1.11 }); + expect(recalculate(c, -100, {})).toMatchObject({ amount: 0, tax_amount: 0 }); +}); +test('disabled or unaccepted charge never reads tax or charges the guest', async () => { + const db = { + collection: () => { + throw new Error('should not read'); + }, + }; + expect( + await create(db, { room_charge: { enabled: true, percent: 5 } }, false, 100, {}) + ).toBeNull(); + expect( + await create(db, { room_charge: { enabled: false, percent: 5 } }, true, 100, {}) + ).toBeNull(); +}); + +test('a changed configured rate requires review rather than charging more silently', async () => { + await expect( + create( + {}, + { + code: 'hotel', + room_charge: { enabled: true, percent: 7, taxed: false, name: 'Room service' }, + }, + true, + 100, + {}, + { percent: 5, taxed: false, tax_rate: 0 } + ) + ).rejects.toThrow('Settings changed'); +}); + +test('taxed room charge reads only the branch tax and uses the reviewed quote', async () => { + const id = '123456789012345678901234'; + let filter; + const db = { + collection: (name) => { + expect(name).toBe('grouptax'); + return { + findOne: async (q) => { + filter = q; + return { name: 'GST', rate: 18 }; + }, + }; + }, + }; + const charge = await create( + db, + { + code: 'hotel', + room_charge: { enabled: true, percent: 5, taxed: true, name: 'Room service' }, + }, + true, + 100, + { _id: 'branch', license: 'shop', default_tax: id }, + { percent: 5, taxed: true, tax_rate: 18 } + ); + expect(filter).toMatchObject({ branch_id: 'branch', license: 'shop' }); + expect(charge).toMatchObject({ amount: 5, tax_amount: 0.9, tax_name: 'GST', percent: 5 }); +}); diff --git a/api/tests/unit/services/sale.service.test.js b/api/tests/unit/services/sale.service.test.js index 28acbcd63..08d212c46 100644 --- a/api/tests/unit/services/sale.service.test.js +++ b/api/tests/unit/services/sale.service.test.js @@ -2321,6 +2321,23 @@ describe('SalesService', () => { // ── getTablesWithActiveOrders ───────────────────────────────────────────── describe('getTablesWithActiveOrders', () => { + test('unassigned dine-in orders remain individually visible', async () => { + salesRepository.aggregate.mockResolvedValue([ + { + dine_type: 'Dine-in', + table_number: '', + sales: [ + { id: 'one', number: '101', amount: 100, payment_status: 'Unpaid' }, + { id: 'two', number: '102', amount: 50, payment_status: 'Unpaid' }, + ], + }, + ]); + const result = await salesService.getTablesWithActiveOrders(BRANCH_ID); + expect(result.data.unassigned_orders.map((row) => row.sale_id)).toEqual(['one', 'two']); + expect(result.data.tables).toEqual([]); + expect(result.data.has_takeaway).toBe(false); + }); + test('keeps takeaway identities and payment status separate while retaining old summary', async () => { salesRepository.aggregate.mockResolvedValue([ { diff --git a/api/tests/unit/utils/a-shop-with-no-tables-has-no-table-rules.test.js b/api/tests/unit/utils/a-shop-with-no-tables-has-no-table-rules.test.js index 0402dd548..67500aff4 100644 --- a/api/tests/unit/utils/a-shop-with-no-tables-has-no-table-rules.test.js +++ b/api/tests/unit/utils/a-shop-with-no-tables-has-no-table-rules.test.js @@ -99,7 +99,7 @@ describe('the table limit asks whether there are tables', () => { ); test('a shop that does not run table service is never refused for a table', () => { - expect(REPO).toMatch(/const runsTableService = branchDoc\.table_options === true;/); + expect(REPO).toMatch(/const runsTableService = branchDoc\.table_options === true && !staffRoom;/); expect(REPO).toMatch(/if \(runsTableService && openTableLimit > 0 && wantsTable\)/); }); diff --git a/api/tests/unit/utils/guest-bills.test.js b/api/tests/unit/utils/guest-bills.test.js index e286756f9..0f4a3e3a4 100644 --- a/api/tests/unit/utils/guest-bills.test.js +++ b/api/tests/unit/utils/guest-bills.test.js @@ -216,3 +216,22 @@ test('translated names survive guest bill splitting without menu descriptions', expect(bill.items[0].translations).toEqual([{ locale: 'nl', name: 'Soep' }]); expect(bill.items[0].name).toBe('Soup'); }); + +test('room charges remain named and split exactly without becoming rounding', () => { + const sale = { + _id: 'room-sale', + sales_total: 105.9, + sales_sub_total: 100, + tax: 0.9, + items: [{ name: 'Meal', item_quantity: 1, unit_price: 100, item_tax: 0 }], + charges: [{ name: 'Room service', amount: 5, tax_amount: 0.9, source: 'room' }], + }; + const snapshot = snapshotFrom([sale], {}, 'Hotel - 35B'); + expect(snapshot.totalMinor).toBe(10590); + expect(snapshot.labels['charge:Room service']).toBe('Room service'); + expect(snapshot.lines[0].components.find((c) => c.key === 'adjustment').minor).toBe(0); + const guests = split(snapshot, { guests: ['A', 'B'], mode: 'equal' }); + expect(guests.reduce((sum, g) => sum + g.totalMinor, 0)).toBe(10590); + const bill = billForGuest(snapshot, guests[0], {}, sale, 'room-bill'); + expect(bill.charges).toEqual([{ name: 'Room service', amount: 2.5 }]); +}); diff --git a/docs/API.md b/docs/API.md index 68ff58a71..0f8419ada 100644 --- a/docs/API.md +++ b/docs/API.md @@ -3,7 +3,7 @@ Generated from `api/src/routes/` by `npm run docs:api`. Do not edit this file by hand; edit the routes and regenerate. -**897 endpoints across 53 route groups.** +**900 endpoints across 53 route groups.** All paths below are relative to the API root. On a desktop install the API runs on a port derived per installation (see `local-ports.js`); in the cloud it sits @@ -15,8 +15,8 @@ behind the tenant hostname. Postman, Insomnia or Bruno, point Swagger UI or Redoc at it, or generate a client from it. It is OpenAPI 3.0.3. -**104 of 897 endpoints carry a request schema** (12%), recovered from -their `express-validator` middleware. The remaining 793 are listed with +**104 of 900 endpoints carry a request schema** (12%), recovered from +their `express-validator` middleware. The remaining 796 are listed with path, method and handler but no body, because inventing a schema is worse than admitting the gap. @@ -244,6 +244,9 @@ Mounted at `/captain/v1`. Source: `api/src/routes/captain-access.routes.js`. | POST | `/captain/v1/refresh` | — | `access.refresh` | | POST | `/captain/v1/route-proof` | — | `access.routeProof` | | GET | `/captain/v1/discovery` | — | `addresses` | +| GET | `/captain/v1/attendance` | — | `attendance.current` | +| POST | `/captain/v1/attendance` | — | `attendance.record` | +| POST | `/captain/v1/attendance/settings` | — | `attendance.settings` | | POST | `/captain/v1/connections` | — | `c.branch` | | POST | `/captain/v1/takeaway-number` | — | `number` | | GET | `/captain/v1/paper-orders/options` | — | `paper.options` | diff --git a/docs/openapi.json b/docs/openapi.json index cb78b12ff..25cec2eef 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -3725,6 +3725,75 @@ } } }, + "/captain/v1/attendance": { + "get": { + "summary": "current", + "operationId": "get_captain_v1_attendance", + "tags": [ + "Captain access" + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Validation failed" + }, + "401": { + "description": "Authentication required" + }, + "500": { + "description": "Server error" + } + } + }, + "post": { + "summary": "record", + "operationId": "post_captain_v1_attendance", + "tags": [ + "Captain access" + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Validation failed" + }, + "401": { + "description": "Authentication required" + }, + "500": { + "description": "Server error" + } + }, + "description": "Request body is not yet documented. This endpoint has no validation middleware, so its shape cannot be derived from the source. Adding validation improves both runtime safety and this document." + } + }, + "/captain/v1/attendance/settings": { + "post": { + "summary": "settings", + "operationId": "post_captain_v1_attendance_settings", + "tags": [ + "Captain access" + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Validation failed" + }, + "401": { + "description": "Authentication required" + }, + "500": { + "description": "Server error" + } + }, + "description": "Request body is not yet documented. This endpoint has no validation middleware, so its shape cannot be derived from the source. Adding validation improves both runtime safety and this document." + } + }, "/captain/v1/connections": { "post": { "summary": "branch", diff --git a/frontend/modules/settings_write.html b/frontend/modules/settings_write.html index dc5203f45..1d8f7f2ec 100644 --- a/frontend/modules/settings_write.html +++ b/frontend/modules/settings_write.html @@ -6905,6 +6905,7 @@
Paper orders
Table cleaning

Off by default. When enabled, tables must be marked Available after service.

+

diff --git a/frontend/static/script/js/core/receipt-designer.js b/frontend/static/script/js/core/receipt-designer.js index 22ed87f83..e6710a6da 100644 --- a/frontend/static/script/js/core/receipt-designer.js +++ b/frontend/static/script/js/core/receipt-designer.js @@ -308,7 +308,7 @@ if (hasTaxAmount(sgst)) content += pair('SGST', money(sgst)); } else if (hasTaxAmount(data.tax)) content += pair(contract.taxLabel(data, PosnicPro.local.get('country_setting')), money(data.tax)); - (data.charges || []).forEach(function (c) { content += pair(c.name || 'Charge', money(Number(c.amount || 0) + Number(c.tax_amount || 0))); }); + (data.charges || []).forEach(function (c) { content += pair(c.name || 'Charge', money(Number(c.amount || 0))); }); if (Number(data.round_off)) content += pair('Round Off', money(data.round_off)); content += pair('Total', money(data.items_total), true); if (!beforePayment && data.sales_id) { diff --git a/frontend/static/script/js/modules/js/kot_v2.js b/frontend/static/script/js/modules/js/kot_v2.js index beecdd6c3..66a3b5889 100644 --- a/frontend/static/script/js/modules/js/kot_v2.js +++ b/frontend/static/script/js/modules/js/kot_v2.js @@ -271,6 +271,7 @@ ]); if (generation !== state.generation) return; P.restaurantFeedback?.apply(floor.orderFeedback, branch()); + state.roomVenues = floor.roomVenues || []; state.floor = floor.tables || []; state.sales = list.list || []; let page = 1; @@ -478,7 +479,7 @@ const s = state.sale; if (!s) return emptyOrderHTML(); const details = s.restaurant_details || {}; - return `

${esc(s.dine_type === 'Take away' ? 'Takeaway ' + (s.takeaway_number || s.token_id || '') : 'Table ' + s.table_number)}

${takeawayStatus(s)}${esc(s.person_count || 0)} guests · ${esc(details.taken_by)} · ${esc(time(s.created_date))}
${iconButton('move', 'Move table')}${iconButton('merge', 'Merge tables')}${iconButton('split', 'Split payment')}${button('Actions', 'actions', 'aria-haspopup="true" aria-expanded="false" aria-controls="kv2-actions-menu"')}
Customer ${esc(s.customer_name || 'Walk-in customer')} ${esc(s.customer_phone || '')}${button('Choose customer', 'customer')}
${state.tab === 'activity' ? (details.events || []).map((e) => `
${esc(e.kind)} · ${esc(time(e.at))} · ${esc(e.actor)}

${esc((e.items || []).map((l) => l.quantity + ' × ' + l.name).join(', '))}

`).join('') : roundHTML(s)}
Bill breakdown & kitchen note

Subtotal ${esc(money(s.sales_sub_total || s.subtotal || 0))}

Tax ${esc(money(s.tax || 0))}

${taxBreakdown(s)}${(s.charges || []).map(c => `

${esc(c.name)}${esc(money(c.amount))}

`).join('')}

Discount ${esc(money(Number(s.discount || 0) + Number(s.sale_extra_discount || 0)))}

${Number(s.round_off || s.sales_round_off) ? `

Rounding ${esc(money(s.round_off || s.sales_round_off))}

` : ''}

${esc(details.preparation_note || 'No kitchen note')}

${s.payment_status === 'Paid' ? '' : button('Additional charges', 'charges') + button('Edit details', 'notes')}
Total${esc(money(s.sales_total))}
${s.payment_status === 'Paid' ? '' : button('Discount', 'discount')}${button('Print bill', 'printBill')}${button('Print KOT', 'printKOT')}${s.payment_status === 'Paid' ? '' + esc(P.i18n.t('lang_paid', 'Paid')) + '' : button('Take payment', 'pay', 'class="primary"')}
`; + return `

${esc(s.dine_type === 'Take away' ? 'Takeaway ' + (s.takeaway_number || s.token_id || '') : s.dine_type === 'Room' ? s.venue?.label || s.table_number : 'Table ' + s.table_number)}

${takeawayStatus(s)}${esc(s.person_count || 0)} guests · ${esc(details.taken_by)} · ${esc(time(s.created_date))}
${iconButton('move', 'Move table')}${iconButton('merge', 'Merge tables')}${iconButton('split', 'Split payment')}${button('Actions', 'actions', 'aria-haspopup="true" aria-expanded="false" aria-controls="kv2-actions-menu"')}
Customer ${esc(s.customer_name || 'Walk-in customer')} ${esc(s.customer_phone || '')}${button('Choose customer', 'customer')}
${state.tab === 'activity' ? (details.events || []).map((e) => `
${esc(e.kind)} · ${esc(time(e.at))} · ${esc(e.actor)}

${esc((e.items || []).map((l) => l.quantity + ' × ' + l.name).join(', '))}

`).join('') : roundHTML(s)}
Bill breakdown & kitchen note

Subtotal ${esc(money(s.sales_sub_total || s.subtotal || 0))}

Tax ${esc(money(s.tax || 0))}

${taxBreakdown(s)}${(s.charges || []).map(c => `

${esc(c.name)}${esc(money(c.amount))}

`).join('')}

Discount ${esc(money(Number(s.discount || 0) + Number(s.sale_extra_discount || 0)))}

${Number(s.round_off || s.sales_round_off) ? `

Rounding ${esc(money(s.round_off || s.sales_round_off))}

` : ''}

${esc(details.preparation_note || 'No kitchen note')}

${s.payment_status === 'Paid' ? '' : button('Additional charges', 'charges') + button('Edit details', 'notes')}
Total${esc(money(s.sales_total))}
${s.payment_status === 'Paid' ? '' : button('Discount', 'discount')}${button('Print bill', 'printBill')}${button('Print KOT', 'printKOT')}${s.payment_status === 'Paid' ? '' + esc(P.i18n.t('lang_paid', 'Paid')) + '' : button('Take payment', 'pay', 'class="primary"')}
`; } function catalogueHTML() { return state.catalogue.map((item, i) => { @@ -525,7 +526,7 @@ } function draftHTML() { const d = state.draft; - return `
${esc(d.customer?.name || 'Walk-in customer')}${button('Choose customer', 'customer')}

${PosnicPro.i18n.t('lang_add_items', 'Add items')} / ${esc(d.table || 'Takeaway')}

Search all dishes by name, barcode or quick code. F2 to focus.
${d.intent ? button('Review latest order', 'rebase') : button('Discard draft', 'discard')}
${d.tableConflict ? `
${esc(d.tableConflict)} Your items are kept.${button('Change table', 'changeDraftTable')}
` : ''}
${button('Item not on menu', 'offmenu')}
${categoriesHTML()}
${catalogueHTML()}
${button(PosnicPro.i18n.t('lang_cancel', 'Cancel'), 'discard')}${d.items.reduce((n, l) => n + Number(l.quantity), 0)} Items${button(d.intent ? PosnicPro.i18n.t('lang_retry_saved_send', 'Retry saved send') : PosnicPro.i18n.t('lang_kot_workspace_send', 'Send to kitchen'), 'send', `class="primary" ${d.items.length ? '' : 'disabled'}`)}
`; + return `
${esc(d.customer?.name || 'Walk-in customer')}${button('Choose customer', 'customer')}

${PosnicPro.i18n.t('lang_add_items', 'Add items')} / ${esc(d.table || 'Takeaway')}

Search all dishes by name, barcode or quick code. F2 to focus.
${d.intent ? button('Review latest order', 'rebase') : button('Discard draft', 'discard')}
${d.tableConflict ? `
${esc(d.tableConflict)} Your items are kept.${button('Change table', 'changeDraftTable')}
` : ''}
${button('Item not on menu', 'offmenu')}
${categoriesHTML()}
${catalogueHTML()}
${button(PosnicPro.i18n.t('lang_cancel', 'Cancel'), 'discard')}${d.items.reduce((n, l) => n + Number(l.quantity), 0)} Items${button(d.intent ? PosnicPro.i18n.t('lang_retry_saved_send', 'Retry saved send') : PosnicPro.i18n.t('lang_kot_workspace_send', 'Send to kitchen'), 'send', `class="primary" ${d.items.length ? '' : 'disabled'}`)}
`; } function render() { const desktopTools = document.getElementById('posnic-desktop-fab'); @@ -533,7 +534,7 @@ if (!root()) return; root().classList.toggle('kv2-composing', !!state.draft); root().classList.toggle('kv2-playful', !!P.restaurantFeedback?.allowed?.()); - root().innerHTML = `

Table orders

${state.refreshed ? 'Refreshed ' + esc(time(state.refreshed)) : ''}
${state.draft ? button(state.expanded ? PosnicPro.i18n.t('lang_review_this_round','Review this round') : PosnicPro.i18n.t('lang_tables','Tables'), 'expand') : ''}${iconButton('refresh', 'Refresh orders')}${state.draft ? '' : button('Takeaway', 'takeaway') + button('+ New order', 'new', 'class="primary"')}
${floorHTML()}${state.draft ? draftHTML() : orderHTML()}
`; + root().innerHTML = `

Table orders

${state.refreshed ? 'Refreshed ' + esc(time(state.refreshed)) : ''}
${state.draft ? button(state.expanded ? PosnicPro.i18n.t('lang_review_this_round','Review this round') : PosnicPro.i18n.t('lang_tables','Tables'), 'expand') : ''}${iconButton('refresh', 'Refresh orders')}${state.draft ? '' : (state.roomVenues?.length ? button(roomText('Rooms'), 'rooms') : '') + button('Takeaway', 'takeaway') + button('+ New order', 'new', 'class="primary"')}
${floorHTML()}${state.draft ? draftHTML() : orderHTML()}
`; if (state.draft) { bindSearch(); if (!state.catalogueLoaded) loadCatalogue(); @@ -771,6 +772,7 @@ kiosk_table_no: d.table, kiosk_table_id: d.tableId || '', person_count: d.guests, + ...(d.venue ? { venue: d.venue, unit: d.unit, destination: { unit: d.unit }, room_charge_accepted: d.roomChargeAccepted, room_charge_quote: {percent:d.roomCharge?.percent, taxed:d.roomCharge?.taxed === true, tax_rate:d.roomCharge?.tax_rate || 0} } : {}), customer_id: d.customer?.id || '', customer_name: d.customer?.name || '', customerMobile: d.customer?.phone || '', @@ -852,25 +854,70 @@ await refresh(); if (d.saleId && P.kotPrint) P.kotPrint.afterSave(d.saleId); } + const roomText = text => { switch (text) { +case "Rooms": return P.i18n.t("lang_rooms", "Rooms"); +case "Venue": return P.i18n.t("lang_room_venue", "Venue"); +case "Room number": return P.i18n.t("lang_room_number", "Room number"); +case "Room order": return P.i18n.t("lang_room_order", "Room order"); +case "Room service charge": return P.i18n.t("lang_room_service_charge", "Room service charge"); +case "Guest agreed to this additional charge": return P.i18n.t("lang_room_charge_agreement", "Guest agreed to this additional charge"); +case "Choose a venue and enter the room number.": return P.i18n.t("lang_room_destination_required", "Choose a venue and enter the room number."); +case "The room charge tax is unavailable. Review the tax settings.": return P.i18n.t("lang_room_charge_tax_unavailable", "The room charge tax is unavailable. Review the tax settings."); +case "Allow staff room orders": return P.i18n.t("lang_staff_room_orders", "Allow staff room orders"); +case "Staff room orders use normal menu prices. Web menu markup and venue commission are separate.": return P.i18n.t("lang_staff_room_prices", "Staff room orders use normal menu prices. Web menu markup and venue commission are separate."); +case "Offer an additional room charge": return P.i18n.t("lang_room_charge_offer", "Offer an additional room charge"); +case "Charge name": return P.i18n.t("lang_charge_name", "Charge name"); +case "Apply default tax": return P.i18n.t("lang_apply_default_tax", "Apply default tax"); +case "Percentage of food after discounts, before tax. Apply only with guest agreement; staff can remove it from the bill.": return P.i18n.t("lang_room_charge_basis", "Percentage of food after discounts, before tax. Apply only with guest agreement; staff can remove it from the bill."); +case "{0}: {1}% of food after discounts, before tax.": return P.i18n.t("lang_room_charge_rate", "{0}: {1}% of food after discounts, before tax."); +case "Included on the final bill.": return P.i18n.t("lang_room_charge_final", "Included on the final bill."); +case "Enable staff room orders in Settings \u2192 Restaurant \u2192 Hotels and other venues.": return P.i18n.t("lang_room_orders_enable_help", "Enable staff room orders in Settings → Restaurant → Hotels and other venues."); +default: return text; } }; + async function newRoomOrder() { + if (resolveDraftBeforeSwitch(() => newRoomOrder())) return; + const floor = await api('get', 'captain/v1/tables', { branchId: branch() }); + const venues = floor.roomVenues || []; + if (!venues.length) throw new Error(roomText('Enable staff room orders in Settings → Restaurant → Hotels and other venues.')); + const modal = dialog(roomText('Room order'), `

`, async form => { + const venue = venues.find(v => v.code === form.get('venue')); + const unit = String(form.get('unit') || '').trim(); + if (!venue || !unit) throw new Error(roomText('Choose a venue and enter the room number.')); + if (form.get('acceptCharge') === 'on' && venue.room_charge?.taxed && !venue.room_charge.tax_available) throw new Error(roomText('The room charge tax is unavailable. Review the tax settings.')); + state.sale = null; state.selected = null; + startDraft({ saleId: null, dineType: 'Room', table: `${venue.name} - ${unit}`, tableId: '', guests: 0, + venue: venue.code, unit, roomChargeAccepted: form.get('acceptCharge') === 'on', roomCharge: venue.room_charge }); + }, P.i18n.t('lang_next', 'Next')); + const showRate = () => { + const v = venues.find(v => v.code === modal.querySelector('[name=venue]').value); + const rule = v?.room_charge; + modal.querySelector('[data-room-rate]').textContent = rule?.enabled && rule.percent > 0 + ? (roomText('{0}: {1}% of food after discounts, before tax.').replace('{0}', rule.name).replace('{1}', rule.percent) + (rule.taxed ? ' + ' + P.i18n.t('lang_tax', 'Tax:') + ' ' + (rule.tax_rate ?? '') + '%' : '')) : ''; + modal.querySelector('[data-room-agreement]').hidden = !(rule?.enabled && rule.percent > 0); + modal.querySelector('[name=acceptCharge]').checked = false; + }; + modal.querySelector('[name=venue]').onchange = showRate; showRate(); + } async function newOrder(takeaway = false, tableId = '', movingDraft = null) { if (movingDraft && (state.draft !== movingDraft || movingDraft.intent || movingDraft.saleId)) return; if (!movingDraft && resolveDraftBeforeSwitch(() => newOrder(takeaway, tableId))) return; + let tableRequired = true; if (!takeaway) { const floor = await api('get', 'captain/v1/tables', { branchId: branch() }); state.floor = floor.tables || []; + tableRequired = floor.tableRequired !== false; } const free = state.floor.filter((t) => t.status === 'available'); const modal = dialog( takeaway ? PosnicPro.i18n.t('lang_new_takeaway', 'New takeaway') : PosnicPro.i18n.t('lang_start_a_table_order', 'Start a table order'), - `
${takeaway ? '' : `

${icon('table')} Choose a table

${[...state.floor].sort((a, b) => Number(b.status === 'available') - Number(a.status === 'available')).map((t) => ``).join('')}
`}

${icon('guests')} Number of guests

${takeaway ? '' : `
${[1, 2, 3, 4, 5, 6, 8, 10].map((n) => ``).join('')}
`}
`, + `
${takeaway ? '' : `

${icon('table')} Choose a table

${[...state.floor].sort((a, b) => Number(b.status === 'available') - Number(a.status === 'available')).map((t) => ``).join('')}
`}

${icon('guests')} Number of guests

${takeaway ? '' : `
${[1, 2, 3, 4, 5, 6, 8, 10].map((n) => ``).join('')}
`}
`, async (form) => { let t = free.find((t) => t.id === form.get('table')); const custom = String(form.get('custom') || '').trim(); - if (!takeaway && !t && !custom) throw new Error('Choose a table or enter its number.'); + if (!takeaway && tableRequired && !t && !custom) throw new Error('Choose a table or enter its number.'); if (custom.toUpperCase() === 'TA') throw new Error('Use Takeaway for an order to go.'); - if (!takeaway) { + if (!takeaway && (t || custom)) { const floor = await api('get', 'captain/v1/tables', { branchId: branch() }); state.floor = floor.tables || []; const name = custom || t?.tableorder_value; @@ -908,13 +955,13 @@ render(); } else startDraft(seating); }, - 'Choose dishes', + PosnicPro.i18n.t('lang_next', 'Next'), movingDraft || null, ); modal.classList.add('kv2-seating-dialog'); if (movingDraft) { modal.querySelector('h3').textContent = 'Change table'; - modal.querySelector('[name=guests]').value = movingDraft.guests || 2; + modal.querySelector('[name=guests]').value = movingDraft.guests || 0; } modal.addEventListener('click', (e) => { const b = e.target.closest('[data-guests]'); @@ -1029,9 +1076,10 @@ const taxes = await api('get', 'setting/getTaxAjaxList', {query: ''}); const defaultTax = (taxes.suggestions || []).find(t => String(t.tax_id) === String(P.local.get('default_tax_id'))); const rate = Number(defaultTax?.tax_value) || 0; - const row = (charge = {}) => `
`; + const row = (charge = {}) => `
`; const d = dialog('Additional charges', `
${list.map(row).join('')}
`, async (_, modal) => { const values = [...modal.querySelectorAll('.kv2-charge-row')].map(el => { + if (el.hasAttribute('data-room-charge')) return list.find(c => c.source === 'room'); const name = el.querySelector('[name=charge-name]').value.trim(); const amount = Number(el.querySelector('[name=charge-amount]').value); const taxed = el.querySelector('[name=charge-tax]').value === 'yes'; @@ -1043,6 +1091,7 @@ await api('post', 'sales/updateOrder', {...P.kotWorkspace.editPayload(sale, sale.items.map(l => P.kot.editLine(l))),charges:values}); await refresh(); }, 'Save', sale); + d.querySelectorAll('[data-room-charge] input, [data-room-charge] select').forEach(el => el.disabled = true); const add = () => { if (d.querySelectorAll('.kv2-charge-row').length >= 20) return; d.querySelector('.kv2-charge-list').insertAdjacentHTML('beforeend', row()); @@ -1196,6 +1245,7 @@ const item = state.catalogue[Number(b.dataset.index)]; chooseProduct(item); } else if (a === 'new') await newOrder(); + else if (a === 'rooms') await newRoomOrder(); else if (a === 'takeaway') await newOrder(true); else if (a === 'changeDraftTable') await newOrder(false, '', state.draft); else if (a === 'add') startDraft(); diff --git a/frontend/static/script/js/modules/js/sales.js b/frontend/static/script/js/modules/js/sales.js index 63f1b8a1d..a8fe35a63 100644 --- a/frontend/static/script/js/modules/js/sales.js +++ b/frontend/static/script/js/modules/js/sales.js @@ -3704,6 +3704,10 @@ PosnicPro.kotorder = { }, renderPersons: function () { + PosnicPro.kotorder.tableRequired = true; + PosnicPro.get({url:'captain/v1/table-settings'}, function(data) { + PosnicPro.kotorder.tableRequired = data.tableRequired !== false; + }); let personsHtml = ''; let targetPersonCount = (typeof PosnicPro.kotorder.kotPersonCount === 'number' && PosnicPro.kotorder.kotPersonCount > 0) ? PosnicPro.kotorder.kotPersonCount @@ -3899,12 +3903,8 @@ PosnicPro.kotorder = { $(this).find('strong').css('color', '#6c757d'); }); - // Auto-proceed for Take Away using the shared kotOrderNext flow - // so that new and edit KOT flows behave consistently. - if (PosnicPro.kotorder && typeof PosnicPro.kotorder.kotOrderNext === 'function') { - PosnicPro.kotorder.kotOrderType = dineType; - PosnicPro.kotorder.kotOrderNext(); - } + // Keep selections on this screen until the explicit Next click. + PosnicPro.kotorder.kotOrderType = dineType; } else { $('#kot_dinein_label') .addClass('active btn-info') @@ -4024,7 +4024,7 @@ PosnicPro.kotorder = { } } - if (dineType === 'Dine-in' && (!tableNumber || tableNumber === '')) { + if (dineType === 'Dine-in' && PosnicPro.kotorder.tableRequired !== false && (!tableNumber || tableNumber === '')) { PosnicPro.alert('warning', PosnicPro.i18n.t('lang_select_a_table', 'Select a table.')); return; } diff --git a/frontend/static/script/js/modules/js/settings.js b/frontend/static/script/js/modules/js/settings.js index 3c0e0c6ee..f513e4255 100644 --- a/frontend/static/script/js/modules/js/settings.js +++ b/frontend/static/script/js/modules/js/settings.js @@ -8794,6 +8794,13 @@ PosnicPro.salesChannels = { '
' + '' + '
' + + '
' + + '

' + t('lang_staff_room_prices', 'Staff room orders use normal menu prices. Web menu markup and venue commission are separate.') + '

' + + '' + + '
' + + '' + + '
' + + '

' + t('lang_room_charge_basis', 'Percentage of food after discounts, before tax. Apply only with guest agreement; staff can remove it from the bill.') + '

' + '
' + '
'; }, @@ -9012,6 +9019,13 @@ PosnicPro.salesChannels = { address: String($row.find('.venue-address').val() || '').trim(), delivery_note: String($row.find('.venue-note').val() || '').trim(), ask_floor: $row.find('.venue-ask-floor').is(':checked'), + staff_orders_enabled: $row.find('.venue-staff').is(':checked'), + room_charge: { + enabled: $row.find('.venue-charge-enabled').is(':checked'), + name: String($row.find('.venue-charge-name').val() || 'Room service charge').trim(), + percent: Number($row.find('.venue-charge-percent').val()) || 0, + taxed: $row.find('.venue-charge-taxed').is(':checked') + }, enabled: true }); }); @@ -10374,13 +10388,14 @@ $(document).on('shown.bs.tab', '#captaintables-tab-line', function () { $('#save_captain_tables').prop('disabled',true); PosnicPro.get({url:'captain/v1/table-settings'},function(data){ $('#captain_table_cleaning').prop('checked',data.cleaningEnabled === true); + $('#captain_allow_unassigned').prop('checked',data.tableRequired === false); $('#save_captain_tables').prop('disabled',false); $('#captain_tables_message').empty(); },function(){ $('#captain_tables_message').text(PosnicPro.i18n.t('lang_could_not_load_table_settings_please_retry', 'Could not load table settings. Please retry.')); }); }); $(document).on('click','#save_captain_tables',function(){ var button=$(this).prop('disabled',true); - PosnicPro.post({url:'captain/v1/table-settings',processData:false,data:JSON.stringify({cleaningEnabled:$('#captain_table_cleaning').is(':checked')})},function(){ + PosnicPro.post({url:'captain/v1/table-settings',processData:false,data:JSON.stringify({cleaningEnabled:$('#captain_table_cleaning').is(':checked'),tableRequired:!$('#captain_allow_unassigned').is(':checked')})},function(){ button.prop('disabled',false);$('#captain_tables_message').text(PosnicPro.i18n.t('lang_successfully_completed','Successfully Completed')); },function(){button.prop('disabled',false);$('#captain_tables_message').text(PosnicPro.i18n.t('lang_could_not_save_table_settings_please_retry', 'Could not save table settings. Please retry.'));}); }); diff --git a/languages/_english.json b/languages/_english.json index c07c3b5b5..2c0da8dd9 100644 --- a/languages/_english.json +++ b/languages/_english.json @@ -300,6 +300,7 @@ "lang_applies_to_new_items_in_this_branch_existi": "Applies to new items in this branch. Existing items keep their stock settings. You can still mark an item sold out manually.", "lang_apply_a_default_tax_rate_to_new_items": "Apply a default tax rate to new items", "lang_apply_copies": "Apply copies", + "lang_apply_default_tax": "Apply default tax", "lang_apply_discount": "Apply discount", "lang_apply_explicit_zero_prices_blank_cells_sti": "Apply explicit zero prices (blank cells still stay unchanged)", "lang_apply_margin": "Apply margin", @@ -693,6 +694,7 @@ "lang_charge_free_above": "Free above", "lang_charge_label": "Charge label", "lang_charge_min_order": "Minimum order", + "lang_charge_name": "Charge name", "lang_charge_name_e_g_parcel": "Charge name (e.g. Parcel)", "lang_charge_tax": "+ Charge / tax", "lang_charges_taxes_any_name": "Charges & taxes - any name", @@ -1814,7 +1816,6 @@ "lang_guest_bills_changed": "The table changed. Ask for updated guest bills before collecting these amounts.", "lang_guest_bills_retry": "Retry guest bills", "lang_guest_bills_unpaid": "Unpaid guest shares. Collect and record payment using the cashier payment controls.", - "lang_guest_count": "Guest count", "lang_guests_optional": "Guests (optional)", "lang_handset_phone": "Phone", "lang_handset_status": "Status", @@ -3827,7 +3828,20 @@ "lang_role_editor_refund_cap": "Role editor refund cap", "lang_roles_title": "Roles", "lang_room": "Room", + "lang_room_charge_agreement": "Guest agreed to this additional charge", + "lang_room_charge_basis": "Percentage of food after discounts, before tax. Apply only with guest agreement; staff can remove it from the bill.", + "lang_room_charge_final": "Included on the final bill.", + "lang_room_charge_offer": "Offer an additional room charge", + "lang_room_charge_rate": "{0}: {1}% of food after discounts, before tax.", + "lang_room_charge_tax_unavailable": "The room charge tax is unavailable. Review the tax settings.", + "lang_room_destination_required": "Choose a venue and enter the room number.", + "lang_room_number": "Room number", + "lang_room_order": "Room order", + "lang_room_orders_enable_help": "Enable staff room orders in Settings → Restaurant → Hotels and other venues.", "lang_room_reference": "Room / reference", + "lang_room_service_charge": "Room service charge", + "lang_room_venue": "Venue", + "lang_rooms": "Rooms", "lang_roster_date": "Roster date", "lang_roster_end": "Roster end", "lang_roster_from": "Roster from", @@ -4230,6 +4244,8 @@ "lang_staff_clock_activity": "Staff clock activity", "lang_staff_clock_in_out_from_the_header_clock_l": "Staff clock in/out from the header clock; labour report and payroll exports.", "lang_staff_name_or_email": "Staff name or email", + "lang_staff_room_orders": "Allow staff room orders", + "lang_staff_room_prices": "Staff room orders use normal menu prices. Web menu markup and venue commission are separate.", "lang_staff_unlock_with_a_4_digit_pin_instead_of": "Staff unlock with a 4-digit PIN instead of a password.", "lang_staffroster_title": "Roster", "lang_staffshifts_title": "Workforce", diff --git a/languages/ar.json b/languages/ar.json index 6b3b7b70b..081e9088d 100644 --- a/languages/ar.json +++ b/languages/ar.json @@ -5001,5 +5001,22 @@ "lang_text_style": "نمط النص", "lang_this_product_is_hidden_from_sales_your_bas": "هذا المنتج مخفي عن المبيعات. لم يتم تغيير سلتك.", "lang_this_window_belongs_to_one_branch_open_ano": "تنتمي هذه النافذة إلى فرع واحد. افتح منفذًا آخر من النافذة الرئيسية.", - "lang_total_labels": "إجمالي التسميات" + "lang_total_labels": "إجمالي التسميات", + "lang_rooms": "الغرف", + "lang_room_number": "رقم الغرفة", + "lang_room_order": "طلب غرفة", + "lang_room_service_charge": "رسوم خدمة الغرف", + "lang_room_charge_agreement": "وافق الضيف على هذه الرسوم الإضافية", + "lang_room_destination_required": "اختر مكانًا وأدخل رقم الغرفة.", + "lang_room_charge_tax_unavailable": "ضريبة رسوم الغرفة غير متوفرة. راجع إعدادات الضرائب.", + "lang_staff_room_orders": "السماح بطلبات غرف الموظفين", + "lang_staff_room_prices": "تستخدم طلبات غرف الموظفين أسعار القائمة العادية. ترميز قائمة الويب ولجنة المكان منفصلان.", + "lang_room_charge_offer": "عرض رسوم غرفة إضافية", + "lang_charge_name": "اسم الشحنة", + "lang_apply_default_tax": "تطبيق الضريبة الافتراضية", + "lang_room_charge_basis": "نسبة الطعام بعد الخصومات، قبل الضرائب. لا تنطبق إلا بموافقة الضيف؛ يمكن للموظفين إزالتها من الفاتورة.", + "lang_room_charge_rate": "{0}: {1}% من الطعام بعد الخصومات، قبل الضرائب.", + "lang_room_charge_final": "مدرج في الفاتورة النهائية.", + "lang_room_orders_enable_help": "قم بتمكين طلبات غرف الموظفين في الإعدادات → Restaurant → الفنادق والأماكن الأخرى.", + "lang_room_venue": "المكان" } diff --git a/languages/bg.json b/languages/bg.json index cb9a4f75e..1b606c7a9 100644 --- a/languages/bg.json +++ b/languages/bg.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Стил на текст", "lang_this_product_is_hidden_from_sales_your_bas": "Този продукт е скрит от продажбите. Вашата кошница не е променена.", "lang_this_window_belongs_to_one_branch_open_ano": "Този прозорец принадлежи на един клон. Отворете друг изход от главния прозорец.", - "lang_total_labels": "Общо етикети" + "lang_total_labels": "Общо етикети", + "lang_rooms": "Стаи", + "lang_room_number": "Номер на стаята", + "lang_room_order": "Ред на стаята", + "lang_room_service_charge": "Такса за рум-сървиз", + "lang_room_charge_agreement": "Гостът се съгласи с тази допълнителна такса", + "lang_room_destination_required": "Изберете място и въведете номера на стаята.", + "lang_room_charge_tax_unavailable": "Таксата за стаята не е налична. Прегледайте данъчните настройки.", + "lang_staff_room_orders": "Разрешаване на поръчки за стаи на персонала", + "lang_staff_room_prices": "Поръчките за стаи на персонала използват нормалните цени на менюто. Маркирането на уеб менюто и комисионната на мястото са отделни", + "lang_room_charge_offer": "Предложете допълнителна такса за стая", + "lang_charge_name": "Име на заряда", + "lang_apply_default_tax": "Приложете данък по подразбиране", + "lang_room_charge_basis": "Процент храна след отстъпки, преди данъчно облагане. Кандидатствайте само със съгласие на гостите; персоналът може да го премахне от сметката.", + "lang_room_charge_rate": "{0}: {1}% от храната след отстъпки, преди данъчно облагане.", + "lang_room_charge_final": "Включено в окончателната сметка.", + "lang_room_orders_enable_help": "Активирайте поръчките за стаи на персонала в Настройки „Ресторант“ „Хотели и други места“.", + "lang_room_venue": "Място" } diff --git a/languages/bn.json b/languages/bn.json index 81067ef00..a42c09225 100644 --- a/languages/bn.json +++ b/languages/bn.json @@ -4923,5 +4923,22 @@ "lang_text_style": "পাঠ্য শৈলী", "lang_this_product_is_hidden_from_sales_your_bas": "এই পণ্যটি বিক্রয় থেকে লুকানো। আপনার ঝুড়ি পরিবর্তন করা হয়নি।", "lang_this_window_belongs_to_one_branch_open_ano": "এই জানালা একটি শাখার অন্তর্গত। প্রধান উইন্ডো থেকে অন্য আউটলেট খুলুন।", - "lang_total_labels": "মোট লেবেল" + "lang_total_labels": "মোট লেবেল", + "lang_rooms": "কক্ষ", + "lang_room_number": "রুম নম্বর", + "lang_room_order": "রুম অর্ডার", + "lang_room_service_charge": "রুম সার্ভিস চার্জ", + "lang_room_charge_agreement": "অতিথি এই অতিরিক্ত চার্জে সম্মত", + "lang_room_destination_required": "একটি ভেন্যু চয়ন করুন এবং রুম নম্বর লিখুন।", + "lang_room_charge_tax_unavailable": "রুম চার্জ ট্যাক্স উপলব্ধ নয়। কর সেটিংস পর্যালোচনা করুন।", + "lang_staff_room_orders": "স্টাফ রুম অর্ডার দেওয়া", + "lang_staff_room_prices": "স্টাফ রুম অর্ডার সাধারণ মেনু দাম ব্যবহার করে। ওয়েব মেনু মার্কআপ এবং ভেন্যু কমিশন পৃথক।", + "lang_room_charge_offer": "অতিরিক্ত রুম চার্জ অফার করুন", + "lang_charge_name": "চার্জ নাম", + "lang_apply_default_tax": "ডিফল্ট ট্যাক্স প্রয়োগ করুন", + "lang_room_charge_basis": "ছাড়ের পরে খাবারের শতাংশ, ট্যাক্সের আগে। শুধুমাত্র অতিথি চুক্তির সাথে আবেদন করুন; কর্মীরা এটি বিল থেকে সরাতে পারেন।", + "lang_room_charge_rate": "{0}: ছাড়ের পরে খাবারের {1}%, ট্যাক্সের আগে।", + "lang_room_charge_final": "চূড়ান্ত বিলে অন্তর্ভুক্ত।", + "lang_room_orders_enable_help": "সেটিংস → রেস্তোঁরা → হোটেল এবং অন্যান্য ভেন্যুতে স্টাফ রুম অর্ডার সক্ষম করুন।", + "lang_room_venue": "ভেন্যু" } diff --git a/languages/bs.json b/languages/bs.json index cb68c4dab..a0f2ad9cc 100644 --- a/languages/bs.json +++ b/languages/bs.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Stil teksta", "lang_this_product_is_hidden_from_sales_your_bas": "Ovaj proizvod je skriven od prodaje. Vaša košarica nije promijenjena.", "lang_this_window_belongs_to_one_branch_open_ano": "Ovaj prozor pripada jednoj grani. Otvorite drugu utičnicu iz glavnog prozora.", - "lang_total_labels": "Ukupno oznaka" + "lang_total_labels": "Ukupno oznaka", + "lang_rooms": "Sobe", + "lang_room_number": "Broj sobe", + "lang_room_order": "Redoslijed sobe", + "lang_room_service_charge": "Naknada za poslugu u sobu", + "lang_room_charge_agreement": "Gost je pristao na ovu dodatnu naknadu", + "lang_room_destination_required": "Odaberite mjesto i unesite broj sobe.", + "lang_room_charge_tax_unavailable": "Porez na naknadu za sobu nije dostupan. Pregledajte porezne postavke.", + "lang_staff_room_orders": "Dozvolite narudžbe soba osoblja", + "lang_staff_room_prices": "udžbe soba osoblja koriste normalne cijene menija. Označavanje web menija i provizija mjesta održavanja su odvojeni", + "lang_room_charge_offer": "Ponudite dodatnu naknadu za sobu", + "lang_charge_name": "Naziv naplate", + "lang_apply_default_tax": "Primijeni zadani porez", + "lang_room_charge_basis": "Postotak hrane nakon popusta, prije oporezivanja. Prijavite se samo uz dogovor gosta; osoblje ga može ukloniti sa računa.", + "lang_room_charge_rate": "{0}: {1}% hrane nakon popusta, prije oporezivanja.", + "lang_room_charge_final": "Uključeno u konačni račun.", + "lang_room_orders_enable_help": "Omogućite narudžbu soba osoblja u Postavkama → Restoran → Hoteli i druga mjesta.", + "lang_room_venue": "Mjesto održavanja" } diff --git a/languages/cs.json b/languages/cs.json index 4ee958abb..b7d351329 100644 --- a/languages/cs.json +++ b/languages/cs.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Styl textu", "lang_this_product_is_hidden_from_sales_your_bas": "Tento produkt je skrytý před prodejem. Váš košík nebyl změněn.", "lang_this_window_belongs_to_one_branch_open_ano": "Toto okno patří jedné větvi. Otevřete další zásuvku z hlavního okna.", - "lang_total_labels": "Celkem štítků" + "lang_total_labels": "Celkem štítků", + "lang_rooms": "Pokoje", + "lang_room_number": "Číslo pokoje", + "lang_room_order": "Objednávka pokojů", + "lang_room_service_charge": "Poplatek za pokojovou službu", + "lang_room_charge_agreement": "Host souhlasil s tímto dodatečným poplatkem", + "lang_room_destination_required": "Vyberte místo a zadejte číslo místnosti.", + "lang_room_charge_tax_unavailable": "Daň z poplatku za pokoj není k dispozici. Zkontrolujte nastavení daně.", + "lang_staff_room_orders": "Povolit objednávky místností zaměstnanců", + "lang_staff_room_prices": "Objednávky pokojů pro zaměstnance používají běžné ceny menu. Značení webových nabídek a provize místa konání jsou oddělené.", + "lang_room_charge_offer": "Nabídka za příplatek za pokoj", + "lang_charge_name": "Název poplatku", + "lang_apply_default_tax": "Použít výchozí daň", + "lang_room_charge_basis": "Procento potravin po slevách, před zdaněním. Přihlaste se pouze se souhlasem hosta; zaměstnanci jej mohou odstranit z účtu.", + "lang_room_charge_rate": "{0}: {1}% potravin po slevách, před zdaněním.", + "lang_room_charge_final": "Zahrnuto na konečném účtu.", + "lang_room_orders_enable_help": "Povolte objednávky pokojů zaměstnanců v Nastavení „Restaurace“ — Hotely a dalších místech.", + "lang_room_venue": "Místo konání" } diff --git a/languages/da.json b/languages/da.json index 9ad95c58c..f8be5a88f 100644 --- a/languages/da.json +++ b/languages/da.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Tekstformat", "lang_this_product_is_hidden_from_sales_your_bas": "Dette produkt er skjult for salg. Din kurv er ikke blevet ændret.", "lang_this_window_belongs_to_one_branch_open_ano": "Dette vindue tilhører en gren. Åbn en anden stikkontakt fra hovedvinduet.", - "lang_total_labels": "Etiketter i alt" + "lang_total_labels": "Etiketter i alt", + "lang_rooms": "Værelser", + "lang_room_number": "Værelsesnummer", + "lang_room_order": "Værelsesordre", + "lang_room_service_charge": "Roomservice gebyr", + "lang_room_charge_agreement": "Gæsten accepterede dette tillægsgebyr", + "lang_room_destination_required": "Vælg et sted og indtast værelsesnummeret.", + "lang_room_charge_tax_unavailable": "Værelsesafgiften er ikke tilgængelig. Gennemgå skatteindstillingerne.", + "lang_staff_room_orders": "Tillad ordrer på personalelokaler", + "lang_staff_room_prices": "Personaleordrer bruger normale menupriser. Webmenuopmærkning og stedskommission er separate.", + "lang_room_charge_offer": "Tilbyd et ekstra værelsesgebyr", + "lang_charge_name": "Opladningsnavn", + "lang_apply_default_tax": "Anvend standardskat", + "lang_room_charge_basis": "Procentdel af mad efter rabatter, før skat. Ansøg kun med gæsteaftale; personalet kan fjerne det fra regningen.", + "lang_room_charge_rate": "{0}: {1}% af mad efter rabatter, før skat.", + "lang_room_charge_final": "Inkluderet på den endelige regning.", + "lang_room_orders_enable_help": "Aktivér bestilling af medarbejderværelser i Indstillinger → Restaurant → Hoteller og andre spillesteder.", + "lang_room_venue": "Sted" } diff --git a/languages/de.json b/languages/de.json index a1b761dfd..e921050a6 100644 --- a/languages/de.json +++ b/languages/de.json @@ -5001,5 +5001,22 @@ "lang_text_style": "Stil des Textes", "lang_this_product_is_hidden_from_sales_your_bas": "Dieses Produkt ist vor Verkäufen verborgen. Ihr Warenkorb wurde nicht geändert.", "lang_this_window_belongs_to_one_branch_open_ano": "Dieses Fenster gehört zu einer Filiale. Öffnen Sie eine weitere Steckdose vom Hauptfenster aus.", - "lang_total_labels": "Gesamtzahl der Beschriftungen" + "lang_total_labels": "Gesamtzahl der Beschriftungen", + "lang_rooms": "Zimmer", + "lang_room_number": "Nummer des Zimmers", + "lang_room_order": "Bestellung des Zimmers", + "lang_room_service_charge": "Gebühr für den Zimmerservice", + "lang_room_charge_agreement": "Der Gast stimmte dieser zusätzlichen Gebühr zu", + "lang_room_destination_required": "Wählen Sie einen Veranstaltungsort und geben Sie die Zimmernummer ein.", + "lang_room_charge_tax_unavailable": "Die Zimmersteuer ist nicht verfügbar. Überprüfen Sie die Steuereinstellungen.", + "lang_staff_room_orders": "Erlaube Zimmerbestellungen für Mitarbeiter", + "lang_staff_room_prices": "Für Zimmerbestellungen für das Personal gelten die normalen Menüpreise. Das Aufschlag für das Webmenü und die Provision für den Veranstaltungsort sind getrennt", + "lang_room_charge_offer": "Bieten Sie eine zusätzliche Zimmergebühr an", + "lang_charge_name": "Name der Gebühr", + "lang_apply_default_tax": "Standardsteuer anwenden", + "lang_room_charge_basis": "Prozentsatz der Lebensmittel nach Abzug der Rabatte, vor Steuern. Bewerben Sie sich nur mit Zustimmung des Gastes; das Personal kann es von der Rechnung streichen.", + "lang_room_charge_rate": "{0}: {1}% der Speisen nach Abzug der Rabatte, vor Steuern.", + "lang_room_charge_final": "In der Endabrechnung enthalten.", + "lang_room_orders_enable_help": "Aktiviere Zimmerbestellungen für Mitarbeiter in den Einstellungen „Restaurant“, Hotels und andere Veranstaltungsorte.", + "lang_room_venue": "Veranstaltungsort" } diff --git a/languages/el.json b/languages/el.json index 14baea32b..d9746cab7 100644 --- a/languages/el.json +++ b/languages/el.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Στυλ κειμένου", "lang_this_product_is_hidden_from_sales_your_bas": "Αυτό το προϊόν είναι κρυμμένο από τις πωλήσεις. Το καλάθι σας δεν έχει αλλάξει.", "lang_this_window_belongs_to_one_branch_open_ano": "Αυτό το παράθυρο ανήκει σε ένα υποκατάστημα. Ανοίξτε μια άλλη πρίζα από το κύριο παράθυρο.", - "lang_total_labels": "Συνολικές ετικέτες" + "lang_total_labels": "Συνολικές ετικέτες", + "lang_rooms": "ΔΩΜΑΤΙΑ", + "lang_room_number": "Αριθμός Δωματίου", + "lang_room_order": "Παραγγελία δωματίων", + "lang_room_service_charge": "Χρέωση υπηρεσίας δωματίου", + "lang_room_charge_agreement": "Ο επισκέπτης συμφώνησε με αυτήν την πρόσθετη χρέωση", + "lang_room_destination_required": "Επιλέξτε έναν χώρο και εισαγάγετε τον αριθμό δωματίου.", + "lang_room_charge_tax_unavailable": "Ο φόρος χρέωσης δωματίου δεν είναι διαθέσιμος. Ελέγξτε τις ρυθμίσεις φόρου.", + "lang_staff_room_orders": "Επιτρέψτε παραγγελίες δωματίου προσωπικού", + "lang_staff_room_prices": "παραγγελίες δωματίου προσωπικού χρησιμοποιούν κανονικές τιμές μενού. Η σήμανση μενού Ιστού και η προμήθεια χώρου είναι ξεχωριστές.", + "lang_room_charge_offer": "Προσφέρετε επιπλέον χρέωση δωματίου", + "lang_charge_name": "Όνομα χρέωσης", + "lang_apply_default_tax": "Εφαρμογή προεπιλεγμένου φόρου", + "lang_room_charge_basis": "Ποσοστό τροφίμων μετά από εκπτώσεις, πριν από φόρους. Εφαρμόστε μόνο με συμφωνία επισκέπτη. το προσωπικό μπορεί να το αφαιρέσει από το λογαριασμό.", + "lang_room_charge_rate": "{0}: {1}% των τροφίμων μετά από εκπτώσεις, πριν από φόρους.", + "lang_room_charge_final": "Περιλαμβάνεται στον τελικό λογαριασμό.", + "lang_room_orders_enable_help": "Ενεργοποιήστε παραγγελίες δωματίων προσωπικού στις Ρυθμίσεις «Εστιατόριο»» Ξενοδοχεία και άλλους χώρους.", + "lang_room_venue": "Τόπος διεξαγωγής" } diff --git a/languages/es.json b/languages/es.json index b5c329c55..af599155f 100644 --- a/languages/es.json +++ b/languages/es.json @@ -4997,5 +4997,22 @@ "lang_text_style": "Estilo de texto", "lang_this_product_is_hidden_from_sales_your_bas": "Este producto está oculto para las ventas. Tu cesta no se ha modificado.", "lang_this_window_belongs_to_one_branch_open_ano": "Esta ventana pertenece a una sucursal. Abra otra salida desde la ventana principal.", - "lang_total_labels": "Total de etiquetas" + "lang_total_labels": "Total de etiquetas", + "lang_rooms": "Habitaciones", + "lang_room_number": "Número de habitación", + "lang_room_order": "Orden de habitaciones", + "lang_room_service_charge": "Cargo por servicio de habitaciones", + "lang_room_charge_agreement": "El huésped aceptó este cargo adicional", + "lang_room_destination_required": "Elija un lugar e introduzca el número de habitación.", + "lang_room_charge_tax_unavailable": "El impuesto sobre el precio de la habitación no está disponible. Revisa la configuración de impuestos.", + "lang_staff_room_orders": "Permita los pedidos de habitaciones para el personal", + "lang_staff_room_prices": "Los pedidos de habitaciones para el personal utilizan los precios normales de los menús. El precio del menú web y la comisión del local son independientes", + "lang_room_charge_offer": "Ofrecen un cargo adicional por habitación", + "lang_charge_name": "Nombre del cargo", + "lang_apply_default_tax": "Aplica el impuesto predeterminado", + "lang_room_charge_basis": "Porcentaje de comida después de los descuentos, antes de impuestos. Solo se aplica con el acuerdo del huésped; el personal puede eliminarlo de la factura.", + "lang_room_charge_rate": "{0}: {1}% de comida después de los descuentos, antes de impuestos.", + "lang_room_charge_final": "Incluido en la factura final.", + "lang_room_orders_enable_help": "Activa los pedidos de habitaciones para el personal en Ajustes →, Restaurante, Hoteles y otros establecimientos.", + "lang_room_venue": "Lugar" } diff --git a/languages/et.json b/languages/et.json index 999a399d3..03cd86e84 100644 --- a/languages/et.json +++ b/languages/et.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Tekstiil", "lang_this_product_is_hidden_from_sales_your_bas": "See toode on müügi eest varjatud. Teie ostukorvi pole muudetud.", "lang_this_window_belongs_to_one_branch_open_ano": "See aken kuulub ühele harule. Avage peaaknast teine väljalaskeava.", - "lang_total_labels": "Sildid kokku" + "lang_total_labels": "Sildid kokku", + "lang_rooms": "Toad", + "lang_room_number": "Toa number", + "lang_room_order": "Ruumide järjekord", + "lang_room_service_charge": "Toateeninduse tasu", + "lang_room_charge_agreement": "Külaline nõustus selle lisatasuga", + "lang_room_destination_required": "Valige koht ja sisestage toa number.", + "lang_room_charge_tax_unavailable": "Toatasu maks pole saadaval. Vaadake üle maksuseaded.", + "lang_staff_room_orders": "Luba töötajate ruumide tellimusi", + "lang_staff_room_prices": "Töötajate ruumide tellimisel kasutatakse tavalisi menüühindu. Veebimenüü märgistus ja toimumiskoha komisjon on eraldi", + "lang_room_charge_offer": "Paku lisatasu eest", + "lang_charge_name": "Tasu nimi", + "lang_apply_default_tax": "Rakenda vaikimaksu", + "lang_room_charge_basis": "Toidu protsent pärast allahindlusi, enne maksustamist. Kandideerige ainult külalise kokkuleppel; töötajad saavad selle arvelt eemaldada.", + "lang_room_charge_rate": "{0}: {1}% toidust pärast allahindlusi, enne maksustamist.", + "lang_room_charge_final": "Lisatud lõplikule arvele.", + "lang_room_orders_enable_help": "Luba töötajate ruumide tellimused jaotises Seaded → Restoran → Hotellid ja muud paigad.", + "lang_room_venue": "Toimumiskoht" } diff --git a/languages/fa.json b/languages/fa.json index 94ea230d5..8ff9f1f47 100644 --- a/languages/fa.json +++ b/languages/fa.json @@ -4920,5 +4920,22 @@ "lang_text_style": "سبک متن", "lang_this_product_is_hidden_from_sales_your_bas": "این محصول از فروش پنهان است. سبد خرید شما تغییر نکرده است.", "lang_this_window_belongs_to_one_branch_open_ano": "این پنجره متعلق به یک شاخه است. یک خروجی دیگر را از پنجره اصلی باز کنید.", - "lang_total_labels": "مجموع برچسب ها" + "lang_total_labels": "مجموع برچسب ها", + "lang_rooms": "اتاقها", + "lang_room_number": "شماره اتاق", + "lang_room_order": "سفارش اتاق", + "lang_room_service_charge": "هزینه خدمات اتاق", + "lang_room_charge_agreement": "مهمان با این هزینه اضافی موافقت کرد", + "lang_room_destination_required": "محل برگزاری را انتخاب کنید و شماره اتاق را وارد کنید.", + "lang_room_charge_tax_unavailable": "مالیات هزینه اتاق در دسترس نیست. تنظیمات مالیاتی را مرور کنید.", + "lang_staff_room_orders": "اجازه سفارشات اتاق کارکنان", + "lang_staff_room_prices": "سفارشات اتاق کارکنان از قیمت منوی معمولی استفاده می کنند. نشانه گذاری منوی وب و کمیسیون محل جداگانه هستند.", + "lang_room_charge_offer": "هزینه اتاق اضافی ارائه دهید", + "lang_charge_name": "نام شارژ", + "lang_apply_default_tax": "اعمال مالیات پیش فرض", + "lang_room_charge_basis": "درصد مواد غذایی پس از تخفیف، قبل از مالیات. فقط با موافقت مهمان درخواست کنید؛ کارکنان می توانند آن را از صورتحساب حذف کنند.", + "lang_room_charge_rate": "{0}: {1} درصد مواد غذایی پس از تخفیف، قبل از مالیات.", + "lang_room_charge_final": "در صورتحساب نهایی گنجانده شده است.", + "lang_room_orders_enable_help": "سفارشات اتاق کارکنان را در تنظیمات «رستوران» هتل ها و سایر مکان ها فعال کنید.", + "lang_room_venue": "محل برگزاری" } diff --git a/languages/fi.json b/languages/fi.json index 52da66acb..10f402999 100644 --- a/languages/fi.json +++ b/languages/fi.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Tekstityyli", "lang_this_product_is_hidden_from_sales_your_bas": "Tämä tuote on piilotettu myynnistä. Ostoskoriasi ei ole vaihdettu.", "lang_this_window_belongs_to_one_branch_open_ano": "Tämä ikkuna kuuluu yhteen haaraan. Avaa toinen pistorasia pääikkunasta.", - "lang_total_labels": "Etiketit yhteensä" + "lang_total_labels": "Etiketit yhteensä", + "lang_rooms": "Huoneet", + "lang_room_number": "Huoneen numero", + "lang_room_order": "Huonejärjestys", + "lang_room_service_charge": "Huonepalvelumaksu", + "lang_room_charge_agreement": "Asiakas hyväksyi tämän lisämaksun", + "lang_room_destination_required": "Valitse paikka ja kirjoita huoneen numero.", + "lang_room_charge_tax_unavailable": "Huonemaksuvero ei ole käytettävissä. Tarkista veroasetukset.", + "lang_staff_room_orders": "Salli henkilökunnan huonetilojen tilaukset", + "lang_staff_room_prices": "Henkilökunnan huonetilaukset käyttävät normaaleja valikkohintoja. Web-valikkomerkintä ja tapahtumapalvelu ovat erillisiä.", + "lang_room_charge_offer": "Tarjoa ylimääräinen huonemaksu", + "lang_charge_name": "Maksun nimi", + "lang_apply_default_tax": "Käytä oletusveroa", + "lang_room_charge_basis": "Ruoan prosenttiosuus alennusten jälkeen, ennen veroja. Hae vain vierassopimuksella; henkilökunta voi poistaa sen laskulta.", + "lang_room_charge_rate": "{0}: {1}% elintarvikkeista alennusten jälkeen, ennen veroja.", + "lang_room_charge_final": "Sisältyy lopulliseen laskuun.", + "lang_room_orders_enable_help": "Ota henkilökunnan huonetilojen tilaukset käyttöön Asetuksissa ”Ravintola” Hotelleissa ja muissa tiloissa.", + "lang_room_venue": "Tapahtumapaikka" } diff --git a/languages/fr.json b/languages/fr.json index a60a4d7ad..875894e32 100644 --- a/languages/fr.json +++ b/languages/fr.json @@ -5001,5 +5001,22 @@ "lang_text_style": "Style de texte", "lang_this_product_is_hidden_from_sales_your_bas": "Ce produit est masqué des ventes. Votre panier n'a pas été modifié.", "lang_this_window_belongs_to_one_branch_open_ano": "Cette fenêtre appartient à une branche. Ouvrez une autre prise depuis la fenêtre principale.", - "lang_total_labels": "Nombre total d'étiquettes" + "lang_total_labels": "Nombre total d'étiquettes", + "lang_rooms": "Chambres", + "lang_room_number": "Numéro de chambre", + "lang_room_order": "Ordre des chambres", + "lang_room_service_charge": "Frais de service de chambre", + "lang_room_charge_agreement": "Le client a accepté ces frais supplémentaires", + "lang_room_destination_required": "Choisissez un lieu et entrez le numéro de la chambre.", + "lang_room_charge_tax_unavailable": "La taxe de séjour n'est pas disponible. Vérifiez les paramètres fiscaux.", + "lang_staff_room_orders": "Autoriser les commandes de chambres pour le personnel", + "lang_staff_room_prices": "chambres pour le personnel utilisent les prix normaux des menus. Le balisage du menu Web et la commission du lieu sont distincts", + "lang_room_charge_offer": "Proposez un supplément de chambre", + "lang_charge_name": "Nom de la charge", + "lang_apply_default_tax": "Appliquer la taxe par défaut", + "lang_room_charge_basis": "Pourcentage de nourriture après remises, avant taxes. Postulez uniquement avec l'accord du client ; le personnel peut le supprimer de la facture.", + "lang_room_charge_rate": "{0} : {1} % de la nourriture après les remises, avant taxes.", + "lang_room_charge_final": "Inclus dans la facture finale.", + "lang_room_orders_enable_help": "Activez les commandes de chambres pour le personnel dans les paramètres « Restaurant », « Hôtels » et autres lieux.", + "lang_room_venue": "Lieu" } diff --git a/languages/ga.json b/languages/ga.json index b87d32962..65654e80f 100644 --- a/languages/ga.json +++ b/languages/ga.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Stíl téacs", "lang_this_product_is_hidden_from_sales_your_bas": "Tá an táirge seo i bhfolach ó dhíolacháin. Níor athraíodh do chiseán.", "lang_this_window_belongs_to_one_branch_open_ano": "Baineann an fhuinneog seo le brainse amháin. Oscail asraon eile ón bpríomhfhuinneog.", - "lang_total_labels": "Lipéid iomlána" + "lang_total_labels": "Lipéid iomlána", + "lang_rooms": "Seomraí", + "lang_room_number": "Uimhir an tseomra", + "lang_room_order": "Ordú seomra", + "lang_room_service_charge": "Táille seirbhíse seomra", + "lang_room_charge_agreement": "D'aontaigh aoi leis an táille bhreise seo", + "lang_room_destination_required": "Roghnaigh ionad agus cuir isteach uimhir an tseomra.", + "lang_room_charge_tax_unavailable": "Níl an cháin muirear seomra ar fáil. Athbhreithnigh na socruithe cánach", + "lang_staff_room_orders": "Lig orduithe seomra foirne", + "lang_staff_room_prices": "Úsáideann orduithe seomra foirne gnáth-phraghsanna Tá marcáil roghchlár gréasáin agus coimisiún ionaid ar leithligh.", + "lang_room_charge_offer": "Táille seomra breise a thairiscint", + "lang_charge_name": "Ainm muirear", + "lang_apply_default_tax": "Cuir cáin réamhshocraithe", + "lang_room_charge_basis": "Céatadán bia tar éis lascainí, roimh cháin. Déan iarratas ach amháin le comhaontú aoi; is féidir leis an bhfoireann é a bhaint as an mbille.", + "lang_room_charge_rate": "{0}: {1}% de bhia tar éis lascainí, roimh cháin.", + "lang_room_charge_final": "San áireamh ar an mbille deiridh.", + "lang_room_orders_enable_help": "Cumasaigh orduithe seomra foirne i Socruithe → Bialann → Óstáin agus ionaid eile.", + "lang_room_venue": "Ionad" } diff --git a/languages/gu.json b/languages/gu.json index e0fb445ba..ea9ccaaee 100644 --- a/languages/gu.json +++ b/languages/gu.json @@ -4923,5 +4923,22 @@ "lang_text_style": "ટેક્સ્ટ શૈલી", "lang_this_product_is_hidden_from_sales_your_bas": "આ ઉત્પાદન વેચાણથી છુપાયેલું છે. તમારી ટોપલી બદલવામાં આવી નથી.", "lang_this_window_belongs_to_one_branch_open_ano": "આ વિંડો એક શાખાની છે. મુખ્ય વિંડોમાંથી બીજું આઉટલેટ ખોલો.", - "lang_total_labels": "કુલ લેબલ્સ" + "lang_total_labels": "કુલ લેબલ્સ", + "lang_rooms": "રૂમ", + "lang_room_number": "રૂમ નંબર", + "lang_room_order": "રૂમ ઓર્ડર", + "lang_room_service_charge": "રૂમ સર્વિસ ચાર્જ", + "lang_room_charge_agreement": "મહેમાન આ વધારાના ચાર્જ માટે સંમત થયા", + "lang_room_destination_required": "સ્થળ પસંદ કરો અને રૂમ નંબર દાખલ કરો.", + "lang_room_charge_tax_unavailable": "રૂમ ચાર્જ ટેક્સ ઉપલબ્ધ નથી. કર સેટિંગ્સની સમીક્ષા કરો.", + "lang_staff_room_orders": "સ્ટાફ રૂમ ઓર્ડરની મંજૂરી આપો", + "lang_staff_room_prices": "સ્ટાફ રૂમ ઓર્ડર સામાન્ય મેનુ ભાવોનો ઉપયોગ કરે છે. વેબ મેનૂ માર્કઅપ અને સ્થળ કમિશન અલગ છે", + "lang_room_charge_offer": "વધારાનો રૂમ ચાર્જ ઓફર કરો", + "lang_charge_name": "ચાર્જ નામ", + "lang_apply_default_tax": "ડિફૉલ્ટ ટેક્સ લાગુ કરો", + "lang_room_charge_basis": "ક્સ પહેલાં, ડિસ્કાઉન્ટ પછી ખોરાકની ટકાવારી. ફક્ત મહેમાન કરાર સાથે જ અરજી કરો; સ્ટાફ તેને બિલમાંથી દૂર કરી શકે છે.", + "lang_room_charge_rate": "{0}: ટેક્સ પહેલાં, ડિસ્કાઉન્ટ પછી ખોરાકનો {1}%.", + "lang_room_charge_final": "અંતિમ બિલ પર શામેલ છે.", + "lang_room_orders_enable_help": "સેટિંગ્સ → રેસ્ટોરન્ટ → હોટેલ્સ અને અન્ય સ્થળોમાં સ્ટાફ રૂમ ઓર્ડર સક્ષમ કરો.", + "lang_room_venue": "સ્થળ" } diff --git a/languages/he.json b/languages/he.json index 1468f5d32..f4fc58f25 100644 --- a/languages/he.json +++ b/languages/he.json @@ -4920,5 +4920,22 @@ "lang_text_style": "סגנון טקסט", "lang_this_product_is_hidden_from_sales_your_bas": "מוצר זה מוסתר ממכירות. הסל שלך לא השתנה.", "lang_this_window_belongs_to_one_branch_open_ano": "חלון זה שייך לענף אחד. פתח שקע נוסף מהחלון הראשי.", - "lang_total_labels": "סה\"כ תוויות" + "lang_total_labels": "סה\"כ תוויות", + "lang_rooms": "חדרים", + "lang_room_number": "מספר חדר", + "lang_room_order": "סדר חדר", + "lang_room_service_charge": "דמי שירות חדרים", + "lang_room_charge_agreement": "האורח הסכים לתשלום נוסף זה", + "lang_room_destination_required": "בחר מקום והזן את מספר החדר.", + "lang_room_charge_tax_unavailable": "מס דמי החדר אינו זמין. עיין בהגדרות המס.", + "lang_staff_room_orders": "אפשר הזמנות חדר צוות", + "lang_staff_room_prices": "הזמנות חדרי צוות משתמשות במחירי תפריט רגילים. סימון תפריט האינטרנט ועמלת המקום נפרדים.", + "lang_room_charge_offer": "הצעת תשלום נוסף לחדר", + "lang_charge_name": "שם חיוב", + "lang_apply_default_tax": "החלת מס ברירת מחדל", + "lang_room_charge_basis": "אחוז מזון לאחר הנחות, לפני מס. הגש בקשה רק בהסכם אורח; הצוות יכול להסיר אותו מהחשבון.", + "lang_room_charge_rate": "{0}: {1}% של מזון לאחר הנחות, לפני מס.", + "lang_room_charge_final": "כלול על הצעת החוק הסופית.", + "lang_room_orders_enable_help": "אפשר הזמנות חדרי צוות בהגדרות \"מסעדה\" \"מלונות ומקומות אחרים.", + "lang_room_venue": "מקום" } diff --git a/languages/hi.json b/languages/hi.json index 36be3bbcc..1ee10549b 100644 --- a/languages/hi.json +++ b/languages/hi.json @@ -5001,5 +5001,22 @@ "lang_text_style": "टेक्स्ट स्टाइल", "lang_this_product_is_hidden_from_sales_your_bas": "यह उत्पाद बिक्री से छिपा हुआ है। आपका बास्केट बदला नहीं गया है।", "lang_this_window_belongs_to_one_branch_open_ano": "यह विंडो एक शाखा की है। मुख्य विंडो से दूसरा आउटलेट खोलें।", - "lang_total_labels": "कुल लेबल" + "lang_total_labels": "कुल लेबल", + "lang_rooms": "कमरे", + "lang_room_number": "रूम नंबर", + "lang_room_order": "रूम ऑर्डर", + "lang_room_service_charge": "रूम सर्विस चार्ज", + "lang_room_charge_agreement": "अतिथि ने इस अतिरिक्त शुल्क के लिए सहमति दी", + "lang_room_destination_required": "एक स्थान चुनें और कमरा नंबर दर्ज करें.", + "lang_room_charge_tax_unavailable": "रूम चार्ज टैक्स उपलब्ध नहीं है। टैक्स सेटिंग की समीक्षा", + "lang_staff_room_orders": "स्टाफ़ रूम ऑर्डर की अनुमति दें.", + "lang_staff_room_prices": "स्टाफ़ रूम के ऑर्डर में मेनू की सामान्य कीमतों का उपयोग किया जाता है। वेब मेनू मार्कअप और वेन्यू कमीशन अलग-अलग हैं", + "lang_room_charge_offer": "रूम का अतिरिक्त शुल्क दें.", + "lang_charge_name": "शुल्क का नाम", + "lang_apply_default_tax": "डिफ़ॉल्ट टैक्स लागू करें", + "lang_room_charge_basis": "छूट के बाद खाने का प्रतिशत, टैक्स से पहले। केवल अतिथि अनुबंध के साथ आवेदन करें; कर्मचारी इसे बिल से हटा सकते हैं.", + "lang_room_charge_rate": "{0}: छूट के बाद भोजन का {1}%, टैक्स से पहले.", + "lang_room_charge_final": "अंतिम बिल में शामिल है.", + "lang_room_orders_enable_help": "होटल और अन्य स्थानों की सेटिंग में स्टाफ़ रूम ऑर्डर सक्षम करें.", + "lang_room_venue": "वेन्यू" } diff --git a/languages/hr.json b/languages/hr.json index 604d46052..25c782e18 100644 --- a/languages/hr.json +++ b/languages/hr.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Stil teksta", "lang_this_product_is_hidden_from_sales_your_bas": "Ovaj proizvod je skriven od prodaje. Vaša košarica nije promijenjena.", "lang_this_window_belongs_to_one_branch_open_ano": "Ovaj prozor pripada jednoj grani. Otvorite drugu utičnicu iz glavnog prozora.", - "lang_total_labels": "Ukupno oznaka" + "lang_total_labels": "Ukupno oznaka", + "lang_rooms": "Sobe", + "lang_room_number": "Broj sobe", + "lang_room_order": "Redoslijed sobe", + "lang_room_service_charge": "Naknada za poslugu u sobu", + "lang_room_charge_agreement": "Gost je pristao na ovu dodatnu naknadu", + "lang_room_destination_required": "Odaberite mjesto i unesite broj sobe.", + "lang_room_charge_tax_unavailable": "Porez na naknadu za sobu nije dostupan. Pregledajte porezne postavke.", + "lang_staff_room_orders": "Dopustite narudžbe soba osoblja", + "lang_staff_room_prices": "udžbe soba osoblja koriste uobičajene cijene jelovnika. Oznaka web izbornika i provizija mjesta su odvojeni", + "lang_room_charge_offer": "Ponudite dodatnu naknadu za sobu", + "lang_charge_name": "Naziv naplate", + "lang_apply_default_tax": "Primijeni zadani porez", + "lang_room_charge_basis": "Postotak hrane nakon popusta, prije oporezivanja. Prijavite se samo uz dogovor gosta; osoblje ga može ukloniti s računa.", + "lang_room_charge_rate": "{0}: {1}% hrane nakon popusta, prije oporezivanja.", + "lang_room_charge_final": "Uključeno u konačni račun.", + "lang_room_orders_enable_help": "Omogućite naručivanje soba osoblja u Postavkama → Restoran → Hoteli i druga mjesta.", + "lang_room_venue": "Mjesto održavanja" } diff --git a/languages/hu.json b/languages/hu.json index 94692aa55..918e6170b 100644 --- a/languages/hu.json +++ b/languages/hu.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Szövegstílus", "lang_this_product_is_hidden_from_sales_your_bas": "Ez a termék elrejtve van az értékesítés elől. A kosarat nem változtatták meg.", "lang_this_window_belongs_to_one_branch_open_ano": "Ez az ablak egy ághoz tartozik. Nyisson ki egy másik aljzatot a főablakból.", - "lang_total_labels": "Összes címke" + "lang_total_labels": "Összes címke", + "lang_rooms": "Szobák", + "lang_room_number": "Szoba száma", + "lang_room_order": "Szobaszerrend", + "lang_room_service_charge": "Szobaszerviz díja", + "lang_room_charge_agreement": "A vendég elfogadta ezt a felár ellenértékét", + "lang_room_destination_required": "Válasszon helyszínt, és adja meg a szoba számát.", + "lang_room_charge_tax_unavailable": "A szobadíj adója nem áll rendelkezésre. Tekintse át az adóbeállításokat.", + "lang_staff_room_orders": "Engedélyezze a személyzet helyiség", + "lang_staff_room_prices": "személyzet szobai rendelései a normál menüárakat használják. A webmenü jelölése és a helyszíni jutalék különálló", + "lang_room_charge_offer": "Felár ellenében szobafizettek", + "lang_charge_name": "Díjnév", + "lang_apply_default_tax": "Alapértelmezett adó alkalmazása", + "lang_room_charge_basis": "élelmiszer százalékos aránya kedvezmények után, adózás előtt. Csak vendégmegállapodással jelentkezhet; a személyzet eltávolíthatja azt a számlából.", + "lang_room_charge_rate": "{0}: Az élelmiszer {1}% -a kedvezmény után, adózás előtt.", + "lang_room_charge_final": "A végső számlán szerepel.", + "lang_room_orders_enable_help": "Engedélyezze a személyzeti szobarendezést a Beállítások → Étterem → Hotelek és egyéb helyszínek menüpontban.", + "lang_room_venue": "Helyszín" } diff --git a/languages/id.json b/languages/id.json index d6c08db6b..96b4de01c 100644 --- a/languages/id.json +++ b/languages/id.json @@ -5001,5 +5001,22 @@ "lang_text_style": "Gaya teks", "lang_this_product_is_hidden_from_sales_your_bas": "Produk ini tersembunyi dari penjualan. Keranjang Anda belum diubah.", "lang_this_window_belongs_to_one_branch_open_ano": "Jendela ini milik satu cabang. Buka outlet lain dari jendela utama.", - "lang_total_labels": "Total label" + "lang_total_labels": "Total label", + "lang_rooms": "Kamar", + "lang_room_number": "Nomor kamar", + "lang_room_order": "Pesanan kamar", + "lang_room_service_charge": "Biaya layanan kamar", + "lang_room_charge_agreement": "Tamu menyetujui biaya tambahan ini", + "lang_room_destination_required": "Pilih tempat dan masukkan nomor kamar.", + "lang_room_charge_tax_unavailable": "Pajak biaya kamar tidak tersedia. Tinjau pengaturan pajak.", + "lang_staff_room_orders": "Izinkan pesanan ruang staf", + "lang_staff_room_prices": "Pesanan kamar staf menggunakan harga menu normal. Markup menu web dan komisi venue terpisah.", + "lang_room_charge_offer": "Menawarkan biaya kamar tambahan", + "lang_charge_name": "Nama tagihan", + "lang_apply_default_tax": "Terapkan pajak default", + "lang_room_charge_basis": "Persentase makanan setelah diskon, sebelum pajak. Terapkan hanya dengan persetujuan tamu; staf dapat menghapusnya dari tagihan.", + "lang_room_charge_rate": "{0}: {1}% makanan setelah diskon, sebelum pajak.", + "lang_room_charge_final": "Termasuk pada tagihan akhir.", + "lang_room_orders_enable_help": "Aktifkan pemesanan kamar staf di Pengaturan “Restoran” Hotel dan tempat lainnya.", + "lang_room_venue": "Tempat" } diff --git a/languages/is.json b/languages/is.json index 855c5d482..9c2c58cfd 100644 --- a/languages/is.json +++ b/languages/is.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Textastíll", "lang_this_product_is_hidden_from_sales_your_bas": "Þessi vara er falin fyrir sölu. Körfunni hefur ekki verið breytt.", "lang_this_window_belongs_to_one_branch_open_ano": "Þessi gluggi tilheyrir einni grein. Opnaðu aðra innstungu frá aðalglugganum.", - "lang_total_labels": "Heildarmerki" + "lang_total_labels": "Heildarmerki", + "lang_rooms": "Herbergin", + "lang_room_number": "Herbergisnúmer", + "lang_room_order": "Herbergisröð", + "lang_room_service_charge": "Herbergisþjónustugjald", + "lang_room_charge_agreement": "Gestur samþykkti þetta aukagjald", + "lang_room_destination_required": "Veldu staðsetningu og sláðu inn herbergisnúmerið.", + "lang_room_charge_tax_unavailable": "Herbergisgjaldaskattur er ekki tiltækur. Farið yfir skattstillingarnar.", + "lang_staff_room_orders": "Leyfa pantanir á herbergjum starfsmanna", + "lang_staff_room_prices": "Starfsmannaherbergispantanir nota venjulegt matseðilverð. Merking á vefmatseðli og þóknun á vettvangi eru aðskil", + "lang_room_charge_offer": "Bjóða upp á viðbótarherbergisgjald", + "lang_charge_name": "Heiti hleðslu", + "lang_apply_default_tax": "Beita sjálfgefnum skatti", + "lang_room_charge_basis": "Hlutfall matar eftir afslætti, fyrir skatta. Sækja aðeins um með samkomulagi gesta; starfsfólk getur fjarlægt það úr frumvarpinu.", + "lang_room_charge_rate": "{0}: {1}% af mat eftir afslætti, fyrir skatta.", + "lang_room_charge_final": "Innifalið í endanlegu frumvarpinu.", + "lang_room_orders_enable_help": "Virkja herbergjapantanir starfsfólks í Stillingar → Veitingastaður → Hótel og aðrir staðir.", + "lang_room_venue": "Vettvangur" } diff --git a/languages/it.json b/languages/it.json index 9097d699b..126812725 100644 --- a/languages/it.json +++ b/languages/it.json @@ -5001,5 +5001,22 @@ "lang_text_style": "Stile del testo", "lang_this_product_is_hidden_from_sales_your_bas": "Questo prodotto è nascosto alle vendite. Il tuo carrello non è stato modificato.", "lang_this_window_belongs_to_one_branch_open_ano": "Questa finestra appartiene a un ramo. Apri un'altra presa dalla finestra principale.", - "lang_total_labels": "Etichette totali" + "lang_total_labels": "Etichette totali", + "lang_rooms": "Camere", + "lang_room_number": "Numero della camera", + "lang_room_order": "Ordine della camera", + "lang_room_service_charge": "Costo del servizio in camera", + "lang_room_charge_agreement": "L'ospite ha accettato questo costo aggiuntivo", + "lang_room_destination_required": "Scegli una sede e inserisci il numero della camera.", + "lang_room_charge_tax_unavailable": "La tassa sulla camera non è disponibile. Controlla le impostazioni fiscali.", + "lang_staff_room_orders": "Consenti al personale di ordinare le camere", + "lang_staff_room_prices": "Gli ordini delle camere per il personale utilizzano i normali prezzi del menu. Il markup del menu Web e la commissione sulla sede sono separati", + "lang_room_charge_offer": "Offri un costo aggiuntivo per la camera", + "lang_charge_name": "Nome dell'addebito", + "lang_apply_default_tax": "Applica l'imposta predefinita", + "lang_room_charge_basis": "Percentuale di cibo al netto degli sconti, al lordo delle tasse. Richiedila solo previo accordo con il cliente; il personale può rimuoverla dalla fattura.", + "lang_room_charge_rate": "{0}: {1}% del cibo al netto degli sconti, tasse escluse.", + "lang_room_charge_final": "Incluso nella fattura finale.", + "lang_room_orders_enable_help": "Abilita l'ordine delle camere per il personale nelle Impostazioni → Ristorante → Hotel e altre sedi.", + "lang_room_venue": "Sede" } diff --git a/languages/ja.json b/languages/ja.json index 4b63f22f1..cc1d80830 100644 --- a/languages/ja.json +++ b/languages/ja.json @@ -4923,5 +4923,22 @@ "lang_text_style": "テキストスタイル", "lang_this_product_is_hidden_from_sales_your_bas": "この商品は販売対象外です。カートは変更されていません。", "lang_this_window_belongs_to_one_branch_open_ano": "このウィンドウは1つの支店に属しています。メインウィンドウから別のコンセントを開けてください。", - "lang_total_labels": "合計ラベル数" + "lang_total_labels": "合計ラベル数", + "lang_rooms": "部屋", + "lang_room_number": "部屋番号", + "lang_room_order": "ルームオーダー", + "lang_room_service_charge": "ルームサービス料", + "lang_room_charge_agreement": "ゲストはこの追加料金に同意しました。", + "lang_room_destination_required": "会場を選択し、部屋番号を入力します。", + "lang_room_charge_tax_unavailable": "ルームチャージ税はご利用いただけません。税金設定を見直してください。", + "lang_staff_room_orders": "スタッフルームの注文を許可する", + "lang_staff_room_prices": "スタッフルームの注文には通常のメニュー価格が適用されます。ウェブメニューのマークアップと会場のコミッションは別です", + "lang_room_charge_offer": "追加のルームチャージを申し込んでください。", + "lang_charge_name": "チャージ名", + "lang_apply_default_tax": "デフォルト税を適用", + "lang_room_charge_basis": "割引後、税引き前の食品の割合。お客様の同意がある場合のみお申し込みください。スタッフは請求書から削除できます。", + "lang_room_charge_rate": "{0}: 割引後、税抜きのお食事の {1}%", + "lang_room_charge_final": "最終請求書に含まれています。", + "lang_room_orders_enable_help": "設定 → レストラン → のホテルやその他の施設でスタッフルームの注文を有効にします。", + "lang_room_venue": "会場" } diff --git a/languages/kn.json b/languages/kn.json index ef4d8f3f1..0b068fe87 100644 --- a/languages/kn.json +++ b/languages/kn.json @@ -5001,5 +5001,22 @@ "lang_text_style": "ಪಠ್ಯ ಶೈಲಿ", "lang_this_product_is_hidden_from_sales_your_bas": "ಈ ಉತ್ಪನ್ನವನ್ನು ಮಾರಾಟದಿಂದ ಮರೆಮಾಡಲಾಗಿದೆ. ನಿಮ್ಮ ಬುಟ್ಟಿಯನ್ನು ಬದಲಾಯಿಸಲಾಗಿಲ್ಲ.", "lang_this_window_belongs_to_one_branch_open_ano": "ಈ ವಿಂಡೋ ಒಂದು ಶಾಖೆಗೆ ಸೇರಿದೆ. ಮುಖ್ಯ ವಿಂಡೋದಿಂದ ಮತ್ತೊಂದು ಔಟ್ಲೆಟ್ ತೆರೆಯಿರಿ.", - "lang_total_labels": "ಒಟ್ಟು ಲೇಬಲ್ಗಳು" + "lang_total_labels": "ಒಟ್ಟು ಲೇಬಲ್ಗಳು", + "lang_rooms": "ಕೊಠಡಿಗಳು", + "lang_room_number": "ಕೊಠಡಿ ಸಂಖ್ಯೆ", + "lang_room_order": "ಕೊಠಡಿ ಆದೇಶ", + "lang_room_service_charge": "ಕೊಠಡಿ ಸೇವಾ ಶುಲ್ಕ", + "lang_room_charge_agreement": "ಅತಿಥಿ ಈ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕವನ್ನು ಒಪ್ಪಿಕೊಂಡರು", + "lang_room_destination_required": "ಸ್ಥಳವನ್ನು ಆರಿಸಿ ಮತ್ತು ಕೊಠಡಿ ಸಂಖ್ಯೆಯನ್ನು ನಮೂದಿಸಿ.", + "lang_room_charge_tax_unavailable": "ಕೊಠಡಿ ಶುಲ್ಕ ತೆರಿಗೆ ಲಭ್ಯವಿಲ್ಲ. ತೆರಿಗೆ ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ಪರಿಶೀಲಿಸಿ.", + "lang_staff_room_orders": "ಸಿಬ್ಬಂದಿ ಕೊಠಡಿ ಆದೇಶಗಳನ್ನು ಅನುಮತಿಸಿ", + "lang_staff_room_prices": "ಸಿಬ್ಬಂದಿ ಕೊಠಡಿ ಆದೇಶಗಳು ಸಾಮಾನ್ಯ ಮೆನು ಬೆಲೆಗಳನ್ನು ಬಳಸುತ್ತವೆ. ವೆಬ್ ಮೆನು ಮಾರ್ಕ್ಅಪ್ ಮತ್ತು ಸ್ಥಳ ಆಯೋಗವು ಪ್ರತ್ಯೇಕವಾಗಿದೆ", + "lang_room_charge_offer": "ಹೆಚ್ಚುವರಿ ಕೊಠಡಿ ಶುಲ್ಕವನ್ನು ನೀಡಿ", + "lang_charge_name": "ಚಾರ್ಜ್ ಹೆಸರು", + "lang_apply_default_tax": "ಡೀಫಾಲ್ಟ್ ತೆರಿಗೆ ಅನ್ವಯಿಸಿ", + "lang_room_charge_basis": "ರಿಯಾಯಿತಿಗಳ ನಂತರ ಆಹಾರದ ಶೇಕಡಾವಾರು, ತೆರಿಗೆಯ ಮೊದಲು. ಅತಿಥಿ ಒಪ್ಪಂದದೊಂದಿಗೆ ಮಾತ್ರ ಅರ್ಜಿ ಸಲ್ಲಿಸಿ; ಸಿಬ್ಬಂದಿ ಅದನ್ನು ಬಿಲ್ನಿಂದ ತೆಗೆದುಹಾಕಬಹುದು.", + "lang_room_charge_rate": "{0}: {1}% ರಷ್ಟು ಆಹಾರವನ್ನು ರಿಯಾಯಿತಿಯ ನಂತರ, ತೆರಿಗೆಗೆ ಮುಂಚಿತವಾಗಿ.", + "lang_room_charge_final": "ಅಂತಿಮ ಮಸೂದೆಯಲ್ಲಿ ಸೇರಿಸಲಾಗಿದೆ.", + "lang_room_orders_enable_help": "ಸೆಟ್ಟಿಂಗ್ಸ್ → ರೆಸ್ಟೋರೆಂಟ್ → ಹೋಟೆಲ್ಗಳು ಮತ್ತು ಇತರ ಸ್ಥಳಗಳಲ್ಲಿ ಸಿಬ್ಬಂದಿ ಕೊಠಡಿ ಆದೇಶಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ.", + "lang_room_venue": "ಸ್ಥಳ" } diff --git a/languages/ko.json b/languages/ko.json index 21cf291b0..715f1764e 100644 --- a/languages/ko.json +++ b/languages/ko.json @@ -4923,5 +4923,22 @@ "lang_text_style": "텍스트 스타일", "lang_this_product_is_hidden_from_sales_your_bas": "이 제품은 판매되지 않습니다.장바구니가 변경되지 않았습니다.", "lang_this_window_belongs_to_one_branch_open_ano": "이 창은 한 지점에 속합니다.메인 윈도우에서 다른 콘센트를 여세요.", - "lang_total_labels": "전체 라벨" + "lang_total_labels": "전체 라벨", + "lang_rooms": "객실", + "lang_room_number": "방 번호", + "lang_room_order": "객실 주문", + "lang_room_service_charge": "룸 서비스 요금", + "lang_room_charge_agreement": "게스트가 이 추가 요금에 동의했습니다.", + "lang_room_destination_required": "장소를 선택하고 객실 번호를 입력하세요.", + "lang_room_charge_tax_unavailable": "객실 요금은 부과되지 않습니다.세금 설정을 검토하세요", + "lang_staff_room_orders": "직원 객실 주문 허용", + "lang_staff_room_prices": "직원실 주문은 일반 메뉴 가격을 사용합니다.웹 메뉴 마크업과 장소 수수료는 별개입니다", + "lang_room_charge_offer": "추가 객실 요금을 제공하세요.", + "lang_charge_name": "요금 이름", + "lang_apply_default_tax": "기본세 적용", + "lang_room_charge_basis": "할인 후, 세전 식품의 비율.투숙객 동의가 있는 경우에만 신청하세요. 직원이 청구서에서 해당 금액을 삭제할 수 있습니다.", + "lang_room_charge_rate": "{0}: 할인 후 식재료의 {1}% (세금 미포함)", + "lang_room_charge_final": "최종 청구서에 포함되어 있습니다.", + "lang_room_orders_enable_help": "설정 → 레스토랑 → 호텔 및 기타 장소에서 직원 객실 주문을 활성화하세요.", + "lang_room_venue": "장소" } diff --git a/languages/lt.json b/languages/lt.json index c9dc0a6b8..f4d1430bf 100644 --- a/languages/lt.json +++ b/languages/lt.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Teksto stilius", "lang_this_product_is_hidden_from_sales_your_bas": "Šis produktas yra paslėptas nuo pardavimų. Jūsų krepšelis nebuvo pakeistas.", "lang_this_window_belongs_to_one_branch_open_ano": "Šis langas priklauso vienai šakai. Atidarykite kitą lizdą iš pagrindinio lango.", - "lang_total_labels": "Iš viso etikečių" + "lang_total_labels": "Iš viso etikečių", + "lang_rooms": "Kambariai", + "lang_room_number": "Kambario numeris", + "lang_room_order": "Kambarių tvarka", + "lang_room_service_charge": "Kambarių aptarnavimo mokestis", + "lang_room_charge_agreement": "Svečias sutiko su šiuo papildomu mokesčiu", + "lang_room_destination_required": "Pasirinkite vietą ir įveskite kambario numerį.", + "lang_room_charge_tax_unavailable": "Kambario mokesčio mokestis nėra galimas. Peržiūrėkite mokesčių nustatymus.", + "lang_staff_room_orders": "Leisti užsakyti personalo kambarį", + "lang_staff_room_prices": "Darbuotojų kambarių užsakymams taikomos įprastos meniu kainos. Žiniatinklio meniu žymėjimas ir vietos komisija yra atskir", + "lang_room_charge_offer": "Siūlykite papildomą mokestį už kambarį", + "lang_charge_name": "Mokesčio pavadinimas", + "lang_apply_default_tax": "Taikyti numatytąjį mokestį", + "lang_room_charge_basis": "Maisto procentas po nuolaidų, neatskaičius mokesčių. Kreipkitės tik su svečiu susitarimu; darbuotojai gali jį pašalinti iš sąskaitos.", + "lang_room_charge_rate": "{0}: {1}% maisto po nuolaidų, neatskaičius mokesčių.", + "lang_room_charge_final": "Įtraukta į galutinę sąskaitą.", + "lang_room_orders_enable_help": "Įgalinti personalo kambarių užsakymus Nustatymai → Restoranas → Viešbučiai ir kitos vietos.", + "lang_room_venue": "Vieta" } diff --git a/languages/lv.json b/languages/lv.json index a60bfc1d3..a7944ae0b 100644 --- a/languages/lv.json +++ b/languages/lv.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Teksta stils", "lang_this_product_is_hidden_from_sales_your_bas": "Šis produkts ir paslēpts no pārdošanas. Jūsu grozs nav mainīts.", "lang_this_window_belongs_to_one_branch_open_ano": "Šis logs pieder vienai filiālei. No galvenā loga atveriet citu kontaktligzdu.", - "lang_total_labels": "Kopējais etiķešu skaits" + "lang_total_labels": "Kopējais etiķešu skaits", + "lang_rooms": "Istabas", + "lang_room_number": "Istabas numurs", + "lang_room_order": "Istabu kārtība", + "lang_room_service_charge": "Apkalpošanas numurā maksa", + "lang_room_charge_agreement": "Viesis piekrita šai papildmaksai", + "lang_room_destination_required": "Izvēlieties norises vietu un ievadiet numura numuru.", + "lang_room_charge_tax_unavailable": "Numura maksas nodoklis nav pieejams. Pārskatiet nodokļu iestatījumus.", + "lang_staff_room_orders": "Atļaut personāla telpas pasūtījumus", + "lang_staff_room_prices": "Personāla telpu pasūtījumos tiek izmantotas parastās ēdienkartes cenas. Tīmekļa izvēlnes atzīme un norises vietas komisija ir atsevišķ", + "lang_room_charge_offer": "Piedāvājiet par papildu numura maksu", + "lang_charge_name": "Maksas nosaukums", + "lang_apply_default_tax": "Pielietot noklusējuma nodokli", + "lang_room_charge_basis": "Pārtikas procentuālais daudzums pēc atlaidēm pirms nodokļiem. Piesakieties tikai ar viesu vienošanos; darbinieki to var noņemt no rēķina.", + "lang_room_charge_rate": "{0}: {1}% no pārtikas pēc atlaidēm, pirms nodokļiem.", + "lang_room_charge_final": "Iekļauts galīgajā rēķinā.", + "lang_room_orders_enable_help": "Iespējojiet personāla numuru pasūtījumus sadaļā Iestatījumi → Restorāns → Viesnīcas un citās vietās.", + "lang_room_venue": "Norises vieta" } diff --git a/languages/mk.json b/languages/mk.json index 96adb443c..ed5353ba7 100644 --- a/languages/mk.json +++ b/languages/mk.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Стил на текст", "lang_this_product_is_hidden_from_sales_your_bas": "Овој производ е скриен од продажба. Вашата кошница не е променета.", "lang_this_window_belongs_to_one_branch_open_ano": "Овој прозорец припаѓа на една гранка. Отворете друг излез од главниот прозорец.", - "lang_total_labels": "Вкупно етикети" + "lang_total_labels": "Вкупно етикети", + "lang_rooms": "Соби", + "lang_room_number": "Број на соба", + "lang_room_order": "Редослед на соба", + "lang_room_service_charge": "Надоместок за услуга во соба", + "lang_room_charge_agreement": "Гостинот се согласи со оваа дополнителна наплата", + "lang_room_destination_required": "Изберете место и внесете го бројот на собата.", + "lang_room_charge_tax_unavailable": "Данокот за наплата на собата е недостапен. Прегледајте ги даночните поставки.", + "lang_staff_room_orders": "Дозволете нарачки за соба на персоналот", + "lang_staff_room_prices": "чките за соби за персонал користат нормални цени на менито. Означувањето на веб-менито и комисијата за место се одделни", + "lang_room_charge_offer": "Понудете дополнителна наплата за соба", + "lang_charge_name": "Име на полнење", + "lang_apply_default_tax": "Примени стандарден данок", + "lang_room_charge_basis": "Процент на храна по попусти, пред оданочување. Аплицирајте само со договор за гости; персоналот може да го отстрани од сметката.", + "lang_room_charge_rate": "{0}: {1}% од храната по попусти, пред оданочување.", + "lang_room_charge_final": "Вклучено во конечниот предлог.", + "lang_room_orders_enable_help": "Овозможете нарачки за соби на персоналот во Поставки → Ресторан → Хотели и други места.", + "lang_room_venue": "Место" } diff --git a/languages/ml.json b/languages/ml.json index a3360cdac..1ac958ce4 100644 --- a/languages/ml.json +++ b/languages/ml.json @@ -5001,5 +5001,22 @@ "lang_text_style": "ടെക്സ്റ്റ് ശൈലി", "lang_this_product_is_hidden_from_sales_your_bas": "ഈ ഉൽപ്പന്നം വിൽപ്പനയിൽ നിന്ന് മറഞ്ഞിരിക്കുന്നു. നിങ്ങളുടെ ബാസ്ക്കറ്റ് മാറ്റിയിട്ടില്ല.", "lang_this_window_belongs_to_one_branch_open_ano": "ഈ ജാലകം ഒരു ശാഖയുടേതാണ്. പ്രധാന വിൻഡോയിൽ നിന്ന് മറ്റൊരു ഔട്ട്ലെറ്റ് തുറക്കുക.", - "lang_total_labels": "മൊത്തം ലേബലുകൾ" + "lang_total_labels": "മൊത്തം ലേബലുകൾ", + "lang_rooms": "മുറികൾ", + "lang_room_number": "റൂം നമ്പർ", + "lang_room_order": "റൂം ഓർഡർ", + "lang_room_service_charge": "റൂം സർവീസ് ചാർജ്", + "lang_room_charge_agreement": "അതിഥി ഈ അധിക ചാർജ് സമ്മതിച്ചു", + "lang_room_destination_required": "ഒരു വേദി തിരഞ്ഞെടുത്ത് റൂം നമ്പർ നൽകുക.", + "lang_room_charge_tax_unavailable": "റൂം ചാർജ് ടാക്സ് ലഭ്യമല്ല. നികുതി ക്രമീകരണങ്ങൾ അവലോകനം ചെയ്യുക.", + "lang_staff_room_orders": "സ്റ്റാഫ് റൂം ഓർഡറുകൾ അനുവദിക്കുക", + "lang_staff_room_prices": "സ്റ്റാഫ് റൂം ഓർഡറുകൾ സാധാരണ മെനു വിലകൾ ഉപയോഗിക്കുന്നു. വെബ് മെനു മാർക്കപ്പും വേദി കമ്മീഷനും വേറിട്ടതാണ്", + "lang_room_charge_offer": "അധിക റൂം ചാർജ് വാഗ്ദാനം ചെയ്യുക", + "lang_charge_name": "ചാർജ് പേര്", + "lang_apply_default_tax": "സ്ഥിരസ്ഥിതി നികുതി പ്രയോഗിക്കുക", + "lang_room_charge_basis": "കിഴിവുകൾക്ക് ശേഷമുള്ള ഭക്ഷണത്തിന്റെ ശതമാനം, നികുതിക്ക് മുമ്പ്. അതിഥി കരാർ ഉപയോഗിച്ച് മാത്രം അപേക്ഷിക്കുക; സ്റ്റാഫുകൾക്ക് ബില്ലിൽ നിന്ന് ഇത് നീക്കം ചെയ്യാം.", + "lang_room_charge_rate": "{0}: ഡിസ്കൗണ്ടിനു ശേഷമുള്ള ഭക്ഷണത്തിന്റെ {1}%, നികുതിക്ക് മുമ്പ്.", + "lang_room_charge_final": "അവസാന ബില്ലിൽ ഉൾപ്പെടുത്തിയിട്ടുണ്ട്.", + "lang_room_orders_enable_help": "ക്രമീകരണങ്ങൾ → റസ്റ്റോറന്റ് → ഹോട്ടലുകളിലും മറ്റു വേദികളിലും സ്റ്റാഫ് റൂം ഓർഡറുകൾ പ്രവർത്തനക്ഷമമാക്കുക.", + "lang_room_venue": "വേദി" } diff --git a/languages/mr.json b/languages/mr.json index 847f3f172..ebe7a428a 100644 --- a/languages/mr.json +++ b/languages/mr.json @@ -4923,5 +4923,22 @@ "lang_text_style": "पाठ्य शैली", "lang_this_product_is_hidden_from_sales_your_bas": "हे उत्पादन विक्रीपासून लपलेले आहे. तुमची बास्केट बदलली गेली नाही.", "lang_this_window_belongs_to_one_branch_open_ano": "ही विंडो एका शाखेची आहे. मुख्य विंडोमधून आणखी एक आउटलेट", - "lang_total_labels": "एकूण लेबल" + "lang_total_labels": "एकूण लेबल", + "lang_rooms": "खोल्या", + "lang_room_number": "खोली क्रमांक", + "lang_room_order": "खोली ऑर्डर", + "lang_room_service_charge": "खोली सेवा शुल्क", + "lang_room_charge_agreement": "अतिथी या अतिरिक्त शुल्कास", + "lang_room_destination_required": "ठिकाण निवडा आणि खोली नंबर प्रविष्ट करा", + "lang_room_charge_tax_unavailable": "रूम शुल्क कर उपलब्ध नाही. कर सेटिंग्जचे पुनरावलो", + "lang_staff_room_orders": "स्टाफ रूम ऑर्डरची", + "lang_staff_room_prices": "स्टाफ रूम ऑर्डरमध्ये सामान्य मेनू किं वेब मेनू मार्कअप आणि वेन्यू कमिशन वेगळे", + "lang_room_charge_offer": "अतिरिक्त खोली शुल्क दे", + "lang_charge_name": "शुल्क नाव", + "lang_apply_default_tax": "डीफॉल्ट कर ला", + "lang_room_charge_basis": "कर आधी सवलतीनंतर अन्नाची टक्केवारी केवळ अतिथी करारासह अर्ज करा; कर्मचारी ते बिलमधून काढू", + "lang_room_charge_rate": "{0}: कर आधी सवलतीनंतर अन्नाचे {1}%.", + "lang_room_charge_final": "अंतिम बिलमध्ये समाविष्ट आहे.", + "lang_room_orders_enable_help": "सेटिंग्ज → रेस्टॉरंट → हॉटेल आणि इतर ठिकाणांमध्ये स्टाफ रूम ऑर्ड", + "lang_room_venue": "ठिकाण" } diff --git a/languages/ms.json b/languages/ms.json index 786ae27a0..99e896be5 100644 --- a/languages/ms.json +++ b/languages/ms.json @@ -4923,5 +4923,22 @@ "lang_text_style": "Gaya teks", "lang_this_product_is_hidden_from_sales_your_bas": "Produk ini tersembunyi dari jualan. Bakul anda belum diubah.", "lang_this_window_belongs_to_one_branch_open_ano": "Tetingkap ini tergolong dalam satu cawangan. Buka outlet lain dari tetingkap utama.", - "lang_total_labels": "Jumlah label" + "lang_total_labels": "Jumlah label", + "lang_rooms": "Bilik", + "lang_room_number": "Nombor bilik", + "lang_room_order": "Perintah bilik", + "lang_room_service_charge": "Caj perkhidmatan bilik", + "lang_room_charge_agreement": "Tetamu bersetuju dengan caj tambahan ini", + "lang_room_destination_required": "Pilih tempat dan masukkan nombor bilik.", + "lang_room_charge_tax_unavailable": "Cukai caj bilik tidak tersedia. Semak tetapan cukai.", + "lang_staff_room_orders": "Benarkan pesanan bilik kakitangan", + "lang_staff_room_prices": "anan bilik kakitangan menggunakan harga menu biasa. Penanda menu web dan komisen tempat adalah berasingan", + "lang_room_charge_offer": "Menawarkan caj bilik tambahan", + "lang_charge_name": "Nama caj", + "lang_apply_default_tax": "Terapkan cukai lalai", + "lang_room_charge_basis": "Peratusan makanan selepas diskaun, sebelum cukai. Memohon hanya dengan perjanjian tetamu; kakitangan boleh mengeluarkannya dari bil.", + "lang_room_charge_rate": "{0}: {1}% makanan selepas diskaun, sebelum cukai.", + "lang_room_charge_final": "Termasuk pada rang undang-undang akhir.", + "lang_room_orders_enable_help": "Dayakan pesanan bilik kakitangan di Tetapan → Restoran → Hotel dan tempat lain.", + "lang_room_venue": "Tempat" } diff --git a/languages/mt.json b/languages/mt.json index f4f16c930..b5eddaffe 100644 --- a/languages/mt.json +++ b/languages/mt.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Stil tat-test", "lang_this_product_is_hidden_from_sales_your_bas": "Dan il-prodott huwa moħbi mill-bejgħ. Il-basket tiegħek ma nbidlitx.", "lang_this_window_belongs_to_one_branch_open_ano": "Din it-tieqa tappartjeni għal fergħa waħda. Iftaħ żbokk ieħor mit-tieqa prinċipali.", - "lang_total_labels": "Tikketti totali" + "lang_total_labels": "Tikketti totali", + "lang_rooms": "Kmamar", + "lang_room_number": "Numru tal-kamra", + "lang_room_order": "Ordni tal-kamra", + "lang_room_service_charge": "Ħlas tas-servizz tal-kamra", + "lang_room_charge_agreement": "Il-klijent qabel ma' din il-ħlas addizzjonali", + "lang_room_destination_required": "Agħżel post u daħħal in-numru tal-kamra.", + "lang_room_charge_tax_unavailable": "It-taxxa fuq it-tariffa tal-kamra mhix disponibbli. Irrevedi l-issettjar tat-taxxa.", + "lang_staff_room_orders": "Tippermetti ordnijiet tal-kamra tal-pers", + "lang_staff_room_prices": "Ordnijiet tal-kamra tal-persunal jużaw prezzijiet normali tal-menu Il-markup tal-menu tal-web u l-kummissjoni tal-post huma separati", + "lang_room_charge_offer": "Offri ħlas addizzjonali għall-kamra", + "lang_charge_name": "Isem tat-taxxa", + "lang_apply_default_tax": "Applika t-taxxa default", + "lang_room_charge_basis": "Perċentwal ta' ikel wara skontijiet, qabel it-taxxa. Applika biss bi ftehim mistieden; il-persunal jista 'jneħħih mill-polza.", + "lang_room_charge_rate": "{0}: {1}% tal-ikel wara l-iskontijiet, qabel it-taxxa.", + "lang_room_charge_final": "Inkluż fuq il-polza finali.", + "lang_room_orders_enable_help": "Ippermetti ordnijiet tal-kamra tal-persunal f'Settings → Restaurant → Lukandi u postijiet oħra.", + "lang_room_venue": "Post" } diff --git a/languages/nb.json b/languages/nb.json index 554330316..a0aab03fd 100644 --- a/languages/nb.json +++ b/languages/nb.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Tekststil", "lang_this_product_is_hidden_from_sales_your_bas": "Dette produktet er skjult for salg. Handlekurven din er ikke endret.", "lang_this_window_belongs_to_one_branch_open_ano": "Dette vinduet tilhører en gren. Åpne et annet uttak fra hovedvinduet.", - "lang_total_labels": "Totale etiketter" + "lang_total_labels": "Totale etiketter", + "lang_rooms": "Rom", + "lang_room_number": "Romnummer", + "lang_room_order": "Rombestilling", + "lang_room_service_charge": "Romservicegebyr", + "lang_room_charge_agreement": "Gjester godtok dette tilleggsgebyret", + "lang_room_destination_required": "Velg et sted og skriv inn romnummeret.", + "lang_room_charge_tax_unavailable": "Romavgift er ikke tilgjengelig. Gå gjennom skatteinnstillingene.", + "lang_staff_room_orders": "Tillat bestillinger på personalrom", + "lang_staff_room_prices": "Personalrombestillinger bruker normale menypriser. Markering av nettmeny og stedkommisjon er separate.", + "lang_room_charge_offer": "Tilby en ekstra romkostnad", + "lang_charge_name": "Ladningsnavn", + "lang_apply_default_tax": "Bruk standard skatt", + "lang_room_charge_basis": "Prosentandel av mat etter rabatter, før skatt. Søk bare med gjesteavtale; personalet kan fjerne det fra regningen.", + "lang_room_charge_rate": "{0}: {1}% mat etter rabatter, før skatt.", + "lang_room_charge_final": "Inkludert på den endelige regningen.", + "lang_room_orders_enable_help": "Aktiver bestillinger av personalrom i Innstillinger «Restaurant» Hoteller og andre arenaer.", + "lang_room_venue": "Sted" } diff --git a/languages/ne.json b/languages/ne.json index 8f0ebebc1..5804c0238 100644 --- a/languages/ne.json +++ b/languages/ne.json @@ -4578,5 +4578,22 @@ "lang_review_exactly_these_changes_then_confirm": "यिनै परिवर्तन समीक्षा गरेर पुष्टि गर्नुहोस्।", "lang_scan_barcode_or_search_products": "बारकोड स्क्यान गर्नुहोस् वा उत्पादन खोज्नुहोस्", "lang_server_catalogue_offline_tills_receive_cha": "सर्भर क्याटलग। अफलाइन टिल फेरि जोडिएपछि परिवर्तन प्राप्त गर्छन्।", - "lang_this_product_is_hidden_from_sales_your_bas": "यो उत्पादन बिक्रीबाट लुकाइएको छ। तपाईंको बास्केट परिवर्तन भएको छैन।" + "lang_this_product_is_hidden_from_sales_your_bas": "यो उत्पादन बिक्रीबाट लुकाइएको छ। तपाईंको बास्केट परिवर्तन भएको छैन।", + "lang_rooms": "कोठाहरू", + "lang_room_number": "कोठा नम्बर", + "lang_room_order": "कोठाको अर्डर", + "lang_room_service_charge": "कोठा सेवा शुल्क", + "lang_room_charge_agreement": "अतिथिले यो अतिरिक्त शुल्कमा सहमति जनाएका छन्", + "lang_room_destination_required": "स्थल छान्नुहोस् र कोठा नम्बर लेख्नुहोस्।", + "lang_room_charge_tax_unavailable": "कोठा शुल्कको कर उपलब्ध छैन। कर सेटिङ जाँच्नुहोस्।", + "lang_staff_room_orders": "कर्मचारीलाई कोठाको अर्डर लिन अनुमति दिनुहोस्", + "lang_staff_room_prices": "कर्मचारीले लिने कोठाको अर्डरमा सामान्य मेनु मूल्य लाग्छ। वेब मेनुको थप मूल्य र स्थलको कमिसन अलग हुन्।", + "lang_room_charge_offer": "अतिरिक्त कोठा शुल्क प्रस्ताव गर्नुहोस्", + "lang_charge_name": "शुल्कको नाम", + "lang_apply_default_tax": "पूर्वनिर्धारित कर लगाउनुहोस्", + "lang_room_charge_basis": "छुटपछिको र करअघिको खानाको रकमको प्रतिशत। अतिथिको सहमतिमा मात्र लगाउनुहोस्; कर्मचारीले बिलबाट हटाउन सक्छन्।", + "lang_room_charge_rate": "{0}: छुटपछिको र करअघिको खानाको रकमको {1}%।", + "lang_room_charge_final": "अन्तिम बिलमा समावेश हुन्छ।", + "lang_room_orders_enable_help": "सेटिङ → रेस्टुरेन्ट → होटल र अन्य स्थलमा कर्मचारीका लागि कोठाको अर्डर सक्षम गर्नुहोस्।", + "lang_room_venue": "स्थल" } diff --git a/languages/nl.json b/languages/nl.json index 9ea1fa8d5..f2f22de97 100644 --- a/languages/nl.json +++ b/languages/nl.json @@ -5011,5 +5011,22 @@ "lang_review_exactly_these_changes_then_confirm": "Controleer precies deze wijzigingen en bevestig ze.", "lang_scan_barcode_or_search_products": "Barcodes scannen of producten zoeken", "lang_server_catalogue_offline_tills_receive_cha": "Servercatalogus. Offline kassa's worden gewijzigd wanneer ze opnieuw verbinding maken.", - "lang_this_product_is_hidden_from_sales_your_bas": "Dit product is verborgen voor verkoop. Je winkelmandje is niet gewijzigd." + "lang_this_product_is_hidden_from_sales_your_bas": "Dit product is verborgen voor verkoop. Je winkelmandje is niet gewijzigd.", + "lang_rooms": "Kamers", + "lang_room_number": "Kamernummer", + "lang_room_order": "Kamer bestellen", + "lang_room_service_charge": "Roomservicekosten", + "lang_room_charge_agreement": "De gast ging akkoord met deze extra vergoeding", + "lang_room_destination_required": "Kies een locatie en voer het kamernummer in.", + "lang_room_charge_tax_unavailable": "De kamerbelasting is niet beschikbaar. Controleer de belastinginstellingen.", + "lang_staff_room_orders": "Bestellingen voor personeelskamers toestaan", + "lang_staff_room_prices": "bestellingen van personeelskamers worden de normale menuprijzen gebruikt. De opmaak van het webmenu en de locatiecommissie zijn gescheiden", + "lang_room_charge_offer": "Bied een extra kamertoeslag aan", + "lang_charge_name": "Naam van de lading", + "lang_apply_default_tax": "Standaardbelasting toepassen", + "lang_room_charge_basis": "Percentage eten na kortingen, vóór belasting. Alleen aanvragen met instemming van de gasten; het personeel kan dit van de rekening verwijderen.", + "lang_room_charge_rate": "{0}: {1}% van het eten na kortingen, vóór belasting.", + "lang_room_charge_final": "Inbegrepen op de eindafrekening.", + "lang_room_orders_enable_help": "Schakel kamerbestellingen voor medewerkers in Instellingen → Restaurant → Hotels en andere locaties in.", + "lang_room_venue": "Locatie" } diff --git a/languages/pa.json b/languages/pa.json index f9bb32123..01deec428 100644 --- a/languages/pa.json +++ b/languages/pa.json @@ -4920,5 +4920,22 @@ "lang_text_style": "ਟੈਕਸਟ ਸ਼ੈਲੀ", "lang_this_product_is_hidden_from_sales_your_bas": "ਇਹ ਉਤਪਾਦ ਵਿਕਰੀ ਤੋਂ ਲੁਕਿਆ ਹੋਇਆ ਹੈ. ਤੁਹਾਡੀ ਟੋਕਰੀ ਨਹੀਂ ਬਦਲੀ ਗਈ ਹੈ।", "lang_this_window_belongs_to_one_branch_open_ano": "ਇਹ ਵਿੰਡੋ ਇਕ ਸ਼ਾਖਾ ਨਾਲ ਸਬੰਧਤ ਹੈ. ਮੁੱਖ ਵਿੰਡੋ ਤੋਂ ਇਕ ਹੋਰ ਆਉਟਲੈਟ ਖੋਲ੍ਹੋ.", - "lang_total_labels": "ਕੁੱਲ ਲੇਬਲ" + "lang_total_labels": "ਕੁੱਲ ਲੇਬਲ", + "lang_rooms": "ਕਮਰੇ", + "lang_room_number": "ਕਮਰੇ ਦਾ ਨੰਬਰ", + "lang_room_order": "ਕਮਰੇ ਦਾ ਆਦੇਸ਼", + "lang_room_service_charge": "ਕਮਰਾ ਸੇਵਾ ਚਾਰਜ", + "lang_room_charge_agreement": "ਮਹਿਮਾਨ ਇਸ ਵਾਧੂ ਖਰਚੇ ਲਈ ਸਹਿਮਤ ਹੋਏ", + "lang_room_destination_required": "ਇੱਕ ਸਥਾਨ ਚੁਣੋ ਅਤੇ ਕਮਰੇ ਦਾ ਨੰਬਰ ਦਾਖਲ ਕਰੋ।", + "lang_room_charge_tax_unavailable": "ਕਮਰੇ ਚਾਰਜ ਟੈਕਸ ਉਪਲਬਧ ਨਹੀਂ ਹੈ। ਟੈਕਸ ਸੈਟਿੰਗਾਂ ਦੀ ਸਮੀਖਿਆ ਕਰੋ।", + "lang_staff_room_orders": "ਸਟਾਫ ਰੂਮ ਆਰਡਰ ਦੀ ਆਗਿਆ ਦਿਓ", + "lang_staff_room_prices": "ਸਟਾਫ ਰੂਮ ਆਰਡਰ ਆਮ ਮੀਨੂ ਕੀਮਤਾਂ ਦੀ ਵਰਤੋਂ ਕਰਦੇ ਹਨ. ਵੈੱਬ ਮੀਨੂ ਮਾਰਕਅਪ ਅਤੇ ਸਥਾਨ ਕਮਿਸ਼ਨ ਵੱਖਰੇ ਹਨ", + "lang_room_charge_offer": "ਇੱਕ ਵਾਧੂ ਕਮਰੇ ਚਾਰਜ ਦੀ ਪੇਸ਼ਕਸ਼ ਕਰੋ", + "lang_charge_name": "ਚਾਰਜ ਦਾ ਨਾਮ", + "lang_apply_default_tax": "ਮੂਲ ਟੈਕਸ ਲਾਗੂ ਕਰੋ", + "lang_room_charge_basis": "ਛੋਟ ਤੋਂ ਬਾਅਦ ਭੋਜਨ ਦੀ ਪ੍ਰਤੀਸ਼ਤਤਾ, ਟੈਕਸ ਤੋਂ ਪਹਿਲਾਂ. ਸਿਰਫ ਮਹਿਮਾਨ ਸਮਝੌਤੇ ਨਾਲ ਅਰਜ਼ੀ ਦਿਓ; ਸਟਾਫ ਇਸ ਨੂੰ ਬਿੱਲ ਤੋਂ ਹਟਾ ਸਕਦਾ ਹੈ.", + "lang_room_charge_rate": "{0}: ਛੋਟ ਤੋਂ ਬਾਅਦ ਭੋਜਨ ਦਾ {1}%, ਟੈਕਸ ਤੋਂ ਪਹਿਲਾਂ।", + "lang_room_charge_final": "ਅੰਤਮ ਬਿੱਲ ਤੇ ਸ਼ਾਮਲ.", + "lang_room_orders_enable_help": "ਸੈਟਿੰਗਜ਼ → ਰੈਸਟੋਰੈਂਟ → ਹੋਟਲ ਅਤੇ ਹੋਰ ਸਥਾਨਾਂ ਵਿੱਚ ਸਟਾਫ ਰੂਮ ਆਰਡਰ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ।", + "lang_room_venue": "ਸਥਾਨ" } diff --git a/languages/pl.json b/languages/pl.json index 2a39b22a9..204e53e55 100644 --- a/languages/pl.json +++ b/languages/pl.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Styl tekstu", "lang_this_product_is_hidden_from_sales_your_bas": "Ten produkt jest ukryty przed sprzedażą. Twój koszyk nie został zmieniony.", "lang_this_window_belongs_to_one_branch_open_ano": "To okno należy do jednej gałęzi. Otwórz kolejne gniazdko z głównego okna.", - "lang_total_labels": "Wszystkie etykiety" + "lang_total_labels": "Wszystkie etykiety", + "lang_rooms": "Pokoje", + "lang_room_number": "Numer pokoju", + "lang_room_order": "Porządek pokoju", + "lang_room_service_charge": "Opłata za obsługę pokoju", + "lang_room_charge_agreement": "Gość wyraził zgodę na tę dodatkową opłatę", + "lang_room_destination_required": "Wybierz miejsce i wprowadź numer pokoju.", + "lang_room_charge_tax_unavailable": "Podatek od opłaty za pokój jest niedostępny. Przejrzyj ustawienia podatkowe.", + "lang_staff_room_orders": "Zezwalaj na zamówienia pokoju dla pracowników", + "lang_staff_room_prices": "Zamówienia pokoi dla personelu korzystają z normalnych cen menu. Znaczniki menu internetowego i prowizja miejsca są oddzielne", + "lang_room_charge_offer": "Oferuj dodatkową opłatę za pokój", + "lang_charge_name": "Nazwa opłaty", + "lang_apply_default_tax": "Zastosuj podatek domyślny", + "lang_room_charge_basis": "Procent jedzenia po zniżkach, przed opodatkowaniem. Złóż wniosek tylko za zgodą gościa; personel może usunąć go z rachunku.", + "lang_room_charge_rate": "{0}: {1}% żywności po rabatach, przed opodatkowaniem.", + "lang_room_charge_final": "Zawarte w ostatecznym rachunku.", + "lang_room_orders_enable_help": "Włącz zamawianie pokoi dla personelu w Ustawieniach → Restauracja → Hotele i inne obiekty.", + "lang_room_venue": "Miejsce" } diff --git a/languages/pt.json b/languages/pt.json index ce99824f7..1d436df65 100644 --- a/languages/pt.json +++ b/languages/pt.json @@ -5001,5 +5001,22 @@ "lang_text_style": "Estilo de texto", "lang_this_product_is_hidden_from_sales_your_bas": "Este produto está oculto das vendas. Sua cesta não foi alterada.", "lang_this_window_belongs_to_one_branch_open_ano": "Essa janela pertence a uma filial. Abra outra tomada na janela principal.", - "lang_total_labels": "Total de rótulos" + "lang_total_labels": "Total de rótulos", + "lang_rooms": "Salas", + "lang_room_number": "Número do quarto", + "lang_room_order": "Ordem do quarto", + "lang_room_service_charge": "Taxa de serviço de quarto", + "lang_room_charge_agreement": "O hóspede concordou com essa taxa adicional", + "lang_room_destination_required": "Escolha um local e insira o número do quarto.", + "lang_room_charge_tax_unavailable": "O imposto sobre a tarifa do quarto não está disponível. Revise as configurações fiscais.", + "lang_staff_room_orders": "Permitir pedidos de quartos para funcionários", + "lang_staff_room_prices": "pedidos de quartos dos funcionários usam os preços normais do menu. A marcação do menu da web e a comissão do local são separadas", + "lang_room_charge_offer": "Ofereça uma taxa adicional de quarto", + "lang_charge_name": "Nome da cobrança", + "lang_apply_default_tax": "Aplicar imposto padrão", + "lang_room_charge_basis": "Porcentagem de alimentos após descontos, antes de impostos. Inscreva-se somente com o acordo do hóspede; a equipe pode removê-lo da fatura.", + "lang_room_charge_rate": "{0}: {1}% de alimentos após descontos, antes de impostos.", + "lang_room_charge_final": "Incluído na fatura final.", + "lang_room_orders_enable_help": "Habilite pedidos de quartos para funcionários em Configurações → Restaurante → Hotéis e outros locais.", + "lang_room_venue": "Local" } diff --git a/languages/ro.json b/languages/ro.json index 0c55f9911..01e6f31fd 100644 --- a/languages/ro.json +++ b/languages/ro.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Stilul textului", "lang_this_product_is_hidden_from_sales_your_bas": "Acest produs este ascuns vânzărilor. Coșul dvs. nu a fost schimbat.", "lang_this_window_belongs_to_one_branch_open_ano": "Această fereastră aparține unei ramuri. Deschideți o altă priză din fereastra principală.", - "lang_total_labels": "Total etichete" + "lang_total_labels": "Total etichete", + "lang_rooms": "Camere", + "lang_room_number": "Numărul camerei", + "lang_room_order": "Comanda camerei", + "lang_room_service_charge": "Taxa de room service", + "lang_room_charge_agreement": "Oaspeții au fost de acord cu această taxă suplimentară", + "lang_room_destination_required": "Alegeți un loc și introduceți numărul camerei.", + "lang_room_charge_tax_unavailable": "Taxa pentru cameră nu este disponibilă. Examinați setările fiscale.", + "lang_staff_room_orders": "Permiteți comenzile camerei personalului", + "lang_staff_room_prices": "enzile camerelor de personal utilizează prețuri normale ale meniului. Marcarea meniului web și comisionul locației sunt separate.", + "lang_room_charge_offer": "Oferă un cost suplimentar pentru cameră", + "lang_charge_name": "Numele taxei", + "lang_apply_default_tax": "Aplicați taxa implicită", + "lang_room_charge_basis": "Procentul de alimente după reduceri, înainte de impozitare. Aplicați numai cu acordul oaspeților; personalul îl poate elimina din factură.", + "lang_room_charge_rate": "{0}: {1}% din produsele alimentare după reduceri, înainte de impozitare.", + "lang_room_charge_final": "Inclus în factura finală.", + "lang_room_orders_enable_help": "Activați comenzile camerelor personalului în Setări „Restaurant”” Hoteluri și alte locații.", + "lang_room_venue": "Locul de desfășurare" } diff --git a/languages/ru.json b/languages/ru.json index de9ad3126..d5925585b 100644 --- a/languages/ru.json +++ b/languages/ru.json @@ -4923,5 +4923,22 @@ "lang_text_style": "Стиль текста", "lang_this_product_is_hidden_from_sales_your_bas": "Этот продукт скрыт от продаж. Ваша корзина не была изменена.", "lang_this_window_belongs_to_one_branch_open_ano": "Это окно принадлежит одному филиалу. Откройте другую розетку в главном окне.", - "lang_total_labels": "Всего этикеток" + "lang_total_labels": "Всего этикеток", + "lang_rooms": "Номера", + "lang_room_number": "Номер комнаты", + "lang_room_order": "Порядок номеров", + "lang_room_service_charge": "Плата за обслуживание в номере", + "lang_room_charge_agreement": "Гость согласился с этой дополнительной оплатой", + "lang_room_destination_required": "Выберите место проведения и введите номер комнаты.", + "lang_room_charge_tax_unavailable": "Налог на оплату номера недоступен. Ознакомьтесь с настройками налогообложения.", + "lang_staff_room_orders": "Разрешите заказы на комнаты для персонала", + "lang_staff_room_prices": "заказе номеров для персонала используются обычные цены на меню. Разметка веб-меню и комиссия за посещение заведения — это отдельно", + "lang_room_charge_offer": "Предложите дополнительную плату за номер", + "lang_charge_name": "Название платежа", + "lang_apply_default_tax": "Применить налог по умолчанию", + "lang_room_charge_basis": "Процент продуктов питания после скидок до уплаты налогов. Подайте заявку только с согласия гостя; сотрудники могут исключить эту сумму из счета.", + "lang_room_charge_rate": "{0}: {1}% от стоимости еды после вычета скидок до уплаты налогов.", + "lang_room_charge_final": "Включено в окончательный счет.", + "lang_room_orders_enable_help": "Разрешите заказ номеров для персонала в меню «Настройки», «Ресторан», «Отели» и другие заведения.", + "lang_room_venue": "Место проведения" } diff --git a/languages/server/_english.json b/languages/server/_english.json index dac4b52dd..ccf7f49ec 100644 --- a/languages/server/_english.json +++ b/languages/server/_english.json @@ -439,12 +439,18 @@ "Choose a supplier": [ "src/repositories/purchase-order.repository.js" ], + "Choose a table first.": [ + "src/repositories/sale.repository.js" + ], "Choose a table with enough seats.": [ "src/repositories/sale.repository.js" ], "Choose a valid Take Away order and branch": [ "src/repositories/sale.repository.js" ], + "Choose an enabled venue and enter a room number (up to 40 characters).": [ + "src/repositories/sale.repository.js" + ], "Choose discount or markup quotation pricing.": [ "src/repositories/quote.repository.js", "src/repositories/settings.repository.js" @@ -1925,6 +1931,9 @@ "Role not found": [ "src/repositories/role.repository.js" ], + "Room settings could not be loaded. Try again.": [ + "src/repositories/sale.repository.js" + ], "SSO authentication successful": [ "src/services/user.service.js" ], diff --git a/languages/si.json b/languages/si.json index 21f4767ce..803adeae8 100644 --- a/languages/si.json +++ b/languages/si.json @@ -5001,5 +5001,22 @@ "lang_text_style": "පෙළ ශෛලිය", "lang_this_product_is_hidden_from_sales_your_bas": "මෙම නිෂ්පාදනය අලෙවියෙන් සැඟවී ඇත. ඔබේ කූඩය වෙනස් කර නැත.", "lang_this_window_belongs_to_one_branch_open_ano": "මෙම කවුළුව එක් ශාඛාවකට අයත් වේ. ප්රධාන කවුළුවෙන් තවත් වෙළඳසැලක් විවෘත කරන්න.", - "lang_total_labels": "මුළු ලේබල්" + "lang_total_labels": "මුළු ලේබල්", + "lang_rooms": "කාමර", + "lang_room_number": "කාමර අංකය", + "lang_room_order": "කාමර අනුපිළිවෙල", + "lang_room_service_charge": "කාමර සේවා ගාස්තුව", + "lang_room_charge_agreement": "අමුත්තා මෙම අමතර ගාස්තුව සඳහා එකඟ විය", + "lang_room_destination_required": "ස්ථානයක් තෝරාගෙන කාමර අංකය ඇතුළත් කරන්න.", + "lang_room_charge_tax_unavailable": "කාමර ගාස්තු බද්ද ලබා ගත නොහැක. බදු සැකසුම් සමාලෝචනය කරන්න.", + "lang_staff_room_orders": "කාර්ය මණ්ඩල කාමර ඇණවුම් කිරීමට ඉඩ", + "lang_staff_room_prices": "කාර්ය මණ්ඩල කාමර ඇණවුම් සාමාන්ය මෙනු මිල භාවිතා කරයි. වෙබ් මෙනු සලකුණු කිරීම සහ ස්ථාන කොමිෂන් සභාව වෙනම වේ", + "lang_room_charge_offer": "අමතර කාමර ගාස්තුවක් ලබා දෙන්න", + "lang_charge_name": "ගාස්තු නම", + "lang_apply_default_tax": "පෙරනිමි බද්ද යොදන්න", + "lang_room_charge_basis": "බද්දට පෙර වට්ටම් වලින් පසු ආහාර ප්රතිශතය. ආගන්තුක ගිවිසුම සමඟ පමණක් අයදුම් කරන්න; කාර්ය මණ්ඩලයට එය බිල්පතෙන් ඉවත් කළ හැකිය.", + "lang_room_charge_rate": "{0}: {1}% ආහාර වට්ටම් වලින් පසුව, බද්දට පෙර.", + "lang_room_charge_final": "අවසාන පනතට ඇතුළත් කර ඇත.", + "lang_room_orders_enable_help": "සැකසුම් → අවන්හල් → හෝටල් සහ වෙනත් ස්ථානවල කාර්ය මණ්ඩල කාමර ඇණවුම් සක්රීය කරන්න.", + "lang_room_venue": "ස්ථානය" } diff --git a/languages/sk.json b/languages/sk.json index 04eedfa73..94b702362 100644 --- a/languages/sk.json +++ b/languages/sk.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Štýl textu", "lang_this_product_is_hidden_from_sales_your_bas": "Tento produkt je skrytý pred predajom. Váš košík nebol zmenený.", "lang_this_window_belongs_to_one_branch_open_ano": "Toto okno patrí do jednej vetvy. Otvorte ďalšiu zásuvku z hlavného okna.", - "lang_total_labels": "Celkový počet štítkov" + "lang_total_labels": "Celkový počet štítkov", + "lang_rooms": "Izby", + "lang_room_number": "Číslo izby", + "lang_room_order": "Poradie miestnosti", + "lang_room_service_charge": "Poplatok za izbovú službu", + "lang_room_charge_agreement": "Hostia súhlasili s týmto príplatkom", + "lang_room_destination_required": "Vyberte miesto konania a zadajte číslo izby.", + "lang_room_charge_tax_unavailable": "Daň za izbu nie je k dispozícii. Skontrolujte daňové nastavenia.", + "lang_staff_room_orders": "Povoliť objednávky miestností zamestnancov", + "lang_staff_room_prices": "návky personálnych izieb používajú bežné ceny menu. Značenie webového menu a provízia miesta konania sú samostatné", + "lang_room_charge_offer": "Ponúknite príplatok za izbu", + "lang_charge_name": "Názov poplatku", + "lang_apply_default_tax": "Použiť predvolenú daň", + "lang_room_charge_basis": "Percento potravín po zľavách, pred zdanením. Prihláste sa len so súhlasom hosťa; personál ho môže odstrániť z účtu.", + "lang_room_charge_rate": "{0}: {1}% potravín po zľavách, pred zdanením.", + "lang_room_charge_final": "Zahrnuté v konečnom účte.", + "lang_room_orders_enable_help": "Povoliť objednávky izieb pre zamestnancov v nastaveniach → Reštaurácia → Hotely a iné miesta.", + "lang_room_venue": "Miesto konania" } diff --git a/languages/sl.json b/languages/sl.json index d8f559177..fdf786f6e 100644 --- a/languages/sl.json +++ b/languages/sl.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Besedilni slog", "lang_this_product_is_hidden_from_sales_your_bas": "Ta izdelek je skrit pred prodajo. Vaša košarica ni bila spremenjena.", "lang_this_window_belongs_to_one_branch_open_ano": "To okno pripada eni veji. Odprite drugo vtičnico iz glavnega okna.", - "lang_total_labels": "Skupaj oznak" + "lang_total_labels": "Skupaj oznak", + "lang_rooms": "Sobe", + "lang_room_number": "Številka sobe", + "lang_room_order": "Vrstni red sobe", + "lang_room_service_charge": "Cena sobne postrežbe", + "lang_room_charge_agreement": "Gostje se strinjajo s tem doplačilom", + "lang_room_destination_required": "Izberite prizorišče in vnesite številko sobe.", + "lang_room_charge_tax_unavailable": "Davek na sobo ni na voljo. Preglejte davčne nastavitve.", + "lang_staff_room_orders": "Dovoli naročila sob za osebje", + "lang_staff_room_prices": "Naročila sob za osebje uporabljajo običajne cene menija. Oznaka spletnega menija in provizija prizorišča sta ločena", + "lang_room_charge_offer": "Ponudite doplačilo sobe", + "lang_charge_name": "Ime polnilnika", + "lang_apply_default_tax": "Uporabi privzeti davek", + "lang_room_charge_basis": "Odstotek hrane po popustih, pred davkom. Prijavite se samo s soglasjem gosta; osebje ga lahko odstrani iz računa.", + "lang_room_charge_rate": "{0}: {1}% hrane po popustu, pred davkom.", + "lang_room_charge_final": "Vključeno v končni račun.", + "lang_room_orders_enable_help": "Omogočite naročanje sob za osebje v nastavitvah → Restavracija → Hoteli in druga prizorišča.", + "lang_room_venue": "Prizorišče" } diff --git a/languages/sq.json b/languages/sq.json index 44414947f..78b750329 100644 --- a/languages/sq.json +++ b/languages/sq.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Stili i tekstit", "lang_this_product_is_hidden_from_sales_your_bas": "Ky produkt është i fshehur nga shitjet. Shporta juaj nuk është ndryshuar.", "lang_this_window_belongs_to_one_branch_open_ano": "Kjo dritare i përket një degë. Hapni një prizë tjetër nga dritarja kryesore.", - "lang_total_labels": "Totali i etiketave" + "lang_total_labels": "Totali i etiketave", + "lang_rooms": "Dhomat", + "lang_room_number": "Numri i dhomës", + "lang_room_order": "Rendi i dhomës", + "lang_room_service_charge": "Tarifa e shërbimit të dhomës", + "lang_room_charge_agreement": "Mysafiri pranoi këtë pagesë shtesë", + "lang_room_destination_required": "Zgjidhni një vend dhe futni numrin e dhomës.", + "lang_room_charge_tax_unavailable": "Taksa e tarifës së dhomës nuk është e disponueshme. Rishikoni cilësimet e taksave.", + "lang_staff_room_orders": "Lejoni porositë e dhomës së", + "lang_staff_room_prices": "Porositë e dhomave të stafit përdorin çmimet normale të men Shënimi i menusë në internet dhe komisioni i vendit janë të ndara", + "lang_room_charge_offer": "Ofroni një tarifë shtesë për dhomë", + "lang_charge_name": "Emri i ngarkesës", + "lang_apply_default_tax": "Aplikoni tatimin e parazgjed", + "lang_room_charge_basis": "Përqindja e ushqimit pas zbritjeve, para tatimit. Aplikoni vetëm me marrëveshjen e mysafirëve; stafi mund ta heqë atë nga fatura.", + "lang_room_charge_rate": "{0}: {1}% e ushqimit pas zbritjeve, para taksave.", + "lang_room_charge_final": "Përfshirë në projektligjin përfundimtar.", + "lang_room_orders_enable_help": "Aktivizoni porositë e dhomave të stafit në Cilësimet → Restorant → Hotele dhe vende të tjera.", + "lang_room_venue": "Vendi" } diff --git a/languages/sr.json b/languages/sr.json index 2cce3cfa0..abf6708f5 100644 --- a/languages/sr.json +++ b/languages/sr.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Stil teksta", "lang_this_product_is_hidden_from_sales_your_bas": "Ovaj proizvod je skriven od prodaje. Vaša korpa nije promenjena.", "lang_this_window_belongs_to_one_branch_open_ano": "Ovaj prozor pripada jednoj grani. Otvorite drugu utičnicu iz glavnog prozora.", - "lang_total_labels": "Ukupno oznaka" + "lang_total_labels": "Ukupno oznaka", + "lang_rooms": "Sobe", + "lang_room_number": "Broj sobe", + "lang_room_order": "Redosled sobe", + "lang_room_service_charge": "Naknada za poslugu u sobu", + "lang_room_charge_agreement": "Gost je pristao na ovu dodatnu naknadu", + "lang_room_destination_required": "Izaberite mesto i unesite broj sobe.", + "lang_room_charge_tax_unavailable": "Porez na naknadu za sobu nije dostupan. Pregledajte poreska podešavanja.", + "lang_staff_room_orders": "Dozvolite narudžbe soba osoblja", + "lang_staff_room_prices": "udžbe soba osoblja koriste normalne cene menija. Označavanje veb menija i provizija mesta su odvojeni", + "lang_room_charge_offer": "Ponudite dodatnu naknadu za sobu", + "lang_charge_name": "Naziv naplate", + "lang_apply_default_tax": "Primenite podrazumevani porez", + "lang_room_charge_basis": "Procenat hrane nakon popusta, pre oporezivanja. Prijavite se samo uz dogovor gosta; osoblje ga može ukloniti sa računa.", + "lang_room_charge_rate": "{0}: {1}% hrane nakon popusta, pre oporezivanja.", + "lang_room_charge_final": "Uključeno u konačni račun.", + "lang_room_orders_enable_help": "Omogućite naručivanje soba osoblja u Podešavanjima → Restoran → Hoteli i druga mesta.", + "lang_room_venue": "Mesto održavanja" } diff --git a/languages/sv.json b/languages/sv.json index 0854c558a..84389b923 100644 --- a/languages/sv.json +++ b/languages/sv.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Textstil", "lang_this_product_is_hidden_from_sales_your_bas": "Denna produkt är dold från försäljning. Din varukorg har inte ändrats.", "lang_this_window_belongs_to_one_branch_open_ano": "Detta fönster tillhör en gren. Öppna ett annat uttag från huvudfönstret.", - "lang_total_labels": "Totala etiketter" + "lang_total_labels": "Totala etiketter", + "lang_rooms": "Rum", + "lang_room_number": "Rumsnummer", + "lang_room_order": "Rumsbeställning", + "lang_room_service_charge": "Avgift för rumsservice", + "lang_room_charge_agreement": "Gästen godkände denna extra avgift", + "lang_room_destination_required": "Välj en plats och ange rumsnumret.", + "lang_room_charge_tax_unavailable": "Rumsavgiften är inte tillgänglig. Granska skatteinställningarna.", + "lang_staff_room_orders": "Tillåt personalrumsbeställningar", + "lang_staff_room_prices": "Personalrumsbeställningar använder normala menypriser. Webbmenymarkering och platskommission är separata.", + "lang_room_charge_offer": "Erbjud en extra rumsavgift", + "lang_charge_name": "Avgiftsnamn", + "lang_apply_default_tax": "Tillämpa standardskatt", + "lang_room_charge_basis": "Procentandel mat efter rabatter, före skatt. Ansök endast med gästavtal; personalen kan ta bort det från räkningen.", + "lang_room_charge_rate": "{0}: {1}% mat efter rabatter, före skatt.", + "lang_room_charge_final": "Ingår i slutfakturan.", + "lang_room_orders_enable_help": "Aktivera personalrumsbeställningar i Inställningar ”Restaurang” Hotell och andra lokaler.", + "lang_room_venue": "Mötesplats" } diff --git a/languages/sw.json b/languages/sw.json index 2ec7ec0b0..c74271a65 100644 --- a/languages/sw.json +++ b/languages/sw.json @@ -5001,5 +5001,22 @@ "lang_text_style": "Mtindo wa maandishi", "lang_this_product_is_hidden_from_sales_your_bas": "Bidhaa hii imefichwa kutoka kwa mauzo. Kikapu chako hakijabadilishwa.", "lang_this_window_belongs_to_one_branch_open_ano": "Dirisha hili ni ya tawi moja. Fungua duka lingine kutoka dirisha kuu.", - "lang_total_labels": "Jumla ya lebo" + "lang_total_labels": "Jumla ya lebo", + "lang_rooms": "Vyumba", + "lang_room_number": "Nambari ya chumba", + "lang_room_order": "Utaratibu wa chumba", + "lang_room_service_charge": "Gharama ya huduma ya chumba", + "lang_room_charge_agreement": "Mgeni alikubali malipo hii ya ziada", + "lang_room_destination_required": "Chagua ukumbi na uingie nambari ya chumba.", + "lang_room_charge_tax_unavailable": "Ushuru wa malipo ya chumba haipatikani. Pitia mipangilio ya ushuru.", + "lang_staff_room_orders": "Ruhusu maagizo ya chumba cha waf", + "lang_staff_room_prices": "Maagizo ya chumba cha wafanyikazi hutumia bei za kawaida za orod Alama ya menyu ya wavuti na tume ya ukumbi ni tofauti.", + "lang_room_charge_offer": "Toa malipo ya ziada ya chumba", + "lang_charge_name": "Jina la malipo", + "lang_apply_default_tax": "Tumia ushuru wa default", + "lang_room_charge_basis": "Asilimia ya chakula baada ya punguzo, kabla ya ushuru. Tumia tu na makubaliano ya wageni; wafanyikazi wanaweza kuiondoa kwenye muswada.", + "lang_room_charge_rate": "{0}: {1}% ya chakula baada ya punguzo, kabla ya ushuru.", + "lang_room_charge_final": "Imejumuishwa kwenye muswada wa mwisho.", + "lang_room_orders_enable_help": "Wezesha maagizo ya chumba cha wafanyikazi katika Mipangilio → Mkahawa → Hoteli na maeneo mengine.", + "lang_room_venue": "Ukumbi" } diff --git a/languages/ta.json b/languages/ta.json index b697d9a61..27dcb5d7c 100644 --- a/languages/ta.json +++ b/languages/ta.json @@ -5005,5 +5005,22 @@ "lang_text_style": "உரை பாணி", "lang_this_product_is_hidden_from_sales_your_bas": "இந்த தயாரிப்பு விற்பனையிலிருந்து மறைக்கப்பட்டுள்ளது. உங்கள் கூடை மாற்றப்படவில்லை.", "lang_this_window_belongs_to_one_branch_open_ano": "இந்த சாளரம் ஒரு கிளைக்கு சொந்தமானது. பிரதான சாளரத்திலிருந்து மற்றொரு வெளியீட்டைத் திறக்கவும்.", - "lang_total_labels": "மொத்த லேபிள்கள்" + "lang_total_labels": "மொத்த லேபிள்கள்", + "lang_rooms": "அறைகள்", + "lang_room_number": "அறை எண்", + "lang_room_order": "அறைக்கான ஆர்டர்", + "lang_room_service_charge": "அறை சேவைக் கட்டணம்", + "lang_room_charge_agreement": "இந்த கூடுதல் கட்டணத்திற்கு விருந்தினர் ஒப்புக்கொண்டார்", + "lang_room_destination_required": "இடத்தைத் தேர்ந்தெடுத்து அறை எண்ணை உள்ளிடவும்.", + "lang_room_charge_tax_unavailable": "அறை சேவைக் கட்டணத்திற்கான வரி கிடைக்கவில்லை. வரி அமைப்புகளைச் சரிபார்க்கவும்.", + "lang_staff_room_orders": "பணியாளர்கள் அறைகளுக்கான ஆர்டர்களை எடுக்க அனுமதிக்கவும்", + "lang_staff_room_prices": "பணியாளர்கள் எடுக்கும் அறை ஆர்டர்களுக்கு வழக்கமான மெனு விலைகள் பொருந்தும். இணைய மெனுவின் கூடுதல் விலையும் இடத்திற்கான கமிஷனும் தனித்தனியானவை.", + "lang_room_charge_offer": "கூடுதல் அறை சேவைக் கட்டணத்தை வழங்கவும்", + "lang_charge_name": "கட்டணத்தின் பெயர்", + "lang_apply_default_tax": "இயல்புநிலை வரியைச் சேர்க்கவும்", + "lang_room_charge_basis": "தள்ளுபடிக்குப் பின், வரிக்கு முந்தைய உணவுத் தொகையின் சதவீதம். விருந்தினரின் ஒப்புதலுடன் மட்டும் சேர்க்கவும்; பணியாளர்கள் பில்லிலிருந்து நீக்கலாம்.", + "lang_room_charge_rate": "{0}: தள்ளுபடிக்குப் பின், வரிக்கு முந்தைய உணவுத் தொகையில் {1}%.", + "lang_room_charge_final": "இறுதி பில்லில் சேர்க்கப்படும்.", + "lang_room_orders_enable_help": "அமைப்புகள் → உணவகம் → ஹோட்டல்கள் மற்றும் பிற இடங்களில் பணியாளர்களுக்கான அறை ஆர்டர்களை இயக்கவும்.", + "lang_room_venue": "இடம்" } diff --git a/languages/te.json b/languages/te.json index bf50acd0c..ec5d159d2 100644 --- a/languages/te.json +++ b/languages/te.json @@ -5001,5 +5001,22 @@ "lang_text_style": "టెక్స్ట్ శైలి", "lang_this_product_is_hidden_from_sales_your_bas": "ఈ ఉత్పత్తి అమ్మకాల నుండి దాగి ఉంది. మీ బుట్ట మార్చబడలేదు.", "lang_this_window_belongs_to_one_branch_open_ano": "ఈ విండో ఒక శాఖకు చెందినది. ప్రధాన విండో నుండి మరొక అవుట్లెట్ తెరవండి.", - "lang_total_labels": "మొత్తం లేబుల్స్" + "lang_total_labels": "మొత్తం లేబుల్స్", + "lang_rooms": "గదులు", + "lang_room_number": "గది సంఖ్య", + "lang_room_order": "రూమ్ ఆర్డర్", + "lang_room_service_charge": "రూమ్ సర్వీస్ ఛార్జ్", + "lang_room_charge_agreement": "అతిథి ఈ అదనపు ఛార్జీకి అంగీకరించాడు", + "lang_room_destination_required": "వేదికను ఎంచుకోండి మరియు గది సంఖ్యను నమోదు చేయండి.", + "lang_room_charge_tax_unavailable": "రూమ్ ఛార్జ్ పన్ను అందుబాటులో లేదు. పన్ను సెట్టింగులను సమీక్షించండి.", + "lang_staff_room_orders": "సిబ్బంది గది ఆర్డర్లను అనుమతించండి", + "lang_staff_room_prices": "స్టాఫ్ రూమ్ ఆర్డర్లు సాధారణ మెను ధరలను ఉపయోగిస్తాయి. వెబ్ మెను మార్కప్ మరియు వేదిక కమిషన్ వేరు వేరు.", + "lang_room_charge_offer": "అదనపు గది ఛార్జీని అందించండి", + "lang_charge_name": "ఛార్జ్ పేరు", + "lang_apply_default_tax": "డిఫాల్ట్ పన్నును వర్తించండి", + "lang_room_charge_basis": "డిస్కౌంట్ తర్వాత ఆహార శాతం, పన్నుకు ముందు. అతిథి ఒప్పందంతో మాత్రమే దరఖాస్తు చేసుకోండి; సిబ్బంది దానిని బిల్లు నుండి తీసివేయవచ్చు.", + "lang_room_charge_rate": "{0}: ఆహారంలో {1}% డిస్కౌంట్ల తర్వాత, పన్ను ముందు.", + "lang_room_charge_final": "తుది బిల్లులో చేర్చబడింది.", + "lang_room_orders_enable_help": "సెట్టింగ్స్ → రెస్టారెంట్ → హోటల్స్ మరియు ఇతర వేదికలలో సిబ్బంది గది ఆర్డర్లను ప్రారంభించండి.", + "lang_room_venue": "వేదిక" } diff --git a/languages/th.json b/languages/th.json index 11671fe10..d6fa1662c 100644 --- a/languages/th.json +++ b/languages/th.json @@ -5001,5 +5001,22 @@ "lang_text_style": "รูปแบบข้อความ", "lang_this_product_is_hidden_from_sales_your_bas": "ผลิตภัณฑ์นี้ถูกซ่อนอยู่จากการขายตะกร้าของคุณยังไม่ได้รับการเปลี่ยนแปลง", "lang_this_window_belongs_to_one_branch_open_ano": "หน้าต่างนี้เป็นของสาขาหนึ่งเปิดเต้ารับอื่นจากหน้าต่างหลัก", - "lang_total_labels": "ป้ายกำกับทั้งหมด" + "lang_total_labels": "ป้ายกำกับทั้งหมด", + "lang_rooms": "ห้องพัก", + "lang_room_number": "หมายเลขห้อง", + "lang_room_order": "ลำดับห้อง", + "lang_room_service_charge": "ค่าบริการรูมเซอร์วิส", + "lang_room_charge_agreement": "ผู้เข้าพักตกลงรับค่าบริการเพิ่มเติม", + "lang_room_destination_required": "เลือกสถานที่และป้อนหมายเลขห้อง", + "lang_room_charge_tax_unavailable": "ไม่สามารถใช้ภาษีค่าห้องพักตรวจสอบการตั้งค่าภาษี", + "lang_staff_room_orders": "อนุญาตให้สั่งซื้อห้องพนักงาน", + "lang_staff_room_prices": "การสั่งซื้อห้องพนักงานใช้ราคาเมนูปกติมาร์กอัปเมนูเว็บและค่าคอมมิชชั่นสถานที่แยกต่างหาก", + "lang_room_charge_offer": "มีค่าธรรมเนียมเพิ่มเติมสำหรับห้องพัก", + "lang_charge_name": "ชื่อค่าใช้จ่าย", + "lang_apply_default_tax": "ใช้ภาษีเริ่มต้น", + "lang_room_charge_basis": "เปอร์เซ็นต์ของอาหารหลังส่วนลดก่อนหักภาษีสมัครด้วยข้อตกลงของผู้เข้าพักเท่านั้น พนักงานสามารถลบออกจากใบเรียกเก็บเงินได้", + "lang_room_charge_rate": "{0}: {1}% ของอาหารหลังส่วนลด ก่อนหักภาษี", + "lang_room_charge_final": "รวมอยู่ในใบเรียกเก็บเงินขั้นสุดท้าย", + "lang_room_orders_enable_help": "เปิดใช้งานการสั่งซื้อห้องพักสำหรับพนักงานใน การตั้งค่า → ร้านอาหาร → โรงแรมและสถานที่อื่นๆ", + "lang_room_venue": "สถานที่จัดงาน" } diff --git a/languages/tl.json b/languages/tl.json index c46dee831..c99cf2b64 100644 --- a/languages/tl.json +++ b/languages/tl.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Estilo ng teksto", "lang_this_product_is_hidden_from_sales_your_bas": "Ang produktong ito ay nakatago mula sa mga benta. Hindi pa binago ang iyong basket.", "lang_this_window_belongs_to_one_branch_open_ano": "Ang window na ito ay kabilang sa isang sangay. Buksan ang isa pang outlet mula sa pangunahing window.", - "lang_total_labels": "Kabuuang mga label" + "lang_total_labels": "Kabuuang mga label", + "lang_rooms": "Mga silid", + "lang_room_number": "Numero ng kuwarto", + "lang_room_order": "Order ng kuwarto", + "lang_room_service_charge": "Singil sa serbisyo sa kuwarto", + "lang_room_charge_agreement": "Sumang-ayon ang guest sa karagdagang bayad na ito", + "lang_room_destination_required": "Pumili ng lugar at ipasok ang numero ng kuwarto.", + "lang_room_charge_tax_unavailable": "Hindi magagamit ang buwis sa singil sa kuwarto. Suriin ang mga setting ng buwis.", + "lang_staff_room_orders": "Payagan ang mga order ng silid ng kawani", + "lang_staff_room_prices": "order ng staff room ay gumagamit ng mga normal na presyo ng menu. Ang markup ng web menu at komisyon ng lugar ay hiwalay.", + "lang_room_charge_offer": "Mag-alok ng karagdagang bayad sa kuwarto", + "lang_charge_name": "Pangalan ng singil", + "lang_apply_default_tax": "Ilapat ang default na buwis", + "lang_room_charge_basis": "Porsyento ng pagkain pagkatapos ng mga diskwento, bago ang buwis. Mag-apply lamang sa kasunduan sa panauhin; maaaring alisin ito ng kawani mula sa bill.", + "lang_room_charge_rate": "{0}: {1}% ng pagkain pagkatapos ng mga diskwento, bago ang buwis.", + "lang_room_charge_final": "Kasama sa pangwakas na panukalang panukalang.", + "lang_room_orders_enable_help": "Paganahin ang mga order ng kuwarto ng staff sa Mga Setting → Restaurant → Hotel at iba pang mga lugar.", + "lang_room_venue": "Lugar" } diff --git a/languages/tr.json b/languages/tr.json index 4b89c6efb..4580c6a89 100644 --- a/languages/tr.json +++ b/languages/tr.json @@ -4923,5 +4923,22 @@ "lang_text_style": "Metin stili", "lang_this_product_is_hidden_from_sales_your_bas": "Bu ürün satışlardan gizlenmiştir. Sepetiniz değiştirilmedi.", "lang_this_window_belongs_to_one_branch_open_ano": "Bu pencere bir şubeye aittir. Ana pencereden başka bir çıkış açın.", - "lang_total_labels": "Toplam etiket" + "lang_total_labels": "Toplam etiket", + "lang_rooms": "Odalar", + "lang_room_number": "Oda numarası", + "lang_room_order": "Oda siparişi", + "lang_room_service_charge": "Oda servisi ücreti", + "lang_room_charge_agreement": "Konuk bu ek ücreti kabul etti", + "lang_room_destination_required": "Bir mekan seçin ve oda numarasını girin.", + "lang_room_charge_tax_unavailable": "Oda ücreti vergisi mevcut değildir. Vergi ayarlarını gözden geçirin.", + "lang_staff_room_orders": "Personel odası siparişlerine izin ver", + "lang_staff_room_prices": "Personel odası siparişleri normal menü fiyatlarını kullanır. Web menüsü işaretlemesi ve mekan komisyonu ayrıdır.", + "lang_room_charge_offer": "Ek oda ücreti teklif edin", + "lang_charge_name": "Ücret adı", + "lang_apply_default_tax": "Varsayılan vergi uygula", + "lang_room_charge_basis": "İndirimlerden sonra yiyecek yüzdesi, vergi öncesi. Sadece misafir sözleşmesi ile başvurun; personel faturadan kaldırabilir.", + "lang_room_charge_rate": "{0}: Vergi öncesi indirimlerden sonra yiyeceklerin {1} %'si.", + "lang_room_charge_final": "Son faturaya dahildir.", + "lang_room_orders_enable_help": "Ayarlar → Restoran → Oteller ve diğer mekanlarda personel odası siparişlerini etkinleştirin.", + "lang_room_venue": "Mekan" } diff --git a/languages/uk.json b/languages/uk.json index a5ef6ed26..0657f69f7 100644 --- a/languages/uk.json +++ b/languages/uk.json @@ -4920,5 +4920,22 @@ "lang_text_style": "Стиль тексту", "lang_this_product_is_hidden_from_sales_your_bas": "Цей товар прихований від продажів. Ваш кошик не змінено.", "lang_this_window_belongs_to_one_branch_open_ano": "Це вікно належить одній гілці. Відкрийте іншу розетку з головного вікна.", - "lang_total_labels": "Всього етикеток" + "lang_total_labels": "Всього етикеток", + "lang_rooms": "Номери", + "lang_room_number": "Номер кімнати", + "lang_room_order": "Замовлення номерів", + "lang_room_service_charge": "Плата за обслуговування номерів", + "lang_room_charge_agreement": "Гість погодився на цю додаткову плату", + "lang_room_destination_required": "Виберіть місце проведення та введіть номер кімнати.", + "lang_room_charge_tax_unavailable": "Податок за номер недоступний. Перегляньте параметри податку.", + "lang_staff_room_orders": "Дозволити замовлення кабінету персоналу", + "lang_staff_room_prices": "Замовлення номерів персоналу використовують звичайні ціни меню. Розмітка веб-меню та комісія місця проведення окремі.", + "lang_room_charge_offer": "Пропонуємо додаткову плату за номер", + "lang_charge_name": "Назва зарядки", + "lang_apply_default_tax": "Застосувати податок за замовчуванням", + "lang_room_charge_basis": "Відсоток їжі після знижок, до оподаткування. Подайте заявку тільки за домовленістю гостя, співробітники можуть зняти його з рахунку.", + "lang_room_charge_rate": "{0}: {1}% продуктів харчування після знижок, до оподаткування.", + "lang_room_charge_final": "Включено до остаточного рахунку.", + "lang_room_orders_enable_help": "Увімкніть замовлення номерів персоналу в налаштуваннях «Ресторан» «Готелі та інші заклади».", + "lang_room_venue": "Місце проведення" } diff --git a/languages/ur.json b/languages/ur.json index fb0fea406..d267b169c 100644 --- a/languages/ur.json +++ b/languages/ur.json @@ -4923,5 +4923,22 @@ "lang_text_style": "ٹیکسٹ اسٹائل", "lang_this_product_is_hidden_from_sales_your_bas": "یہ مصنوعات فروخت سے پوشیدہ ہے۔ آپ کی ٹوکری تبدیل نہیں کی گئی ہے۔", "lang_this_window_belongs_to_one_branch_open_ano": "یہ ونڈو ایک شاخ سے تعلق رکھتی ہے۔ مرکزی ونڈو سے ایک اور آؤٹ لیٹ کھولیں۔", - "lang_total_labels": "کل لیبلز" + "lang_total_labels": "کل لیبلز", + "lang_rooms": "کمرے", + "lang_room_number": "کمرے کا نمبر", + "lang_room_order": "کمرے کا آرڈر", + "lang_room_service_charge": "کمرے سروس چارج", + "lang_room_charge_agreement": "مہمان اس اضافی چارج پر اتفاق کیا", + "lang_room_destination_required": "ایک مقام منتخب کریں اور کمرے کا نمبر درج کریں۔", + "lang_room_charge_tax_unavailable": "کمرے چارج ٹیکس دستیاب نہیں ہے۔ ٹیکس کی ترتیبات کا جائزہ لیں۔", + "lang_staff_room_orders": "عملے کے کمرے کے آرڈرز", + "lang_staff_room_prices": "اسٹاف روم آرڈرز عام مینو قیمتیں استعمال کرتے ہیں۔ ویب مینو مارک اپ اور وینیو کمیشن الگ ہیں۔", + "lang_room_charge_offer": "کمرے کا اضافی چارج پیش کریں", + "lang_charge_name": "چارج کا نام", + "lang_apply_default_tax": "ڈیفالٹ ٹیکس لگائیں", + "lang_room_charge_basis": "ٹیکس سے پہلے، چھوٹ کے بعد کھانے کا فیصد۔ صرف مہمان معاہدے کے ساتھ درخواست دیں۔ عملہ اسے بل سے ہٹا سکتا ہے۔", + "lang_room_charge_rate": "{0}: ٹیکس سے پہلے، چھوٹ کے بعد کھانے کا {1} فیصد", + "lang_room_charge_final": "حتمی بل میں شامل ہے۔", + "lang_room_orders_enable_help": "سیٹنگز → ریستوراں → ہوٹلوں اور دیگر مقامات میں عملے کے کمرے کے آرڈرز کو فعال کریں۔", + "lang_room_venue": "مقام" } diff --git a/languages/vi.json b/languages/vi.json index aa22f52cb..ba1b3a491 100644 --- a/languages/vi.json +++ b/languages/vi.json @@ -4923,5 +4923,22 @@ "lang_text_style": "Kiểu văn bản", "lang_this_product_is_hidden_from_sales_your_bas": "Sản phẩm này bị ẩn khỏi bán hàng. Giỏ hàng của bạn chưa được thay đổi.", "lang_this_window_belongs_to_one_branch_open_ano": "Cửa sổ này thuộc về một nhánh. Mở một ổ cắm khác từ cửa sổ chính.", - "lang_total_labels": "Tổng số nhãn" + "lang_total_labels": "Tổng số nhãn", + "lang_rooms": "Phòng", + "lang_room_number": "Số phòng", + "lang_room_order": "Đặt phòng", + "lang_room_service_charge": "Phí dịch vụ phòng", + "lang_room_charge_agreement": "Khách đã đồng ý với khoản phụ phí này", + "lang_room_destination_required": "Chọn một địa điểm và nhập số phòng.", + "lang_room_charge_tax_unavailable": "Thuế phí phòng không có sẵn. Xem lại cài đặt thuế.", + "lang_staff_room_orders": "Cho phép đặt phòng nhân viên", + "lang_staff_room_prices": "đặt phòng nhân viên sử dụng giá thực đơn bình thường. Đánh dấu menu web và hoa hồng địa điểm là riêng biệt.", + "lang_room_charge_offer": "Cung cấp thêm phí phòng", + "lang_charge_name": "Tên phí", + "lang_apply_default_tax": "Áp dụng thuế mặc định", + "lang_room_charge_basis": "Tỷ lệ thực phẩm sau khi giảm giá, trước thuế. Chỉ áp dụng với sự đồng ý của khách; nhân viên có thể xóa nó khỏi hóa đơn.", + "lang_room_charge_rate": "{0}: {1}% thực phẩm sau khi giảm giá, trước thuế.", + "lang_room_charge_final": "Bao gồm trong hóa đơn cuối cùng.", + "lang_room_orders_enable_help": "Bật đơn đặt hàng phòng nhân viên trong Cài đặt “Nhà hàng”” Khách sạn và các địa điểm khác.", + "lang_room_venue": "Địa điểm" } diff --git a/languages/zh-CN.json b/languages/zh-CN.json index a3de6f49f..4020d2169 100644 --- a/languages/zh-CN.json +++ b/languages/zh-CN.json @@ -4923,5 +4923,22 @@ "lang_text_style": "文字风格", "lang_this_product_is_hidden_from_sales_your_bas": "该产品隐藏在销售范围内。您的购物篮未更改。", "lang_this_window_belongs_to_one_branch_open_ano": "此窗口属于一个分支。从主窗口打开另一个插座。", - "lang_total_labels": "标签总数" + "lang_total_labels": "标签总数", + "lang_rooms": "房间", + "lang_room_number": "房间号", + "lang_room_order": "房间订单", + "lang_room_service_charge": "客房服务费", + "lang_room_charge_agreement": "房客同意这笔额外费用", + "lang_room_destination_required": "选择场地并输入房间号。", + "lang_room_charge_tax_unavailable": "房费税不可用。查看税收设置。", + "lang_staff_room_orders": "允许员工订购房间", + "lang_staff_room_prices": "员工房间的订单使用正常的菜单价格。网络菜单标记和场地佣金是分开的", + "lang_room_charge_offer": "提供额外的房费", + "lang_charge_name": "收费名称", + "lang_apply_default_tax": "应用默认税", + "lang_room_charge_basis": "折扣后、税前食物的百分比。仅在房客同意的情况下申请;工作人员可以将其从账单中删除。", + "lang_room_charge_rate": "{0}:折扣后、税前食品的 {1}%。", + "lang_room_charge_final": "包含在最终账单中。", + "lang_room_orders_enable_help": "在 “设置” “餐厅” “酒店和其他场所” 中启用员工房间订单。", + "lang_room_venue": "地点" } diff --git a/languages/zh-TW.json b/languages/zh-TW.json index 37af266df..e5de1f47a 100644 --- a/languages/zh-TW.json +++ b/languages/zh-TW.json @@ -4923,5 +4923,22 @@ "lang_text_style": "文字樣式", "lang_this_product_is_hidden_from_sales_your_bas": "此產品隱藏在銷售中。您的購物籃沒有更改。", "lang_this_window_belongs_to_one_branch_open_ano": "此視窗屬於一個分支。從主視窗開啟另一個插座。", - "lang_total_labels": "標籤總數" + "lang_total_labels": "標籤總數", + "lang_rooms": "房間", + "lang_room_number": "房間編號", + "lang_room_order": "房間順序", + "lang_room_service_charge": "客房服務費", + "lang_room_charge_agreement": "客人同意支付此額外費用", + "lang_room_destination_required": "選擇場地並輸入房間號碼。", + "lang_room_charge_tax_unavailable": "客房費稅不適用。檢閱稅捐設定。", + "lang_staff_room_orders": "允許員工房間訂單", + "lang_staff_room_prices": "員工房間訂單使用正常菜單價格。網頁選單標記和場地佣金分開。", + "lang_room_charge_offer": "提供額外房費", + "lang_charge_name": "費用名稱", + "lang_apply_default_tax": "套用預設稅", + "lang_room_charge_basis": "折扣後的食物百分比,稅前。僅在客人同意下申請;工作人員可以將其從賬單中刪除。", + "lang_room_charge_rate": "{0}:折扣後的 {1}% 食物,稅前。", + "lang_room_charge_final": "包括在最終賬單中。", + "lang_room_orders_enable_help": "在「設定」「餐廳」「酒店和其他場地」中啟用員工房間訂單。", + "lang_room_venue": "場地" } diff --git a/src/escpos-receipt.js b/src/escpos-receipt.js index fbe26194a..6b3a89654 100644 --- a/src/escpos-receipt.js +++ b/src/escpos-receipt.js @@ -898,6 +898,7 @@ function renderSale(sale, options = {}, renderer) { if (sale.totalQty) r.pairAtColumn('Total Qty', String(sale.totalQty), r.qtyColumn); if (sale.subTotal != null) r.pair('Subtotal', money(sale.subTotal)); for (const t of sale.taxes || []) r.pair(t.label, money(t.amount)); + for (const c of sale.charges || []) r.pair(c.name || 'Charge', money(c.amount)); if (sale.discount) r.pair('Discount', '-' + money(sale.discount)); if (sale.roundOff) r.pair('Round off', money(sale.roundOff)); diff --git a/tests/channel-settings-roundtrip.test.js b/tests/channel-settings-roundtrip.test.js index b0afee3d2..24c9dbb73 100644 --- a/tests/channel-settings-roundtrip.test.js +++ b/tests/channel-settings-roundtrip.test.js @@ -172,7 +172,7 @@ test('a venue drawn on the screen is the venue that gets saved', () => { assert.strictEqual(saved.length, 1, 'the row rendered but the save could not read it'); /* Every field, because each one that fails to round-trip fails silently and each one is somebody's money or somebody's dinner. */ - assert.deepStrictEqual(plain(saved[0]), ROYAL); + assert.deepStrictEqual(plain(saved[0]), {...ROYAL, staff_orders_enabled:false, room_charge:{enabled:false,name:'Room service charge',percent:0,taxed:false}}); }); test('the markup and the commission stay two different numbers', () => { diff --git a/tests/kot-v2.test.js b/tests/kot-v2.test.js index 242e3d76a..9b2938d15 100644 --- a/tests/kot-v2.test.js +++ b/tests/kot-v2.test.js @@ -11,7 +11,7 @@ function setup(){ const sale={_id:'a'.repeat(24),table_number:'6',dine_type:'Dine-in',person_count:2,updated_date:'2026-10-05T06:00:00Z',created_date:'2026-10-05T06:00:00Z',payment_status:'Unpaid',sale_process:'KOT',sales_total:60,items:[{item_id:'tea',line_id:'original',item_name:'Tea',item_quantity:2,item_price:30}],restaurant_details:{rounds:[{id:'c0',items:[{id:'c0i0',line_key:'original',product:'tea',name:'Tea',quantity:2,served:0,remaining:2}]}]}}; let search; w.PosnicPro={i18n:{t:(_,fallback)=>fallback},local:{get:k=>({table_options:'enable',branch_id_set:'b',username:'owner',currencySign:'₹'}[k])},escapeHtml:s=>s.replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])),HideSideBarModal(){},alert:(_,m)=>errors.push(m),restaurantFeedback:{apply(){},load(){},play:k=>effects.push(k)},kot:{bindProductSearch:(_,callback)=>search=callback,editLine:(l,q)=>({product_id:l.item_id,line_id:l.line_id,quantity:q??l.item_quantity,price:l.item_price}),},kotWorkspace:{editPayload:(s,items)=>({order_id:s._id,items,seen_at:s.updated_date})}}; - for(const method of ['get','post','put'])w.PosnicPro[method]=(o,done,fail)=>{const body=typeof o.data==='string'?JSON.parse(o.data):o.data;calls.push({method,url:o.url,body});if(setup.failure&&method==='post'){fail({responseJSON:setup.failureResponse||{message:'Offline'}});return;}let data={};if(o.url==='captain/v1/tables')data={tables:[{id:'t6',tableorder_value:'6',status:'occupied',orders:[{id:sale._id}]},{id:'t7',tableorder_value:'7',status:'available',orders:[]}]};else if(o.url==='sales')data={list:sale.payment_status==='Paid'&&!sale.floor_lifecycle?[]:[sale],total:1};else if(o.url==='sales/'+sale._id)data=JSON.parse(JSON.stringify(sale));else if(o.url==='items/instantItemTax')data={id:'tax',rate:5,name:'GST'};else if(o.url==='items/instanceItemInsert')data={id:'instant',selling_price:25};done({type:'success',data});}; + for(const method of ['get','post','put'])w.PosnicPro[method]=(o,done,fail)=>{const body=typeof o.data==='string'?JSON.parse(o.data):o.data;calls.push({method,url:o.url,body});if(setup.failure&&method==='post'){fail({responseJSON:setup.failureResponse||{message:'Offline'}});return;}let data={};if(o.url==='captain/v1/tables')data={roomVenues:setup.roomVenues||[],tables:[{id:'t6',tableorder_value:'6',status:'occupied',orders:[{id:sale._id}]},{id:'t7',tableorder_value:'7',status:'available',orders:[]}]};else if(o.url==='sales')data={list:sale.payment_status==='Paid'&&!sale.floor_lifecycle?[]:[sale],total:1};else if(o.url==='sales/'+sale._id)data=JSON.parse(JSON.stringify(sale));else if(o.url==='items/instantItemTax')data={id:'tax',rate:5,name:'GST'};else if(o.url==='items/instanceItemInsert')data={id:'instant',selling_price:25};done({type:'success',data});}; w.eval(source);const app=w.PosnicPro.kot_v2; const click=async a=>{w.document.querySelector('[data-action="'+a+'"]').click();await flush();}; return {dom,w,app,calls,effects,errors,sale,click,search:()=>search,close:()=>dom.window.close()}; @@ -211,7 +211,7 @@ test('takeaway opens with guest count focused and selected, and add-again and di try { const selected=[];h.w.HTMLInputElement.prototype.select=function(){selected.push(this);}; h.app.showDataTablePage();await flush();await h.click('takeaway'); - let input=h.w.document.querySelector('dialog [name=guests]');assert.equal(input.value,'0');assert.equal(h.w.document.activeElement,input);assert.ok(selected.includes(input)); + let input=h.w.document.querySelector('dialog [name=guests]');assert.equal(input.value,'');assert.equal(h.w.document.activeElement,input);assert.ok(selected.includes(input)); h.w.document.querySelector('dialog').close();h.app.state.selected=h.sale._id;await h.app.refresh(); await h.click('again');input=h.w.document.querySelector('dialog [name=qty]');assert.equal(input.value,'1');assert.equal(h.w.document.activeElement,input);assert.ok(selected.includes(input)); h.w.document.querySelector('dialog').close();await h.click('discount');input=h.w.document.querySelector('dialog [name=amount]');assert.equal(h.w.document.activeElement,input);assert.ok(selected.includes(input)); @@ -256,3 +256,29 @@ test('paid takeaway remains selectable without offering payment or price editing assert.match(h.w.document.querySelector('[data-action=serveAll]').textContent,/Given to customer/); const filters=JSON.parse(h.calls.find(c=>c.url==='sales').body.filters);assert.equal(filters.floor_closed_at.$exists,false);assert.ok(filters.$or.some(f=>f.floor_lifecycle===true));h.close(); }); + +test('choosing table and guests does not navigate until Next; pax can be omitted',async()=>{ + const h=setup();h.app.showDataTablePage();await flush();await h.click('new'); + const d=h.w.document.querySelector('.kv2-seating-dialog'); + d.querySelector('[value=t7]').click();d.querySelector('[data-guests="3"]').click();await flush(); + assert.equal(h.app.state.draft,null);assert.equal(d.open,true); + d.querySelector('[name=guests]').value=''; + assert.equal(d.querySelector('[name=guests]').required,false); + d.querySelector('[type=submit]').click();await flush(); + assert.equal(h.app.state.draft.table,'7');assert.equal(h.app.state.draft.guests,0); + h.close(); +}); + +test('rooms accept a typed number but wait for Next and preserve charge agreement',async()=>{ + setup.roomVenues=[{code:'hotel',name:'Our hotel',room_charge:{enabled:true,percent:5,name:'Room service'}}]; + const h=setup();try { + h.app.showDataTablePage();await flush();await h.click('rooms'); + const d=h.w.document.querySelector('dialog');d.querySelector('[name=unit]').value='35B'; + assert.equal(h.app.state.draft,null); + d.querySelector('[name=acceptCharge]').checked=true; + d.querySelector('[type=submit]').click();await flush(); + assert.equal(h.app.state.draft.unit,'35B');assert.equal(h.app.state.draft.roomChargeAccepted,true); + h.search()({item_id:'new',selling_price:10,item_name:'Naan'},1,()=>{});await h.click('send'); + const request=h.calls.find(c=>c.url==='sales/qrOrder');assert.equal(request.body.venue,'hotel');assert.equal(request.body.unit,'35B');assert.equal(request.body.room_charge_accepted,true); + } finally {setup.roomVenues=null;h.close();} +}); diff --git a/tests/one-order-per-table-by-default.test.js b/tests/one-order-per-table-by-default.test.js index 87926ee61..e8515779e 100644 --- a/tests/one-order-per-table-by-default.test.js +++ b/tests/one-order-per-table-by-default.test.js @@ -149,7 +149,7 @@ test('and a shop with no tables never reaches the query either', () => { * runs table service, so a retail counter whose printed code carried a * segment refused a customer with "Table 5 already has an open order". */ - assert.match(SALE_REPO, /const runsTableService = branchDoc\.table_options === true;/); + assert.match(SALE_REPO, /const runsTableService = branchDoc\.table_options === true && !staffRoom;/); assert.match(SALE_REPO, /if \(runsTableService && openTableLimit > 0 && wantsTable\) \{/); });