Ownware
Home›Vendra›Manual
Vendra · Manual

Vendra Manual, as shipped in the download

Vendra — User Manual

A self-hosted point-of-sale and inventory system. Version 3.1.4 [src: app/controllers/v3.php:78].

About this manual

Every statement here was written by reading Vendra's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug — the markers exist so you can check rather than take our word for it.

Nothing is described that the code does not do. Where something is deliberately limited, that is said plainly rather than left out.

What Vendra is, and what runs it

Vendra is a till and a stock ledger in one application, for a single shop. Money is held as integer cents throughout, so no total is ever the result of floating-point arithmetic [src: app/controllers/v3.php:80].

It runs on PHP and stores its data in either SQLite or MySQL; the driver is chosen in configuration and defaults to SQLite [src: app/src/Database.php:19]. There is no build step and no separate migration command.

Signing in and where you land

Sign in at /login [src: app/controllers/pos.php:43]. After a successful password sign-in you are sent to the application root [src: app/controllers/pos.php:34]. If two-step verification is enrolled you are sent to /login/2fa first [src: app/controllers/pos.php:32].

Single sign-on is available as an alternative route in, and it does one thing the local form does not: it sends a cashier straight to /pos and everyone else to the dashboard [src: app/controllers/v3.php:741].

Users, roles and what each may do

Vendra has four roles: cashier, viewer, manager and admin [src: app/src/Auth.php:67].

Two of those are older than the other two, and the distinction matters when you upgrade. Vendra originally had only admin and cashier [src: app/src/Auth.php:2]. The permission ladder added viewer and manager as an extension rather than a replacement, and existing installations are untouched by it — every stored role is still admin or cashier, both keep exactly the rights they had, and the two new roles exist only once somebody assigns them [src: app/src/Auth.php:53].

The source sets the four out in a comment, and they divide like this: the cashier is the shop floor, with the till, holds and returns but no catalogue, no money reports and no administration; the viewer is a read-only back-office seat, the accountant who sees reports and lists and changes nothing; the manager holds catalogue, purchases, stock, customers and reports, but not users, settings, API keys or restore; and the admin holds everything, including the things that can lock people out or leak credentials [src: app/src/Auth.php:45].

What each role actually holds

The table below was produced by evaluating Vendra's own permission map rather than by reading it, against the seventeen permissions the controllers really enforce [src: app/src/Auth.php:67].

Permissioncashierviewermanageradmin
pos.useyes—yesyes
sale.createyes—yesyes
product.viewyesyesyesyes
catalog.view—yesyesyes
product.edit——yesyes
customer.viewyesyesyesyes
customer.edityes—yesyes
purchase.view—yesyesyes
purchase.edit——yesyes
stock.view—yesyesyes
stock.verify——yesyes
stock.adjust——yesyes
report.view—yesyesyes
audit.view—yesyesyes
view.save——yesyes
user.edit———yes
settings.edit———yes

Two rows in that table are deliberate and worth understanding, because getting either wrong would loosen access quietly.

A cashier holds product.view but not catalog.view. The till has to be able to look a product up, and so does the reporting tool described later — but the back-office catalogue screen is gated on catalog.view instead, precisely so that the right to look up a price does not hand the shop floor the back office [src: app/src/Auth.php:58].

A cashier also holds customer.view and customer.edit. That is not a new grant: the till has always been able to add a customer mid-sale, and the permission map records the behaviour that already existed rather than introducing it [src: app/src/Auth.php:63]. The customer list is gated on being signed in at all, for the same reason [src: app/controllers/admin.php:324].

Unknown or legacy roles fall through the same map rather than being special-cased [src: app/src/Auth.php:80].

The till

Opening a register

A shift begins by opening a register session with the cash you are starting with [src: app/src/Registers.php:22]. Money paid in or out during the shift — a float top-up, a supplier paid from the drawer — is recorded against the session with a type, an amount and a reason [src: app/src/Registers.php:42].

Ringing a sale

