Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 99 additions & 51 deletions packages/backend/src/adapters/de/lexware-office.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,11 @@
"properties": {
"voucherType": {
"type": "string",
"description": "Required by Lexware. Comma-separated types: salesinvoice, salescreditnote, purchaseinvoice, purchasecreditnote, invoice, creditnote, orderconfirmation, quotation, deliverynote, downpaymentinvoice, or any."
"description": "Required by Lexware. Comma-separated list, or `any` for all types: salesinvoice, salescreditnote, purchaseinvoice, purchasecreditnote (bookkeeping vouchers: read one with lexware_office_get_voucher), invoice (lexware_office_get_invoice), creditnote (lexware_office_get_credit_note), quotation (lexware_office_get_quotation), downpaymentinvoice, orderconfirmation, deliverynote (lexware_office_get_sales_document)."
},
"voucherStatus": {
"type": "string",
"description": "Required by Lexware. Comma-separated statuses: draft, open, paid, paidoff, voided, transferred, sepadebit, overdue, accepted, rejected, or any. overdue cannot be combined with other statuses: ask for it on its own (Lexware answers 400 otherwise)."
"description": "Required by Lexware. Comma-separated list, or `any`: draft, open, paid, paidoff, voided, transferred, sepadebit, accepted/rejected (quotations only), unchecked (bookkeeping vouchers only), overdue. `overdue` must be sent ALONE (voucherStatus=overdue), never combined with other statuses: Lexware answers 400 otherwise. Overdue vouchers are already included in `open`, so for 'everything unpaid' use `open,sepadebit,transferred`."
},
"archived": {
"type": "boolean",
Expand All @@ -137,7 +137,31 @@
},
"size": {
"type": "number",
"description": "Page size, 1–250 (default 25)."
"description": "Page size, 1-250 (default 25). Lexware returns at most 10,000 results in total; narrow by date beyond that."
},
"voucherNumber": {
"type": "string",
"description": "Exact voucher number, e.g. RE0042."
},
"createdDateFrom": {
"type": "string",
"description": "Created on or after, YYYY-MM-DD."
},
"createdDateTo": {
"type": "string",
"description": "Created on or before, YYYY-MM-DD."
},
"updatedDateFrom": {
"type": "string",
"description": "Updated on or after, YYYY-MM-DD."
},
"updatedDateTo": {
"type": "string",
"description": "Updated on or before, YYYY-MM-DD."
},
"sort": {
"type": "string",
"description": "voucherDate, voucherNumber, createdDate or updatedDate, optionally followed by ,ASC or ,DESC, e.g. voucherDate,DESC."
}
},
"required": [
Expand All @@ -156,7 +180,13 @@
"voucherDateFrom": "$voucherDateFrom",
"voucherDateTo": "$voucherDateTo",
"page": "$page",
"size": "$size"
"size": "$size",
"voucherNumber": "$voucherNumber",
"createdDateFrom": "$createdDateFrom",
"createdDateTo": "$createdDateTo",
"updatedDateFrom": "$updatedDateFrom",
"updatedDateTo": "$updatedDateTo",
"sort": "$sort"
}
}
},
Expand All @@ -168,7 +198,7 @@
"properties": {
"id": {
"type": "string",
"description": "Invoice UUID, as returned in the voucher list's `id` field."
"description": "Invoice UUID. Only for voucher-list rows whose voucherType is `invoice`. For salesinvoice, purchaseinvoice, salescreditnote or purchasecreditnote use lexware_office_get_voucher; for downpaymentinvoice, orderconfirmation or deliverynote use lexware_office_get_sales_document. A wrong pairing answers 404."
}
},
"required": [
Expand Down Expand Up @@ -220,6 +250,57 @@
"path": "/credit-notes/{id}"
}
},
{
"name": "lexware_office_get_voucher",
"description": "Read one bookkeeping voucher (voucher-list voucherType salesinvoice, salescreditnote, purchaseinvoice or purchasecreditnote): contact, voucher items grouped by tax rate, totals, due date and attached file ids.",
"parameters": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Voucher UUID from lexware_office_list_vouchers whose voucherType is salesinvoice, salescreditnote, purchaseinvoice or purchasecreditnote."
}
},
"required": [
"id"
]
},
"endpointMapping": {
"method": "GET",
"path": "/vouchers/{id}"
}
},
{
"name": "lexware_office_get_sales_document",
"description": "Read one down payment invoice, order confirmation, delivery note or dunning by id.",
"parameters": {
"type": "object",
"properties": {
"resource": {
"type": "string",
"enum": [
"down-payment-invoices",
"order-confirmations",
"delivery-notes",
"dunnings"
],
"description": "downpaymentinvoice → down-payment-invoices, orderconfirmation → order-confirmations, deliverynote → delivery-notes."
},
"id": {
"type": "string",
"description": "UUID from the voucher list."
}
},
"required": [
"resource",
"id"
]
},
"endpointMapping": {
"method": "GET",
"path": "/{resource}/{id}"
}
},
{
"name": "lexware_office_list_articles",
"description": "List articles (Artikel) from the product catalogue, optionally narrowed by article number or type, so an invoice line can reference a real product.",
Expand Down Expand Up @@ -269,29 +350,24 @@
},
{
"name": "lexware_office_create_contact",
"description": "Create a new contact with the customer and/or vendor role. Writes to the live company — check with lexware_office_list_contacts first that the party does not already exist.",
"description": "Create a new company contact with the customer and/or vendor role. Writes to the live company: check with lexware_office_list_contacts first that the party does not already exist.",
"parameters": {
"type": "object",
"properties": {
"companyName": {
"type": "string",
"description": "Company name. Provide this OR firstName+lastName, not both."
},
"firstName": {
"type": "string",
"description": "Given name, for a private person."
"description": "Company name."
},
"lastName": {
"type": "string",
"description": "Family name, for a private person."
},
"isCustomer": {
"type": "boolean",
"description": "Give the contact the customer role (default true)."
"roles": {
"type": "object",
"default": {
"customer": {}
},
"description": "Roles as an object: {\"customer\":{}} (default), {\"vendor\":{}}, or both {\"customer\":{},\"vendor\":{}}."
},
"isVendor": {
"type": "boolean",
"description": "Give the contact the vendor role."
"billingAddress": {
"type": "object",
"description": "Billing address: {\"street\":\"Musterstr. 1\",\"zip\":\"79098\",\"city\":\"Freiburg\",\"countryCode\":\"DE\"}. countryCode (ISO 3166-1 alpha-2) is required when an address is given. Omit for no address."
},
"email": {
"type": "string",
Expand All @@ -300,22 +376,6 @@
"phone": {
"type": "string",
"description": "Primary business phone number."
},
"street": {
"type": "string",
"description": "Street and house number of the billing address."
},
"zip": {
"type": "string",
"description": "Postal code of the billing address."
},
"city": {
"type": "string",
"description": "City of the billing address."
},
"countryCode": {
"type": "string",
"description": "ISO 3166-1 alpha-2 country code, e.g. DE."
}
},
"required": [
Expand All @@ -327,25 +387,13 @@
"path": "/contacts",
"bodyMapping": {
"version": 0,
"roles": {
"customer": {},
"vendor": {}
},
"roles": "$roles",
"company": {
"name": "$companyName"
},
"person": {
"firstName": "$firstName",
"lastName": "$lastName"
},
"addresses": {
"billing": [
{
"street": "$street",
"zip": "$zip",
"city": "$city",
"countryCode": "$countryCode"
}
"$billingAddress"
]
},
"emailAddresses": {
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/src/adapters/de/lexware-office.live.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ describe('lexware-office adapter — static spec conformance', () => {
it("exposes the voucher list, which is what 'what is unpaid' needs", () => {
expect(toolNames).toContain('lexware_office_list_vouchers');
});

// Voucher-list rows of type salesinvoice/purchaseinvoice/... live under
// /vouchers/{id}; sending them to /invoices/{id} answered 404 for a user.
it('reads bookkeeping vouchers and the other sales documents at their own endpoints', () => {
const path = (n: string) => a.tools.find((t) => t.name === n)?.endpointMapping.path;
expect(path('lexware_office_get_voucher')).toBe('/vouchers/{id}');
expect(path('lexware_office_get_sales_document')).toBe('/{resource}/{id}');
});

it('creates a contact with the roles asked for, and no empty address or person', () => {
const body = a.tools.find((t) => t.name === 'lexware_office_create_contact')!.endpointMapping
.bodyMapping as Record<string, unknown>;
expect(body.roles).toBe('$roles');
expect(body.person).toBeUndefined();
expect(body.addresses).toEqual({ billing: ['$billingAddress'] });
});
});

// Opt-in live check. Needs real credentials; skipped in CI.
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/adapters/intl/odoo-jsonrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"category": "erp",
"icon": "odoo",
"docsUrl": "https://www.odoo.com/documentation/17.0/developer/reference/external_api.html",
"instructions": "**Which Odoo connector?** This one is for **Odoo 14 to 18** (and works on 19 too). It talks to Odoo's classic JSON-RPC endpoint `/jsonrpc`. For Odoo 19 and newer, the \"Odoo\" connector (JSON-2 API) is the simpler choice: it needs no user id. Your version is shown at the bottom of **Settings**.\n\n**What you need (four values)**\n1. `ODOO_URL`: the address of your Odoo, scheme and host only, e.g. `https://erp.example.com` or `https://mycompany.odoo.com`. Not the address of a page inside Odoo: nothing after the host, no `/web`, no `?db=` or `#action=`.\n2. `ODOO_DB`: the database name. On Odoo Online it is usually the subdomain. On your own server it is the name shown on the database selector, or the value after `?db=` in the address bar.\n3. `ODOO_API_KEY`: in Odoo open your user menu → **Preferences** (Odoo 14/15: **My Profile**) → **Account Security** → **New API Key**. Odoo shows the key once. If the Account Security tab or the API key button is missing, turn on developer mode (Settings → Activate the developer mode). The key replaces your password for API calls; your login password itself also works but is not recommended.\n4. `ODOO_UID`: the numeric id of the Odoo user the key belongs to. Open **Settings → Users & Companies → Users**, open that user, and read the number after `id=` in the address bar (e.g. `...#id=7&model=res.users` → `7`).\n\nThe key, the user id and the database must belong together: a key of user 7 with `ODOO_UID=2` is refused.\n\n**Errors**: Odoo answers JSON-RPC errors with HTTP 200 and an `error` object (`Access Denied`, `Invalid field 'x' on model 'y'`, `Record does not exist`). Read the `error.data.message` of the response.\n\n**Domains are Odoo's query language.** A domain is a list of triples: `[[\"state\",\"=\",\"sale\"],[\"amount_total\",\">\",1000]]`, implicitly AND-ed. `|` and `!` prefix operators express OR and NOT.\n\n**Always pass `fields`** to `odoo_search_read` and `odoo_read`: Odoo models have hundreds of columns. Start with `odoo_fields_get` to see what exists; field names differ between Odoo versions (e.g. `mobile` on res.partner is gone in 19).\n\n**Permissions follow the user.** The key inherits its owner's access rights and record rules. A restricted user sees fewer rows, not an error.\n\n**Self-hosted**: on **AnythingMCP Cloud** the instance must be reachable from the public internet with a valid TLS certificate. On an internal host, self-host AnythingMCP on the same network and add the host to `SSRF_ALLOWED_HOSTS`.\n\n**Writes**: `odoo_create`, `odoo_write` and `odoo_call_method` change the live database, and Odoo's automations fire as if a person had done it.",
"instructions": "**Which Odoo connector?** This one is for **Odoo 14 to 18** (and works on 19 too). It talks to Odoo's classic JSON-RPC endpoint `/jsonrpc`. For Odoo 19 and newer, the \"Odoo\" connector (JSON-2 API) is the simpler choice: it needs no user id. Your version is shown at the bottom of **Settings**.\n\n**What you need (four values)**\n1. `ODOO_URL`: the address of your Odoo, scheme and host only, e.g. `https://erp.example.com` or `https://mycompany.odoo.com`. Not the address of a page inside Odoo: nothing after the host, no `/web`, no `?db=` or `#action=`.\n2. `ODOO_DB`: the database name. On Odoo Online it is usually the subdomain. On your own server it is the name shown on the database selector, or the value after `?db=` in the address bar.\n3. `ODOO_API_KEY`: in Odoo open your user menu → **Preferences** (Odoo 14/15: **My Profile**) → **Account Security** → **New API Key**. Odoo shows the key once. If the Account Security tab or the API key button is missing, turn on developer mode (Settings → Activate the developer mode). The key replaces your password for API calls; your login password itself also works but is not recommended.\n4. `ODOO_UID`: the numeric id of the Odoo user the key belongs to. Open **Settings → Users & Companies → Users**, open that user, and read the number after `id=` in the address bar (e.g. `...#id=7&model=res.users` → `7`).\n\nThe key, the user id and the database must belong together: a key of user 7 with `ODOO_UID=2` is refused.\n\n**Errors**: Odoo answers JSON-RPC errors inside an HTTP 200 with an `error` object. AnythingMCP raises them as errors carrying Odoo's own message: `Access Denied` (wrong key, user id or database), `Invalid field 'x' on model 'y'` (field names differ between versions), `Record does not exist`.\n\n**Domains are Odoo's query language.** A domain is a list of triples: `[[\"state\",\"=\",\"sale\"],[\"amount_total\",\">\",1000]]`, implicitly AND-ed. `|` and `!` prefix operators express OR and NOT.\n\n**Always pass `fields`** to `odoo_search_read` and `odoo_read`: Odoo models have hundreds of columns. Start with `odoo_fields_get` to see what exists; field names differ between Odoo versions (e.g. `mobile` on res.partner is gone in 19).\n\n**Permissions follow the user.** The key inherits its owner's access rights and record rules. A restricted user sees fewer rows, not an error.\n\n**Self-hosted**: on **AnythingMCP Cloud** the instance must be reachable from the public internet with a valid TLS certificate. On an internal host, self-host AnythingMCP on the same network and add the host to `SSRF_ALLOWED_HOSTS`.\n\n**Writes**: `odoo_create`, `odoo_write` and `odoo_call_method` change the live database, and Odoo's automations fire as if a person had done it.",
"requiredEnvVars": [
"ODOO_URL",
"ODOO_DB",
Expand Down
Loading
Loading