=== AMS Studio ERP — Invoicing & Accounting ===
Contributors: anthonysgro
Tags: erp, invoicing, quotes, crm, factur-x
Requires at least: 6.4
Tested up to: 7.1
Requires PHP: 8.1
Stable tag: 0.5.25
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

An ERP inside WordPress: manage customers, products, quotes and invoices on your own server, with no external service.

== Description ==

AMS Studio ERP turns WordPress into the back office of a small company. Quotes, invoices,
purchase orders, stock, deals, documents, treasury and accounting all live in your
own database, on your own server. There is no service to subscribe to and no second
password to remember.

**The document chain**

Nothing is retyped from one step to the next. A quote becomes an invoice, the invoice
calls for its payment, the payment produces its ledger entry, and every document keeps
the link back to the one it came from.

* Quotes sent by email with their PDF, then signed online by the customer
* Invoices numbered in a continuous, chronological series
* Payments entered by hand or matched to an imported bank statement
* Ledger entries posted to the right journal and financial year

**What holds up under audit**

These rules live in the engine, and the plugin refuses whatever breaks them.

* Numbering stays continuous: a prefix already in use cannot change, and a reopened
  document keeps its number
* An approved document is sealed by a fingerprint over its amounts, its party and its
  number; a mistake is corrected by a credit note, never by editing
* VAT is broken down by rate, as the law requires on the document itself
* Factur-X (PDF/A-3) and Peppol BIS Billing 3.0; a non-compliant invoice refuses to go
  out, in print, by email and on the public page
* Attachments are encrypted with AES-256-GCM, outside the public media library
* Exact decimal arithmetic throughout, never floating point

**What this plugin does**

* Customers: records, addresses, contacts, categories
* Catalogue: products and services, purchase and sale prices, margins, VAT rates
* Quotes: drafted, sent by email with their PDF, signed online by the customer
* Customer invoices and credit notes: continuous numbering, deposits, payment terms,
  what is paid and what remains due
* Factur-X (PDF/A-3) and Peppol BIS Billing 3.0 on every invoice
* Encrypted attachments, exact decimal arithmetic, an audit trail on every document

**What it does not do**

Purchasing, stock, deals, treasury, accounting, the document library and the
WooCommerce bridge are **not part of this plugin**. They come with AMS Studio Pro, a
separate add-on installed alongside it and distributed from https://www.ams-studio.lu/

Nothing here is capped, time-limited or waiting for a key: what you install is
complete. The modules above are simply not in this package — the menu shows them so
you know they exist, and says where to get them.

**Languages**

French and English. The plugin follows your site's language, or its own if you ask it to —
an English site can run a French ERP.

== Installation ==

1. Install and activate the plugin as usual.
2. Open AMS ERP in the admin menu and fill in your company details.
3. Optionally install the demo data set to look around before entering your own.

No command line is required, and no account anywhere: the plugin ships its own
autoloader and its own PDF engine. Two optional features do contact an outside
service when you use them — see "External services" below.

== Third-party libraries ==

Producing an invoice as a PDF needs an HTML-to-PDF engine, and WordPress core
has none. Four libraries are bundled, unmodified except where noted, each with
its own licence file in its directory — all GPL-compatible:

* dompdf/dompdf 3.1.6 — LGPL-2.1 — https://github.com/dompdf/dompdf
* masterminds/html5 — MIT — https://github.com/Masterminds/html5-php
* sabberworm/php-css-parser — MIT — https://github.com/MyIntervals/PHP-CSS-Parser
* php-font-lib — LGPL-2.1 — https://github.com/dompdf/php-font-lib