A sale is quoted first and then checked out [src: app/src/Sales.php:22]. Checkout takes the lines, a sale-level discount, the payments, the session, the user, an optional customer and a note [src: app/src/Sales.php:97].

Payments are an array, not a single value, which is the important detail: one sale can be settled across several tenders, so a customer may pay part in cash and the rest by card. The methods carried in the source are cash, card and other [src: app/src/Sales.php:97].

Holding a sale

A sale in progress can be parked and picked up again later, which is what you want when somebody has forgotten their wallet and there is a queue. Holds are created, listed, resumed and deleted from the till, and all four actions require the till permission [src: app/controllers/pos.php:136].

Returns

Items are returned against the original sale, with a method and a reason recorded [src: app/src/Sales.php:176].

Receipts

A receipt is rendered as text from the sale, its items, its payments and your store settings [src: app/src/Sales.php:235].

Closing the register

Closing takes the counted cash and computes over or short as counted minus expected, storing the counted figure, the expected figure and the difference between them [src: app/src/Registers.php:82]. The session summary is available before you close [src: app/src/Registers.php:53].

Closing produces a Z-report [src: app/controllers/pos.php:219]. Access to it follows a rule worth knowing: you may read a Z-report if you hold the reporting permission or if the session was your own — so a cashier can always read their own shift and nobody else's [src: app/controllers/pos.php:219].

Products and stock

Stock cannot be oversold

Every movement goes through one function, and a movement that would take stock below zero is refused rather than clamped [src: app/src/Inventory.php:27].

The refusal is worth describing exactly, because it is stronger than it looks. The decrement is written as a single conditional update — the row is reduced only where the quantity on hand is already at least the amount being taken — and a zero row count is treated as "this would oversell" [src: app/src/Inventory.php:27]. It is one atomic statement rather than a read followed by a write, so two tills selling the last item at the same moment cannot both succeed.

The stock ledger

Every movement is a row: type, quantity change, unit cost, what it refers to, a note and the user who caused it [src: app/src/Inventory.php:27]. Stock is never simply overwritten, so the history of a quantity is recoverable.

Stock can be adjusted by hand for a count correction, damage or shrinkage [src: app/src/Inventory.php:67], verified against a physical count [src: app/src/Inventory.php:86], and valued [src: app/src/Inventory.php:101].

Purchases and suppliers

A purchase is entered as lines of product, quantity and unit cost, and requires the purchase-editing permission [src: app/controllers/admin.php:264].

Three things happen on save, inside one locked transaction so that a failure leaves nothing behind [src: app/controllers/admin.php:264]. A purchase record is created with a generated reference of the form PO- followed by six characters. Each line is written. And each line is received into stock immediately.

That last point is the one to plan around: a purchase is received at the moment you save it. The status is written as received rather than ordered, so there is no pending-order state to reconcile later [src: app/controllers/admin.php:264]. Vendra records what has arrived, not what has been ordered.

How receiving changes cost

Receiving stock updates the product's average cost, weighted by quantity: the existing quantity at the existing average, plus the incoming quantity at the incoming cost, divided by the total [src: app/src/Inventory.php:51].

One detail protects that figure. If on-hand stock is negative, it is treated as zero for the purposes of the average, so a negative quantity cannot poison the cost of everything received afterwards [src: app/src/Inventory.php:51]. The product row is locked while this happens [src: app/src/Inventory.php:51].

Purchases can be listed and opened individually [src: app/controllers/admin.php:251]. Suppliers are managed separately [src: app/controllers/admin.php:302]; deleting one detaches it from its purchases rather than deleting them, so the purchase history survives the supplier [src: app/controllers/admin.php:302].

Reports and the audit log

Reports are gated on the reporting permission [src: app/controllers/admin.php:399]. Actions that change something are written to an audit log, which the viewer role can read [src: app/src/Auth.php:67].

Settings

Settings are administrator-only, both to view and to save [src: app/controllers/admin.php:490].

