193 lines
8.9 KiB
Python
193 lines
8.9 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""Run My Accounts (Infoniqa) accounting connector.
|
|
|
|
API docs: https://runmyaccountsag.github.io/runmyaccounts-rest-api/
|
|
Auth: Static API key via X-RMA-KEY header.
|
|
Base URL: https://service.runmyaccounts.com/api/latest/clients/{clientName}/
|
|
"""
|
|
|
|
import logging
|
|
from typing import List, Dict, Any
|
|
|
|
import aiohttp
|
|
|
|
from ..accountingConnectorBase import (
|
|
BaseAccountingConnector,
|
|
AccountingBooking,
|
|
AccountingChart,
|
|
ConnectorConfigField,
|
|
SyncResult,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_BASE_URL = "https://service.runmyaccounts.com/api/latest/clients"
|
|
|
|
|
|
class AccountingConnectorRma(BaseAccountingConnector):
|
|
|
|
def getConnectorType(self) -> str:
|
|
return "rma"
|
|
|
|
def getConnectorLabel(self) -> Dict[str, str]:
|
|
return {"en": "Run My Accounts", "de": "Run My Accounts", "fr": "Run My Accounts"}
|
|
|
|
def getRequiredConfigFields(self) -> List[ConnectorConfigField]:
|
|
return [
|
|
ConnectorConfigField(
|
|
key="clientName",
|
|
label={"en": "Client Name", "de": "Mandantenname", "fr": "Nom du client"},
|
|
fieldType="text",
|
|
secret=False,
|
|
placeholder="e.g. meinefirma",
|
|
),
|
|
ConnectorConfigField(
|
|
key="apiKey",
|
|
label={"en": "API Key", "de": "API-Schlüssel", "fr": "Clé API"},
|
|
fieldType="password",
|
|
secret=True,
|
|
),
|
|
]
|
|
|
|
def _buildUrl(self, config: Dict[str, Any], resource: str) -> str:
|
|
clientName = config["clientName"]
|
|
return f"{_BASE_URL}/{clientName}/{resource}"
|
|
|
|
def _buildHeaders(self, config: Dict[str, Any]) -> Dict[str, str]:
|
|
return {
|
|
"X-RMA-KEY": config["apiKey"],
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
async def testConnection(self, config: Dict[str, Any]) -> SyncResult:
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
url = self._buildUrl(config, "customers")
|
|
async with session.get(url, headers=self._buildHeaders(config), timeout=aiohttp.ClientTimeout(total=15)) as resp:
|
|
if resp.status == 200:
|
|
return SyncResult(success=True)
|
|
body = await resp.text()
|
|
return SyncResult(success=False, errorMessage=f"HTTP {resp.status}: {body[:200]}")
|
|
except Exception as e:
|
|
return SyncResult(success=False, errorMessage=str(e))
|
|
|
|
async def getChartOfAccounts(self, config: Dict[str, Any]) -> List[AccountingChart]:
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
url = self._buildUrl(config, "charts")
|
|
async with session.get(url, headers=self._buildHeaders(config), timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
|
if resp.status != 200:
|
|
logger.error(f"RMA charts failed: HTTP {resp.status}")
|
|
return []
|
|
data = await resp.json()
|
|
|
|
charts = []
|
|
items = data if isinstance(data, list) else data.get("chart", data.get("row", []))
|
|
for item in items:
|
|
if isinstance(item, dict):
|
|
accNo = item.get("accno", item.get("account_number", ""))
|
|
label = item.get("description", item.get("label", ""))
|
|
charts.append(AccountingChart(accountNumber=str(accNo), label=str(label)))
|
|
return charts
|
|
except Exception as e:
|
|
logger.error(f"RMA getChartOfAccounts error: {e}")
|
|
return []
|
|
|
|
async def pushBooking(self, config: Dict[str, Any], booking: AccountingBooking) -> SyncResult:
|
|
"""Push a GL batch booking to RMA."""
|
|
try:
|
|
entries = []
|
|
for line in booking.lines:
|
|
entry = {
|
|
"accno": line.accountNumber,
|
|
"transdate": booking.bookingDate,
|
|
"reference": booking.reference,
|
|
"description": line.description or booking.description,
|
|
}
|
|
if line.debitAmount > 0:
|
|
entry["debit"] = line.debitAmount
|
|
if line.creditAmount > 0:
|
|
entry["credit"] = line.creditAmount
|
|
if line.taxCode:
|
|
entry["tax_code"] = line.taxCode
|
|
entries.append(entry)
|
|
|
|
payload = {"gl_batch": {"entry": entries}}
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
url = self._buildUrl(config, "gl")
|
|
async with session.post(url, headers=self._buildHeaders(config), json=payload, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
|
body = await resp.text()
|
|
if resp.status in (200, 201, 204):
|
|
return SyncResult(success=True, rawResponse={"status": resp.status, "body": body[:500]})
|
|
return SyncResult(success=False, errorMessage=f"HTTP {resp.status}: {body[:300]}")
|
|
except Exception as e:
|
|
return SyncResult(success=False, errorMessage=str(e))
|
|
|
|
async def getBookingStatus(self, config: Dict[str, Any], externalId: str) -> SyncResult:
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
url = self._buildUrl(config, f"charts/{externalId}/transactions")
|
|
async with session.get(url, headers=self._buildHeaders(config), timeout=aiohttp.ClientTimeout(total=15)) as resp:
|
|
if resp.status == 200:
|
|
return SyncResult(success=True, externalId=externalId)
|
|
return SyncResult(success=False, errorMessage=f"HTTP {resp.status}")
|
|
except Exception as e:
|
|
return SyncResult(success=False, errorMessage=str(e))
|
|
|
|
async def pushInvoice(self, config: Dict[str, Any], invoice: Dict[str, Any]) -> SyncResult:
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
url = self._buildUrl(config, "invoices")
|
|
async with session.post(url, headers=self._buildHeaders(config), json=invoice, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
|
body = await resp.text()
|
|
if resp.status in (200, 201):
|
|
return SyncResult(success=True, rawResponse={"body": body[:500]})
|
|
return SyncResult(success=False, errorMessage=f"HTTP {resp.status}: {body[:300]}")
|
|
except Exception as e:
|
|
return SyncResult(success=False, errorMessage=str(e))
|
|
|
|
async def getCustomers(self, config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
url = self._buildUrl(config, "customers")
|
|
async with session.get(url, headers=self._buildHeaders(config), timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
|
if resp.status != 200:
|
|
return []
|
|
data = await resp.json()
|
|
return data if isinstance(data, list) else data.get("customer", [])
|
|
except Exception as e:
|
|
logger.error(f"RMA getCustomers error: {e}")
|
|
return []
|
|
|
|
async def getVendors(self, config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
url = self._buildUrl(config, "vendors")
|
|
async with session.get(url, headers=self._buildHeaders(config), timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
|
if resp.status != 200:
|
|
return []
|
|
data = await resp.json()
|
|
return data if isinstance(data, list) else data.get("vendor", [])
|
|
except Exception as e:
|
|
logger.error(f"RMA getVendors error: {e}")
|
|
return []
|
|
|
|
async def uploadDocument(self, config: Dict[str, Any], fileName: str, fileContent: bytes, mimeType: str = "application/pdf") -> SyncResult:
|
|
"""Upload a receipt via POST /belege (multipart/form-data)."""
|
|
try:
|
|
formData = aiohttp.FormData()
|
|
formData.add_field("Filedata", fileContent, filename=fileName, content_type=mimeType)
|
|
|
|
headers = {"X-RMA-KEY": config["apiKey"], "Accept": "application/json"}
|
|
async with aiohttp.ClientSession() as session:
|
|
url = self._buildUrl(config, "belege")
|
|
async with session.post(url, headers=headers, data=formData, timeout=aiohttp.ClientTimeout(total=60)) as resp:
|
|
body = await resp.text()
|
|
if resp.status in (200, 201):
|
|
return SyncResult(success=True, rawResponse={"body": body[:500]})
|
|
return SyncResult(success=False, errorMessage=f"HTTP {resp.status}: {body[:300]}")
|
|
except Exception as e:
|
|
return SyncResult(success=False, errorMessage=str(e))
|