They are bundled rather than pulled by Composer because the plugin must install
from the WordPress admin, on shared hosting, with no command line. Their
namespaces are prefixed under `AMSBM\Vendor\` with PHP-Scoper, so that two
plugins bundling dompdf in different versions cannot break each other; the
prefixing is reproducible with `bin/scoper.sh`. Every other deviation from the
upstream code is marked in place and listed in `lib/pdf/ECARTS.md`: a direct-access guard on the two files that carry code
outside a class, embedding a Factur-X XML attachment into a PDF/A-3 file which
dompdf does not support upstream, heredoc blocks rewritten as concatenation,
and the removal of SVG rendering.

dompdf's fifth dependency, php-svg-lib, is deliberately **not** bundled. It
exists only to draw SVG images inside a PDF, and drawing one means parsing XML
that a site user supplied. The plugin does not need it — logos are PNG, JPEG,
GIF or WebP, and an SVG is refused at upload — so the smaller attack surface is
worth more than the feature. The three places where dompdf reached for it are
neutralised in place, and an SVG is simply not a recognised image type any
more.

One more deviation is replayed automatically rather than by hand: in
php-font-lib, `glyf::toHTML()` — a font-debugging method that builds an HTML
page with inline scripts — is emptied. Neither dompdf nor the plugin calls it.
`bin/pdf-patches.php` applies that change after every copy of the libraries
(`bin/vendor-pdf.sh`) and again after prefixing (`bin/scoper.sh`), and the
package build (`build-zip.sh`) refuses to proceed if it is missing.

== Compiled admin interface ==

The admin interface is a React/TypeScript application, compiled with Vite. The
compiled, minified bundle is in `app/build/`; its **unminified sources ship with
the plugin**, in `app/src/`, along with everything needed to rebuild it:
`app/package.json`, `app/package-lock.json`, `app/tsconfig.json`,
`app/tsconfig.node.json`, `app/vite.config.ts`, `app/tailwind.config.js` and
`app/postcss.config.js`.

The same sources are publicly available at https://github.com/SanMat11/wp-erpx

To rebuild the bundle — Node.js 18 or 20 and later, verified with Node.js 22:

`cd app && npm ci && npm run build`

`npm ci` installs the exact dependency versions recorded in
`app/package-lock.json`. `npm run build` type-checks the sources with TypeScript
5.9, then builds them with Vite 5.4 into `app/build/`. The file
`app/build/morceaux/vendor-[hash].js` is the npm dependencies listed in
`app/package.json` — React, Ant Design and the others — bundled together by
Vite; each package's name and exact version are in `app/package-lock.json`.

Vite writes the entry point as `amsbm-app-[hash].js` and `amsbm-app-[hash].css`,
with the code split into chunks under `app/build/morceaux/`. The hash in the
filename is the version: a new build produces a new name, so a cached file is
never a stale one. `app/build/manifeste.json` records the current pair, and the
plugin reads it rather than guessing.

Nothing about the plugin requires npm at runtime. The bundle is committed and
shipped because the target audience installs from the WordPress admin, on shared
hosting, with no command line.

== External services ==

**VIES — EU VAT number validation** (European Commission)

Used only when you click "check" beside an EU VAT number, to confirm that the number
exists. What is sent: the country code and the VAT number, in the request URL. Nothing
else — no company name, no site address, no identifier.
Service: https://ec.europa.eu/taxation_customs/vies/
Terms and privacy: https://ec.europa.eu/taxation_customs/vies/#/help

**Stripe — card payment of your invoices** (Stripe, Inc.)

Used only if you enter your own Stripe API keys in the settings. It lets a customer of
yours pay a shared invoice by card. What is sent, when your customer clicks "pay": the
amount, the currency, the invoice reference and the return address. The plugin never
sends your customer list or your documents.
Service: https://stripe.com/
Terms: https://stripe.com/legal/ssa
Privacy: https://stripe.com/privacy

**AMS Studio licence server** (AMS Studio, Luxembourg)

Only reached if you click "Upgrade to Pro" in the application header. That button is a
link: your browser opens it, the plugin itself opens no connection and sends nothing on
its own. What travels in that link: an installation identifier drawn at random on your
site, a one-way hash of a secret drawn with it, your site address and your admin email
address — so that the licence you buy is tied to this site and fills itself in when you
install the paid package, with nothing to copy by hand. The hash cannot be reversed, and
the secret itself never appears in the link.
Nothing is sent if you never click the button, and nothing is sent on a schedule.
Service: https://licences.ams-studio.lu/
Terms and privacy: https://www.ams-studio.lu/

The plugin contacts nothing else. No telemetry, no update server, no licence check.

== Frequently Asked Questions ==

= Is my data sent anywhere? =

Your business data stays put. Documents, customers, articles and ledger entries live
in your own database and are never sent anywhere.

Two features do reach outside, and only when you ask them to:

* **VAT number validation.** When you click "check" next to an EU VAT number, that
  number and its country code are sent to VIES, the European Commission's service.
  Nothing else. If you never click, nothing is ever sent.
* **Card payment of an invoice.** If — and only if — you enter your own Stripe keys,
  your customer can pay a shared invoice by card. Stripe then receives the amount, the
  currency and the invoice reference, at the moment your customer clicks "pay".

Both are documented in full under "External services" below.

= Is my data deleted when I uninstall? =

No. Dropping the tables requires adding `define( 'AMSBM_REMOVE_ALL_DATA', true );` to
your `wp-config.php`. Accounting retention obligations — six years in France, up to ten
elsewhere in Europe — outlast the lifetime of any plugin.

= Does it need InnoDB? =

Yes. Without a transactional engine, operations cannot be made atomic. The built-in
health check reports it.

= Are any features locked or limited? =

No. Every feature in this package works, with no cap on the number of customers,
quotes, invoices or articles, and no trial period.

Some modules — purchasing, stock, deals, treasury, accounting, the document library
and the WooCommerce bridge — are not in this package at all. They ship with a separate
add-on. The menu lists them so you know they exist; they are not disabled code waiting
for a key.

= Do I need Composer or npm to install this? =

No. The plugin ships its own autoloader, its own PDF engine and its compiled
admin interface. `composer.json` and the JavaScript sources are included so that
the build is reproducible and the dependencies are auditable, not because
anything has to be run.

= What happens to my data if I deactivate the plugin? =

Nothing. Deactivating stops the code from running; the tables, the documents and
the ledger stay exactly as they are, and reactivating picks up where you left
off.

= Which chart of accounts is supported? =

The French plan comptable général and the Luxembourg plan comptable normalisé 2020.
Accounts, journals and financial years are all editable.

== Screenshots ==

1. Dashboard: monthly revenue, pending invoices and active customers.
2. Quotes, from draft to online signature.
3. Customer invoices, with what is paid and what remains due.
4. Customers, with their addresses and contacts.
5. The catalogue: products and services, prices and margins.
6. Settings: company details, numbering, VAT and appearance.

== Changelog ==

= 0.5.25 =
* On a site without the stock, WooCommerce or accounting modules, the admin
  interface no longer requests routes that do not exist there: the dashboard
  only asks for stock alerts when stock is installed, the VAT settings only list
  accounting accounts when accounting is, and the Warehouses and Store settings
  tabs only appear when their module does.
* Demo data no longer writes purchase orders and supplier invoices on a site
  that has no purchasing module.

= 0.5.24 =
* Plugin Check reports nothing left in the plugin's own code. The hook fired when
  the installation fingerprint is renewed is now fired by its literal, prefixed
  name. The one call it discourages, `load_plugin_textdomain()`, says why it
  stays: the plugin ships its own English catalogue, which WordPress only finds
  through that call.

= 0.5.23 =
* The print page of a document is no longer echoed as one string. Its template
  is included directly in the response, and escapes every value at the exact
  place where it is output.
* No inline JavaScript left: the print button, the confirmation before a status
  change, the setup fields to copy and the "copy backup codes" button now use a
  script enqueued with `wp_enqueue_script()`, or printed with
  `wp_print_inline_script_tag()` on the standalone print page. The "copy backup
  codes" button, which copied nothing, now copies.
* PDF downloads, accounting exports and stored attachments are sent with
  `X-Content-Type-Options: nosniff`.
* Bundled php-font-lib: a font-debugging method that built HTML with inline
  scripts, and that nothing calls, is emptied — see "Third-party libraries".

= 0.5.22 =
* The sources of the admin interface shipped in `app/src/` can be rebuilt from
  the plugin package alone again. A screen that is not part of this package was
  imported unconditionally, so `npm run build` stopped on a missing file; it is
  now loaded only where its file exists.
* The same sources are published at https://github.com/SanMat11/wp-erpx, and the
  readme gives the Node.js version, the exact command and where the output goes.

= 0.5.21 =
* **English is translated again.** The English catalogue was deleted during the
  0.5.7 rename and never rebuilt: since then every string written by the server
  — API refusals shown on screen, e-mails, PDFs, meta boxes — came out in French
  on English sites, while the interface itself switched. All 971 strings the
  code asks for are now translated and shipped as
  `languages/ams-studio-erp-invoicing-accounting-en_US.mo`.
* **A quote could not be accepted in English.** The wording a customer copies
  out to accept a quote was compared against the French `bon pour accord`, while
  the refusal message told English customers to type "Agreed and accepted". They
  typed exactly what was asked and were turned away. The expected wording now
  follows the language of the site, and French stays accepted so agreements
  given before a language change remain valid.
* Four screen strings still quoted the French wording inside English sentences,
  and two were left in French outright. The screen, the refusal message and the
  check now say the same thing.

= 0.5.20 =
* The text domain is now `ams-studio-erp-invoicing-accounting`, matching the
  slug registered on WordPress.org. It was `ams-studio-business-management` on
  1155 i18n calls, which prevented the plugin from being translated through
  translate.wordpress.org. The PHP prefix `amsbm_`, the option, table, post
  type and REST namespace names are unchanged, as is the main file name.
* Internal: the translation catalogue built by `bin/i18n/` was named
  `amsbm-<locale>.po`, which matched no text domain — WordPress would never
  have loaded it. It now carries the domain, as the format requires.

= 0.5.19 =
* Internal: the Peppol reader no longer emits a PHP warning when an incoming
  invoice omits optional elements — payment terms, legal entity, trading name
  or tax scheme. Absent elements read back as an empty string, as intended.
* Internal: the test asserting that every document type is reachable by a role
  performed no assertion at all: it returned on the first role covering
  everything. It now checks the business roles, and was seen to fail when a
  document type is unwired from them.

= 0.5.18 =
* Internal: the bundled PDF libraries — dompdf, php-font-lib, masterminds/html5
  and sabberworm/php-css-parser — now live under the `AMSBM\Vendor\` namespace,
  prefixed with PHP-Scoper. Two plugins bundling dompdf in different versions
  can no longer break each other. A document renders byte-for-byte as before.

= 0.5.17 =
* Internal: the document editor's article catalogue no longer writes its
  `<script>` tag by hand. It goes through `wp_print_inline_script_tag()`, which
  carries the page's Content-Security-Policy nonce where there is one. It stays
  a JSON data block, not executable code. Nothing changes on screen.

= 0.5.16 =
* Security: SVG rendering has been removed from the PDF engine, and the
  php-svg-lib library with it. Drawing an SVG meant parsing user-supplied XML
  inside the printing chain, for a format the plugin never needed: logos are
  PNG, JPEG, GIF or WebP. An SVG is now refused when the logo is uploaded, and
  refused again before it reaches the PDF. Documents without an SVG render
  byte-for-byte as before.

= 0.5.15 =
* Internal: every SQL query now goes through `wpdb::prepare()`, with no exception.
  Table and column names are bound with the `%i` placeholder instead of being
  interpolated into the query string, and the plugin's own table names are checked
  against a hard-coded list before any query is built. Nothing changes on screen:
  the 132 read routes return byte-for-byte identical answers.

= 0.5.14 =
* Internal: the public quote and invoice page no longer writes its script tags by hand.
  They go through the WordPress loader, which now also carries the page's
  Content-Security-Policy nonce. Nothing changes on screen.

= 0.5.13 =
* Internal: four blocks in the bundled PDF libraries were rewritten from heredoc to
  string concatenation, so that the whole package passes the directory's automated
  check. The generated PDFs are byte-for-byte identical. Every deviation from the
  upstream libraries is now listed in `lib/pdf/ECARTS.md`.

= 0.5.12 =
* The "Upgrade to Pro" link in the header now offers to subscribe, in so many words:
  the dialog it opens says "Subscribe" instead of "Learn more" and states the price —
  the same one the website quotes. It no longer names an empty module when it was not
  opened from a particular screen.

= 0.5.11 =
* Internal: this package now refuses any update whose archive does not come from where
  it says it does. Nothing changes for you here — the free edition is updated by
  WordPress.org as it should be.

= 0.5.10 =
* Removing the demo dataset and installing it again left you with no customers and no
  products at all — while reporting it had created twenty. Removing deleted the
  WordPress posts but not the plugin's own rows, and the next install reused a post id
  that pointed at nothing.
* A company named "Roussel & Fils" read "Roussel &amp; Fils" in the demo dataset.
* An IBAN broke into six stacked pieces in the treasury list, and an IBAN entered with
  spaces was regrouped wrongly. A cash account shows a dash instead of an empty box.

= 0.5.9 =
* This installation now has an identity of its own — an identifier drawn at random on
  your site, and a secret drawn with it. Nothing is sent anywhere: they exist so that a
  licence bought later belongs to this site and fills itself in, instead of being copied
  by hand. See "External services" for the one moment anything leaves, and what it is.
* The activate button no longer sits flush against the licence field.

= 0.5.8 =
* Screens that spoke French on an English site. The public invoice page a customer
  opens to pay, the goods receipt screen and the online payment settings carried their
  wording inside the code instead of in the catalogues: 87 sentences that no
  translation could reach. They are now in both catalogues, English included.
* The subscription screen showed its translation keys instead of its labels — its
  whole set of 80 keys was in neither catalogue. It also named the four licence states
  it had never been given a word for, starting with a site that simply has no key.

= 0.5.7 =
* This package now contains exactly what it runs. Purchasing, stock, deals, treasury,
  accounting, the document library and the WooCommerce bridge moved to a separate
  add-on — their code is no longer shipped here. Nothing is capped or locked: what is
  present works, in full.
* New name. The plugin was called ERP-X; it is now AMS Studio ERP.
  Everything it declares was renamed with it — options, tables, custom post types,
  capabilities, hooks and the REST namespace. Nothing carries over from the old
  names, so this version is an installation, not an upgrade.
* Nothing in this package is capped, time-limited or tied to a key. What is here
  is installed and open; what is not here is not shipped at all.
* Electronic invoicing follows. Factur-X (PDF/A-3) and Peppol BIS Billing 3.0
  used to be conditional; they now apply to every invoice, which is what a legal
  obligation ought to be.
* The plugin no longer looks anywhere but here for its updates.

= 0.5.6 =
* A document the WooCommerce bridge found wrong now says so before you click. The
  bridge marks it, approval refuses it — but nothing showed the reason, so the refusal
  arrived out of nowhere.
* The supplier invoice screens stop offering what the server will refuse: five write
  actions in the list, and approving a credit note, are now hidden from roles that do
  not have the right — and the reason is stated, with who to ask.
* A supplier credit note table headed two different columns "Amount excl. VAT".
* The credit note route names the capability it needs instead of relying on the global
  write right.

= 0.5.5 =
* The plugin's own header is translated at last. Its name and description were in no
  catalogue, so an English site read the description in French on its plugins screen —
  the one place never reread. The extractor now covers the header, as xgettext does.
* One header dropped: the plugin address and the author address may not be the same,
  and the site at ams-studio.lu presents the plugin.

= 0.5.4 =
* Every PHP file the plugin ships now refuses a direct HTTP call. On shared hosting
  wp-content is often served without going through WordPress, and a file reached that
  way ran outside it — printing the server's absolute path in the fatal error that
  followed. A test counts them all and fails on the next file shipped without a guard.
* The two-factor QR script goes through the WordPress script queue.
* Repository compliance: exemptions declared for the directory's own database sniff,
  which is stricter than the WordPress coding standard, and one core function dropped
  in favour of a plain expression.

= 0.5.3 =
* The plugin is named ERP-X throughout, with a new mark drawn in one place and used on
  the admin menu, the sidebar and the sign-in screen.

= 0.5.2 =
* fail2ban on the API, installation blocking, and an allow list on the administration.

= 0.5.1 =
* Sealing: an invoice or credit note the WooCommerce bridge found wrong can no longer be
  approved by any path.
* Electronic invoicing: a non-compliant invoice no longer goes out by email or on the
  public page either.
* Purchasing: only what has been received is invoiced; invoicing ahead must be asked for.

= 0.5.0 =
* Two-factor authentication and the per-module permission matrix.

= 0.2.0 =
* The sales cycle end to end: items, parties, quotes, invoices.
* Multi-rate VAT rounded per rate at the foot of the document.
* Quote to invoice transformation, with a line-by-line trace.

= 0.1.0 =
* Exact decimal arithmetic, with no silent overflow.
* Numbering by atomic UPDATE, refusing documents backdated below the last period used.
* Migration lock through a MySQL GET_LOCK, genuinely atomic.