They fall into two groups. The first covers the business and the receipt: business name, e-mail, phone and address, timezone, currency and currency symbol, the default tax rate and what to call tax, whether negative stock is allowed, the default low-stock threshold, receipt header and footer, accent colour and logo [src: app/controllers/admin.php:495].

The second covers mail and notifications: SMTP host, port, user, password, from-address and security mode, a notification address, and four toggles — low stock, a returned sale, the receipt and the Z-report [src: app/controllers/admin.php:495].

Several defaults are applied on save rather than assumed. An unrecognised timezone falls back to UTC, the SMTP port is clamped to a valid port number, and the security mode must be one of STARTTLS, TLS or none, defaulting to STARTTLS [src: app/controllers/admin.php:495].

Two of those toggles behave differently from the rest by design: the receipt and Z-report toggles are the only two that cause mail to be sent to a customer, so an absent checkbox means off, which is also the migration default. Emailing a customer is always a decision somebody made [src: app/controllers/admin.php:495].

One point of arithmetic worth knowing if you compare the form against the database: the tax rate you type is stored multiplied by one hundred, as basis points [src: app/controllers/admin.php:495].

The API and agent access

Keys

The API authenticates with a key rather than a session. Some endpoints take any valid key, and some require an administrator-scoped one [src: app/src/Api.php:31]. Purchase creation and the report endpoint both require the administrator-scoped key [src: app/controllers/api.php:208].

A machine-readable description of the interface is published [src: app/controllers/api.php:307].

Agent tools

Vendra exposes five tools to an agent [src: app/controllers/v3.php:74]:

ToolWrites?What it does
list_productsnoLists products with price and current stock
stock_levelsnoStock on hand for one product, by id or barcode, or for everything
low_stocknoProducts at or below their low-stock threshold
sales_reportnoSales totals for a date range, by product and by cashier
record_saleyesRecords a completed sale and decrements stock

Four are read-only and one writes, and the tool list says so in its own instructions rather than leaving an agent to infer it [src: app/controllers/v3.php:74]. A read-only key cannot call the one that writes.

The instructions given to an agent also state the oversell rule, because an agent needs to know a call can be refused: record_sale goes through the same checkout path the till uses, so a line exceeding available stock is refused and told how much is left [src: app/controllers/v3.php:74].

Webhooks

Two events can be sent to a URL you control: a recorded sale and a low-stock warning [src: app/src/Webhook.php:16].

Spreadsheets: sales and stock

Sales → Download CSV gives one row per sale line — for the last 30 days, the day you are viewing, or any range with ?from=YYYY-MM-DD&to=YYYY-MM-DD — with quantity, quantity returned, unit price, discount, tax, line total, and the unit and line cost the till captured when it rang the sale [src: app/controllers/admin.php:90], [src: app/src/Exports.php:52]. Products → Stock CSV gives price, weighted-average cost, stock, low-stock level and stock value at cost [src: app/src/Exports.php:87]. The sales file needs the report permission and the stock file the catalogue permission, so a cashier gets neither [src: app/controllers/admin.php:92]. Money is written as plain numbers and text cells are guarded against spreadsheet formulas [src: app/src/Exports.php:32].

Backup and data protection

The whole dataset can be exported as JSON, with fields you nominate redacted, or as a SQLite file [src: app/src/BackupExport.php:19].

For a data-protection request, one subject's data can be exported [src: app/src/Gdpr.php:29] or anonymised in place [src: app/src/Gdpr.php:46].

What Vendra does not do

It does not process card payments. It records how a sale was tendered — cash, card or other — and computes the change; the card itself is taken on whatever terminal you already have [src: app/src/Sales.php:97].

It is a single-shop system. The whole installation carries one settings record, written to the row with id 1 [src: app/controllers/admin.php:495], and there is no notion of a second location, a store identifier or a central head office anywhere in the application.

It has no e-commerce side and does not synchronise stock with a website.

← Back to Vendra · Quickstart · API · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →