# Documents

Reusable document templates with placeholders, print and barcode rendering


Render reusable document templates with placeholders, print, and barcode output.

> [!NOTE]
> **Looking for forms?**
>
> The working forms module is `backend/plugins/frontline_api/src/modules/form/`; see [Forms & Surveys](https://erxes.io/docs/frontline-forms-surveys). Core keeps only a legacy shell in `modules/forms/` (the `fieldsCombinedByContentType` query plus `fields`/`fieldsGroups` helpers used by documents and properties).

## The documents module

`backend/core-api/src/modules/documents/`:

```text
graphql/schema.ts, queries.ts, mutations.ts
db/definitions/documents.ts, db/models/Documents.ts
customResolvers/document.ts
trpc/document.ts              # documents.find, findOne, print
routes.ts                     # GET /print
blocksToHtml.ts, barcode.ts, utils.ts
```

A `Document` ([documents.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/documents/db/definitions/documents.ts)) has `contentType` (`<plugin>:<module>` like `core:product`), `subType`, `name`, `code`, `content` (editor blocks), `replacer` (placeholder map), `tagIds`, `createdUserId`. The GraphQL type also resolves `createdUser` and `approvalLockState`.

| Operation | Key arguments |
| --------- | ------------- |
| `documents` | `contentType`, `subType`, `searchValue`, `dateFilters`, `userIds`, `tagIds`, `limit`, cursor params; returns `DocumentListResponse` |
| `documentsDetail` | `_id!` |
| `documentsTypes` | none; returns types across every plugin's `meta.documents` |
| `documentsGetEditorAttributes` | `contentType!`; returns the placeholder list for the editor |
| `documentsTotalCount` | `searchValue`, `contentType` |
| `documentsProcess` | `_id`, `replacerIds`, `config`; renders the template, returns an HTML string |
| `documentsSave` | `_id`, `contentType`, `subType`, `name!`, `content`, `replacer`, `code`; upsert |
| `documentsRemove` | `_id!` |

`blocksToHtml` and `barcode` render printable output; `routes.ts` serves `GET /print` for browser printing and download flows.

### How a plugin contributes document types

A plugin contributes three pieces, and core dispatches on the `contentType` prefix (`<plugin>:<module>`):

- `meta/documents.ts`: `documents.types` entries `{ label, contentType, subTypes? }`. `documentsTypes` aggregates them from every active plugin. Sales declares `{ label: 'Sales', contentType: 'sales:deal' }`.
- A `documents.editorAttributes` tRPC procedure: `documentsGetEditorAttributes` calls `sendTRPCMessage({ pluginName, module: 'documents', action: 'editorAttributes' })` for non-core types. Sales implements it in `modules/sales/trpc/document.ts`, returning deal attributes like `productsInfo`, `totalAmount`, `stageName`, `assignedUsers`, plus schema fields and `customFieldsData.<fieldId>` entries.
- A `<module>.replaceContent` tRPC procedure: `documentsProcess` routes to `module: <moduleName>, action: 'replaceContent'` for non-core types (sales exposes it on `modules/sales/trpc/deal.ts`). The procedure receives `replacerIds`, `content`, `config`, and `contentType`, and returns rendered blocks; `config.dateFormat` controls date rendering (core defaults to `YYYY-MM-DD`).

Core's own declaration ([src/meta/documents.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/meta/documents.ts)) registers `core:contact.customer`, `core:contact.company`, `core:product`, `core:user`, `core:broadcast`, keeps `editorAttributes`/`replaceContent` as direct functions in the meta object, and builds product barcodes through `meta/document/productReplacer.ts`.

## Permissions

`manageDocuments` (create/edit), `removeDocuments` (delete); `documentsRead` is `always: true`. Owner scope `own` tracks `createdUserId`.

## Example

```graphql
mutation SaveTemplate {
  documentsSave(
    contentType: "core:product"
    name: "Product sheet"
    code: "PROD-SHEET"
    content: "<p>{{ name }} — {{ code }} — {{ unitPrice }}</p>"
  ) { _id }
}

query Render($id: String) {
  documentsProcess(_id: $id, replacerIds: ["<productId>"], config: { dateFormat: "YYYY-MM-DD" })
}
```

