Spaces:
Running
Running
| # SPDX-License-Identifier: Apache-2.0 | |
| # © 2026 Lutar, Stephen P. — SZL Holdings · Doctrine v11 · Perplexity Computer Agent. | |
| """ERP connectors (credential-READY) — NetSuite, Dynamics F&O/BC, Sage, Acumatica, Infor. | |
| Each is a REAL client (documented endpoint + record shape + auth header). With NO | |
| creds → state=READY + exact secret name; NEVER fabricates a record. The moment | |
| credentials land in the Space secret → CONNECTED → live data. | |
| API refs (publicly documented shapes; SZL writes its own original client code): | |
| NetSuite SuiteTalk REST https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1540391670.html | |
| Dynamics 365 F&O OData https://learn.microsoft.com/dynamics365/fin-ops-core/dev-itpro/data-entities/odata | |
| Dynamics 365 BC API https://learn.microsoft.com/dynamics365/business-central/dev-itpro/api-reference/v2.0/ | |
| Sage Intacct / Sage 200 https://developer.sage.com/ | |
| Acumatica REST https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=... | |
| Infor ION API https://docs.infor.com/ | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from ..ready import ReadyConnector | |
| from ..registry import register | |
| # ── NetSuite SuiteTalk REST (oauth2 / token-based auth; no free tier) ───────── | |
| class NetSuiteConnector(ReadyConnector): | |
| id = "netsuite" | |
| label = "Oracle NetSuite (SuiteTalk REST)" | |
| category = "erp" | |
| auth_kind = "oauth2" | |
| free_tier = False | |
| env_vars = ["SZL_NETSUITE_ACCOUNT_ID", "SZL_NETSUITE_ACCESS_TOKEN"] | |
| _primary_secret = "SZL_NETSUITE_ACCESS_TOKEN" | |
| provider_base = "https://{account}.suitetalk.api.netsuite.com/services/rest/record/v1" | |
| docs_url = "https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1540391670.html" | |
| schema_preview = ["id", "entityId", "companyName", "email"] | |
| _read_path = "customer?limit=10" | |
| _record_path = "items" | |
| def _base_url(self): | |
| acct = os.environ.get("SZL_NETSUITE_ACCOUNT_ID", "").lower().replace("_", "-") | |
| return self.provider_base.replace("{account}", acct) | |
| def _auth_header(self): | |
| tok = os.environ.get("SZL_NETSUITE_ACCESS_TOKEN") | |
| return {"Authorization": f"Bearer {tok}"} if tok else {} | |
| # ── Dynamics 365 Finance & Operations (OData; Entra oauth2; no free tier) ───── | |
| class DynamicsFOConnector(ReadyConnector): | |
| id = "dynamics_fo" | |
| label = "Microsoft Dynamics 365 Finance & Operations" | |
| category = "erp" | |
| auth_kind = "oauth2" | |
| free_tier = False | |
| env_vars = ["SZL_DYNAMICS_FO_RESOURCE_URL", "SZL_DYNAMICS_FO_ACCESS_TOKEN", | |
| "SZL_DYNAMICS_FO_TENANT_ID", "SZL_DYNAMICS_FO_CLIENT_ID", | |
| "SZL_DYNAMICS_FO_CLIENT_SECRET"] | |
| _primary_secret = "SZL_DYNAMICS_FO_ACCESS_TOKEN" | |
| provider_base = "{resource_url}/data" | |
| docs_url = "https://learn.microsoft.com/dynamics365/fin-ops-core/dev-itpro/data-entities/odata" | |
| schema_preview = ["CustomerAccount", "Name", "SalesCurrencyCode", "CustomerGroupId"] | |
| _read_path = "CustomersV3?$top=10&$select=CustomerAccount,Name,SalesCurrencyCode" | |
| _record_path = "value" | |
| def _base_url(self): | |
| return self.provider_base.replace("{resource_url}", | |
| os.environ.get("SZL_DYNAMICS_FO_RESOURCE_URL", "").rstrip("/")) | |
| def _auth_header(self): | |
| tok = os.environ.get("SZL_DYNAMICS_FO_ACCESS_TOKEN") | |
| return ({"Authorization": f"Bearer {tok}", "OData-MaxVersion": "4.0", | |
| "OData-Version": "4.0"} if tok else {}) | |
| # ── Dynamics 365 Business Central (API v2.0; Entra oauth2; no free tier) ────── | |
| class DynamicsBCConnector(ReadyConnector): | |
| id = "dynamics_bc" | |
| label = "Microsoft Dynamics 365 Business Central" | |
| category = "erp" | |
| auth_kind = "oauth2" | |
| free_tier = False | |
| env_vars = ["SZL_DYNAMICS_BC_ENV_URL", "SZL_DYNAMICS_BC_ACCESS_TOKEN", | |
| "SZL_DYNAMICS_BC_TENANT_ID", "SZL_DYNAMICS_BC_COMPANY_ID"] | |
| _primary_secret = "SZL_DYNAMICS_BC_ACCESS_TOKEN" | |
| provider_base = "{env_url}/api/v2.0" | |
| docs_url = "https://learn.microsoft.com/dynamics365/business-central/dev-itpro/api-reference/v2.0/" | |
| schema_preview = ["id", "number", "displayName", "email"] | |
| _read_path = "customers?$top=10" | |
| _record_path = "value" | |
| def _base_url(self): | |
| return self.provider_base.replace("{env_url}", | |
| os.environ.get("SZL_DYNAMICS_BC_ENV_URL", "").rstrip("/")) | |
| def _auth_header(self): | |
| tok = os.environ.get("SZL_DYNAMICS_BC_ACCESS_TOKEN") | |
| return {"Authorization": f"Bearer {tok}"} if tok else {} | |
| # ── Sage Intacct (REST/oauth2; no free tier) ────────────────────────────────── | |
| class SageConnector(ReadyConnector): | |
| id = "sage" | |
| label = "Sage Intacct" | |
| category = "erp" | |
| auth_kind = "oauth2" | |
| free_tier = False | |
| env_vars = ["SZL_SAGE_ACCESS_TOKEN", "SZL_SAGE_COMPANY_ID"] | |
| _primary_secret = "SZL_SAGE_ACCESS_TOKEN" | |
| provider_base = "https://api.intacct.com/ia/api/v1" | |
| docs_url = "https://developer.sage.com/" | |
| schema_preview = ["key", "id", "name", "status"] | |
| _read_path = "objects/accounts-receivable/customer?size=10" | |
| _record_path = "ia::result" | |
| def _auth_header(self): | |
| tok = os.environ.get("SZL_SAGE_ACCESS_TOKEN") | |
| return {"Authorization": f"Bearer {tok}"} if tok else {} | |
| # ── Acumatica Cloud ERP (REST; basic/oauth2; no free tier) ──────────────────── | |
| class AcumaticaConnector(ReadyConnector): | |
| id = "acumatica" | |
| label = "Acumatica Cloud ERP" | |
| category = "erp" | |
| auth_kind = "oauth2" | |
| free_tier = False | |
| env_vars = ["SZL_ACUMATICA_SITE_URL", "SZL_ACUMATICA_ACCESS_TOKEN"] | |
| _primary_secret = "SZL_ACUMATICA_ACCESS_TOKEN" | |
| provider_base = "{site_url}/entity/Default/24.200.001" | |
| docs_url = "https://help.acumatica.com/" | |
| schema_preview = ["CustomerID", "CustomerName", "Email", "Status"] | |
| _read_path = "Customer?$top=10" | |
| _record_path = "" | |
| def _base_url(self): | |
| return self.provider_base.replace("{site_url}", | |
| os.environ.get("SZL_ACUMATICA_SITE_URL", "").rstrip("/")) | |
| def _auth_header(self): | |
| tok = os.environ.get("SZL_ACUMATICA_ACCESS_TOKEN") | |
| return {"Authorization": f"Bearer {tok}"} if tok else {} | |
| def _dig(self, raw): | |
| # Acumatica returns a bare JSON array of entity objects | |
| if isinstance(raw, list): | |
| out = [] | |
| for r in raw: | |
| if isinstance(r, dict): | |
| out.append({k: (r.get(k, {}) or {}).get("value") | |
| if isinstance(r.get(k), dict) else r.get(k) | |
| for k in self.schema_preview}) | |
| return out | |
| return [] | |
| # ── Infor (ION API gateway; oauth2; no free tier) ───────────────────────────── | |
| class InforConnector(ReadyConnector): | |
| id = "infor" | |
| label = "Infor (ION API / M3 / LN)" | |
| category = "erp" | |
| auth_kind = "oauth2" | |
| free_tier = False | |
| env_vars = ["SZL_INFOR_ION_URL", "SZL_INFOR_ACCESS_TOKEN", "SZL_INFOR_TENANT"] | |
| _primary_secret = "SZL_INFOR_ACCESS_TOKEN" | |
| provider_base = "{ion_url}" | |
| docs_url = "https://docs.infor.com/" | |
| schema_preview = ["CustomerNumber", "Name", "Country", "Currency"] | |
| _read_path = "M3/m3api-rest/v2/execute/CRS610MI/LstByNumber?maxrecs=10" | |
| _record_path = "MIRecord" | |
| def _base_url(self): | |
| return self.provider_base.replace("{ion_url}", | |
| os.environ.get("SZL_INFOR_ION_URL", "").rstrip("/")) | |
| def _auth_header(self): | |
| tok = os.environ.get("SZL_INFOR_ACCESS_TOKEN") | |
| return {"Authorization": f"Bearer {tok}"} if tok else {} | |
| __all__ = ["NetSuiteConnector", "DynamicsFOConnector", "DynamicsBCConnector", | |
| "SageConnector", "AcumaticaConnector", "InforConnector"] | |