You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Gulf POS (GPOS) is an offline-capable Point of Sale (POS) system for ERPNext, designed to work seamlessly on Windows.
Version
Current Version:v2.1.1
Features
Offline billing with local database sync
Fast UI optimized for retail workflows
Automatic sync with ERPNext when connection is restored
Works with ERPNext v15
Support for multi-user and multi-terminal setups
Hardware integration: Barcode scanner, printers
What's New in v2.1.1
Add POS Profiles to Offline Users List
Updated gpos.gpos.pos.getOfflinePOSUsers to include POS Profiles for better control and filtering of offline users.
Changed System User to Offline User in Shift Opening
In gpos.gpos.pos_shift.opening_shift, system user logic now uses the offline user, improving shift audit consistency.
Datetime Format Standardization
Both opening_shift and closing_shift now use the datetime format: YYYY-MM-DD HH:mm:ss for improved compatibility and logging.
Installation
# Navigate to your bench directorycd /opt/frappe-bench
# Get the app (use correct repo and branch)
bench get-app gpos https://github.com/ERPGulf/GPos.git --branch v2.1.1
# Install the app on your site
bench --site your-site-name install-app gpos
Core API (pos.py & pos_shift.py)
The gpos/gpos/pos.py and gpos/gpos/pos_shift.py modules expose the whitelisted (REST-callable) API endpoints that power the GPOS point-of-sale application: authentication, item catalog sync, invoicing, customers, loyalty points, coupons, promotions, ZATCA/e-invoicing settings, SMS/OTP, and shift (till) management.
All endpoints are registered with @frappe.whitelist(...) and are callable via Frappe's REST API at:
Endpoints marked allow_guest=True do not require a logged-in session (they typically do their own token/credential validation). Endpoints without allow_guest=True require a valid Frappe session/OAuth token.
Exchanges API key/secret for an OAuth password-grant token. app_key is base64-encoded and mapped to an OAuth Client record to look up client_id/client_secret, then proxies a token request to Frappe's own OAuth2 get_token endpoint.
Username/password login variant of the above; also logs the login attempt and returns basic user info (User record) and the branch from Claudion POS setting.
create_refresh_token(refresh_token)
Guest
Exchanges a refresh token for a new access token via Frappe's OAuth2 endpoint.
Returns items grouped by item group, with prices (from the POS Profile's selling price list), UOMs, barcodes, and Arabic/English name variants. If last_updated_time is given, only returns items/prices modified since then (delta sync). If pos_profile is given, restricts to that profile's item groups and price list.
Paginated variant of get_items (no POS profile filtering, no disabled/item-group-disabled logic).
get_valuation_rate(itemcode)
Guest
Quick lookup of an item's valuation rate from the Bin doctype. Returns the raw value (or None), not wrapped in Response.
get_loyalty_item(item)
Guest
Given an item, returns its Item Group's custom_loyalty_percentage. Returns {"error": ...} if the item/item group isn't found; otherwise a plain (non-Response-wrapped) value.
Current customer creation endpoint. Creates a Customer with optional linked Address and POS Profile associations, enforcing uniqueness of VAT number and mobile number. Returns 409 if either already exists.
customer_list_old(id=None, pos_profile=None)
Guest
Deprecated — use customer_list instead. Lists customers, optionally filtered by name and restricted to those assigned to a POS Profile.
customer_list(id=None, pos_profile=None)
Logged-in
Current customer listing endpoint replacing customer_list_old. Optionally filters by exact id and restricts to customers linked to pos_profile, flagging the profile's default customer. Returns 404 if none found.
Creates and submits a Sales Invoice return (credit note) against an original invoice. Mirrors create_invoice's idempotency pattern (lock_invoice_numbers, duplicate checks by offline invoice number/unique id), resolves the original invoice via return_against or offline invoice number, maps payment modes, optionally attaches xml/qr_code uploads, reverses loyalty points (handle_loyalty_points_for_return), and updates the ZATCA Multiple Setting's PIH. Returns 404/409 for validation issues, 400/500 for ValidationError/Exception.
get_invoice_details(invoice_number)
Logged-in
Fetches full details of an existing Sales Invoice by name: header info, items (with tax rate), taxes, payments, offline invoice metadata, and xml/qr_code attachments. 404 if not found.
warehouse_details(id=None)
Guest
Given a POS/Sales Invoice name (or all invoices), returns per-item warehouse assignment plus the warehouse's contact/address info.
POS Configuration & Hardware
Function
Auth
Description
pos_setting(machine_name, pos_profile=None)
Guest
Aggregates configuration needed to bootstrap a POS terminal: Claudion POS setting, Scale Settings (barcode-scale parsing rules), ZATCA Multiple Setting/Company e-invoicing certificates and keys (base64-encoded), sales taxes, CardPay settings, and branch/address details — keyed off machine_name (ZATCA multiple setting) and pos_profile.
getOfflinePOSUsers(id=None, offset=0, limit=500)
Guest
Returns offline POS login accounts (POS Offline Users) with decrypted+base64-encoded passwords, their assigned POS Profiles, and resolved print template/format HTML.
add_user_key(user_key, user_name)
Logged-in
Sets a user_key custom field on a User.
user_login_details(user, log_in, log_out)
Guest
Records a login/logout event as a user login details document.
Loyalty & Promotions
Function
Auth
Description
get_loyalty_points(customer_number)
Guest
Computes a customer's available loyalty point balance (sum of debits minus credits from Loyalty Point Entry Gpos, excluding used/expired points), floored at zero.
expire_loyalty_points()
Guest
Batch/cron job endpoint — marks all Loyalty Point Entry Gpos records past their expiry_date as expired (is_expired = 1). No response body; intended to be scheduler-triggered, not client-called.
get_promotion_list(pos_profile)
Guest
Returns active promotions linked to a POS Profile, each with an items array (sale/cost price, discount type/percentage/amount, price after discount, is_free flag, uom). Used by create_invoice to validate free-item-only carts. 404 if the profile isn't linked to any promotion.
get_coupon_details(coupon_code, branch)
Logged-in
Validates a coupon for a branch (existence, branch eligibility, validity window) and returns its linked Pricing Rule discount info plus applicable item groups/items. Returns 404 (invalid code), 403 (not valid for branch), 400 (not yet valid), or 410 (expired) as appropriate.
claim_coupon(coupon_code, user_branch, uuid)
Logged-in
Atomically claims/redeems one coupon use for a branch. Uses cache-based idempotency keyed by uuid plus cached usage counters to avoid double-claims under concurrent requests, then persists the incremented used count. Returns 200 with claimed:0 if already claimed (idempotent) or claimed:1 if newly claimed; 400 if branch invalid or usage limit reached.
get_coupons_by_branch(branch)
Logged-in
Lists all currently valid, non-exhausted coupons for a branch with discount/pricing rule details. 400 if branch missing.
Records a card-payment terminal transaction log (Credit Card Machine Log doctype) for auditing/reconciliation. Returns the created log's fields, or 500 on exception.
OTP / SMS
Function
Auth
Description
generate_otp(mobile_no)
Guest
Generates a 6-digit OTP for loyalty-point redemption, caches it (5 min TTL) keyed by mobile number, and sends it via SMS (send_test_sms, bilingual Arabic/English message). Returns {"otp": otp} — note the OTP is also returned in the response, not just sent by SMS.
validate_otp(mobile_no, otp)
Guest
Validates a previously generated OTP against the cached value and deletes it on success. Returns 404 if expired/not found/mismatched, 200 on match.
generate_sms_otp(mobile_no)
Guest
Similar to generate_otp but with an English-only message; also returns the SMS gateway's status_code/response. 500 on exception.
send_test_sms(phone, message)
Guest
Sends an SMS via a third-party HTTP gateway configured in Claudion POS setting (app key/secret, URL, sender, number ISO), logging success/failure to whatsapp saudi success log or frappe.log_error. Returns {"status_code": ..., "response": ...}.
send_message(mobile_no, otp)
Guest
Sends a WhatsApp OTP message via a "Whatsapp Saudi" gateway, normalizing the phone number first. Returns the raw requests response object. Note: contains a bug where phoneNumber is only defined inside the for receipt in recipients loop.
get_receiver_phone_number(number)
n/a (internal)
Not whitelisted — internal helper called by send_message; normalizes a raw phone number into international format (strips +/-/leading zeros, prefixes Saudi country code 966 when needed).
Shift Status & Sync Logging
Function
Auth
Description
get_shift_status(shift_id)
Guest
Looks up a POS Opening Shift/POS Closing Shift by id and returns its status. 400 if shift_id missing, 404 if not found in either doctype.
Writes a debug/sync log entry (gpos logs doctype) from the offline client, gated by a global debug-logging setting; details is scanned for a "Resource:" line to detect credit-note syncs. Returns 400 if logging disabled, 409 if sync_id already logged, 200 on success.
get_pos_offers()
Logged-in
Returns all currently valid (non-cancelled, non-disabled, within date range) POS Offer records — title, discount type/amount/percentage, item, thresholds, applicable warehouse/pos_profile/company, validity, auto-apply flag. Returned as a raw list, not Response-wrapped.
File Uploads (generic Frappe helpers)
Function
Auth
Description
upload_file()
Logged-in
Standard Frappe multi-file upload endpoint; validates permissions, optionally optimizes images, and attaches files to a doctype/field.
optimize_image_content(content, content_type)
Logged-in
Resizes/optimizes image bytes using Frappe's optimize_image utility.
Opens a new shift. Expects a balance_details form field (JSON list of {mode_of_payment, opening_amount}), maps offline payment mode names to the POS Profile's configured modes, resolves an offline username to its linked User if needed, then creates and submits a POS Opening Shift document. Returns the shift's sync id and opening balances.
Closes a shift. Expects payment_reconciliation (JSON list of {mode_of_payment, opening_amount, expected_amount, closing_amount}) and optional details (invoice/return/cash/bank totals). If the referenced opening shift is already closed, returns the existing POS Closing Shift instead of creating a duplicate. Otherwise creates and submits a new POS Closing Shift, dropping any loyalty payment rows.
get_pos_profiles_with_users()
Guest
Returns every POS Profile along with the list of users it's applicable to.
format_datetime_safe(value)
n/a (internal)
Helper that normalizes a datetime/date/date-string into a "%Y-%m-%d %H:%M:%S" string for JSON responses.
build_closing_shift_response(doc)
n/a (internal)
Helper that shapes a POS Closing Shift document into the JSON response format used by closing_shift.
parse_json_field(field)
Guest
Shared helper — parses a JSON string form field into a Python object.
Notes / Gotchas
Both opening_shift and closing_shift re-read several parameters from frappe.form_dict rather than trusting the typed function arguments directly (e.g. name, balance_details, payment_reconciliation) — a pattern also used throughout pos.py's create_invoice.
closing_shift treats loyalty as a payment mode to be excluded from cash/bank reconciliation.
Errors are logged via frappe.log_error and returned as JSON with 4xx/5xx status codes rather than raising, so API clients must check the HTTP status/error field rather than relying on exceptions.
About
Offline POS app for ERPNext - backend for windows APP