sefersubasi/exact-api

Exact API 的 PHP 客户端

dev-main 2024-03-25 22:48 UTC

This package is not auto-updated.

Last update: 2024-09-24 23:11:57 UTC


README

Run phpunit

PHP 客户端库,用于 Exact Online API。此客户端允许您与 Exact Online 集成,例如

  • 创建并发送发票,
  • 添加日记条目,
  • 或上传收到的发票。

此客户端使用与 Exact API 相同的命名和约定,因此了解此客户端的最佳方式是查看 Exact Online 文档API 参考

此库由 Picqer 创建和维护。我们正在寻找加入我们团队的 PHP 开发者

Composer 安装

通过 Composer 安装此 PHP 客户端非常简单。

composer require sefersubasi/exact-api

用法

  1. 在 Exact App Center 设置应用以获取凭据
  2. 从您的应用授权集成
  3. 解析回调并完成连接设置
  4. 使用库执行操作

步骤 1-3 在设置时只需执行一次。

在 Exact App Center 设置应用以获取凭据

在 Exact App Center 设置应用以获取您的 Client IDClient Secret。您还需要设置正确的 Callback URL 以确保 OAuth 舞蹈正常工作。

从您的应用授权集成

下面的代码是一个示例 authorize() 函数。

$connection = new \Picqer\Financials\Exact\Connection();
$connection->setRedirectUrl('CALLBACK_URL'); // Same as entered online in the App Center
$connection->setExactClientId('CLIENT_ID');
$connection->setExactClientSecret('CLIENT_SECRET');
$connection->redirectForAuthorization();

这将重定向用户到 Exact 进行登录并授权您的集成。

解析回调并完成连接设置

Exact 将重定向回您提供的 callback url。回调将接收一个 code 参数。这是 OAuth 的授权码。保存此代码。

创建一个新的连接到 Exact,以便库可以交换代码并获取 accesstokenrefreshtokenaccesstoken 是一个临时令牌,允许您的应用与 Exact 通信。refreshtoken 是一个令牌,用于获取新的 accesstoken,同时刷新 refreshtoken。库会为您处理所有这些。下面的代码可以是一个通用的 connect() 函数,它返回 API 连接。

$connection = new \Picqer\Financials\Exact\Connection();
$connection->setRedirectUrl('CALLBACK_URL');
$connection->setExactClientId('CLIENT_ID');
$connection->setExactClientSecret('CLIENT_SECRET');

if (getValue('authorizationcode')) {
    // Retrieves authorizationcode from database
    $connection->setAuthorizationCode(getValue('authorizationcode'));
}

if (getValue('accesstoken')) {
    // Retrieves accesstoken from database
    $connection->setAccessToken(unserialize(getValue('accesstoken')));
}

if (getValue('refreshtoken')) {
    // Retrieves refreshtoken from database
    $connection->setRefreshToken(getValue('refreshtoken'));
}

if (getValue('expires_in')) {
    // Retrieves expires timestamp from database
    $connection->setTokenExpires(getValue('expires_in'));
}

// Make the client connect and exchange tokens
try {
    $connection->connect();
} catch (\Exception $e)
{
    throw new Exception('Could not connect to Exact: ' . $e->getMessage());
}

// Save the new tokens for next connections
setValue('accesstoken', serialize($connection->getAccessToken()));
setValue('refreshtoken', $connection->getRefreshToken());

// Optionally, save the expiry-timestamp. This prevents exchanging valid tokens (ie. saves you some requests)
setValue('expires_in', $connection->getTokenExpires());

// Optionally, set the lock and unlock callbacks to prevent multiple request for acquiring a new refresh token with the same refresh token.
$connection->setAcquireAccessTokenLockCallback('CALLBACK_FUNCTION');
$connection->setAcquireAccessTokenUnlockCallback('CALLBACK_FUNCTION');

关于部门(管理)

默认情况下,库将使用用户的默认管理。这意味着当用户在 Exact Online 中切换部门时,库也将开始使用此部门。

速率限制

Exact 使用每分钟和每日速率限制。每天可以向每个公司进行最大数量的调用,并且为了防止超限,它们还实施每分钟的限制。此 PR 将此信息存储在 Connection 中,并添加了读取速率限制的方法,以便您可以根据您的应用适当处理这些限制。有关速率限制的 Exact 文档可在以下位置找到: https://support.exactonline.com/community/s/knowledge-base#All-All-DNO-Simulation-gen-apilimits

如果您达到速率限制,将抛出带有代码 429 的 ApiException。在这种情况下,您可以确定是否已达到每分钟或每日限制。如果已达到每分钟限制,请等待 60 秒后重试。如果已达到每日限制,请等待每日重置后重试。

您可以在 Connection 上使用以下方法,这些方法将在第一次 API 调用后返回限制(基于 Exact 的标题)。

$connection->getDailyLimit(); // Retrieve your daily limit
$connection->getDailyLimitRemaining(); // Retrieve the remaining amount of API calls for this day
$connection->getDailyLimitReset(); // Retrieve the timestamp for when the limit will reset
$connection->getMinutelyLimit(); // Retrieve your limit per minute
$connection->getMinutelyLimitRemaining(); // Retrieve the amount of API calls remaining for this minute
$connection->getMinutelyLimitReset(); // Retrieve the timestamp for when the minutely limit will reset

请注意,当您没有更多的每分钟调用可用时,Exact 只会发送每分钟限制标题。因此,在这种情况下,每日限制标题将保持为 0,直到每分钟重置。

在达到分钟速率限制时,有基本的sleep支持。如果您启用“在分钟速率限制达到时等待”,则客户端将休眠,直到限制重置。不考虑每日限制。

$connection->setWaitOnMinutelyRateLimitHit(true);

使用库进行操作(示例)

// Optionally set administration, otherwise use the current administration of the user
$connection->setDivision(123456);

// Create a new account
$account = new \Picqer\Financials\Exact\Account($connection);
$account->AddressLine1 = 'Customers address line';
$account->AddressLine2 = 'Customer address line 2';
$account->City = 'Customer city';
$account->Code = 'Customer code';
$account->Country = 'Customer country';
$account->IsSales = 'true';
$account->Name = 'Customer name';
$account->Postcode = 'Customer postcode';
$account->Status = 'C';
$account->save();

// Add a product in Exact
$item = new \Picqer\Financials\Exact\Item($connection);
$item->Code = 'product code';
$item->CostPriceStandard = 2.50;
$item->Description = 'product description';
$item->IsSalesItem = true;
$item->SalesVatCode = 'VH';
$item->save();

// Retrieve an item by id
$item = new \Picqer\Financials\Exact\Item($connection);
$id = '097A82A9-6EF7-4EDC-8036-3F7559D9EF82';
$item->find($id);

// List items
$item = new \Picqer\Financials\Exact\Item($connection);
$item->get();

// List items as a generator
$item = new \Picqer\Financials\Exact\Item($connection);
$item->getGenerator();

// List items with filter (using a filter always returns a collection)
$item = new \Picqer\Financials\Exact\Item($connection);
$items = $item->filter("Code eq '$item->Code'"); // Uses filters as described in Exact API docs (odata filters)

// Create new invoice with invoice lines
$invoiceLines[] = [
    'Item'      => $item->ID,
    'Quantity'  => 1,
    'UnitPrice' => $item->CostPriceStandard
];

$salesInvoice = new \Picqer\Financials\Exact\SalesInvoice($connection);
$salesInvoice->InvoiceTo = $account->ID;
$salesInvoice->OrderedBy = $account->ID;
$salesInvoice->YourRef = 'Invoice reference';
$salesInvoice->SalesInvoiceLines = $invoiceLines;
$salesInvoice->save();

// Print and email the invoice
$printedInvoice = new \Picqer\Financials\Exact\PrintedSalesInvoice($connection);
$printedInvoice->InvoiceID = $salesInvoice->InvoiceID;
$printedInvoice->SendEmailToCustomer = true;
$printedInvoice->SenderEmailAddress = "from@example.com";
$printedInvoice->DocumentLayout = "401f3020-35cd-49a2-843a-d904df0c09ff";
$printedInvoice->ExtraText = "Some additional text";
$printedInvoice->save();

连接到荷兰以外的其他Exact国家

根据Exact开发者指南选择正确的基地址

$connection = new \Picqer\Financials\Exact\Connection();
$connection->setRedirectUrl('CALLBACK_URL');
$connection->setExactClientId('CLIENT_ID');
$connection->setExactClientSecret('CLIENT_SECRET');
$connection->setBaseUrl('https://start.exactonline.de');

查看src/Picqer/Financials/Exact以获取所有可用实体。

Webhooks

可以通过WebhookSubscription实体管理webhook订阅。

对于验证传入的webhook调用,您可以使用Authenticatable特质。向authenticate方法提供完整的JSON请求和Exact提供的Webhook secret,它将返回true或false。

故障排除

出现消息为'Picqer\Financials\Exact\ApiException'的错误'错误400:请在查询字符串中添加一个$select或$a top=1语句。

在特定情况下,遗憾的是,Exact API文档中没有记录这一点,这是一个要求。可能是为了防止请求过多。遇到此错误时,您需要添加一个select或top。Select用于提供您想要提取的字段列表,$top=1将结果限制为一条项目。

示例

仅返回EntryID和FinancialYear。

$test = new \Picqer\Financials\Exact\GeneralJournalEntry($connection);
var_dump($test->filter('', '', 'EntryID, FinancialYear'));

添加$top=1的方式如下

$test = new \Picqer\Financials\Exact\GeneralJournalEntry($connection);
var_dump($test->filter('', '', '', ['$top'=> 1]));

身份验证错误

发生错误:未捕获异常:无法连接到Exact:客户端错误:POST https://start.exactonline.nl/api/oauth2/token 导致了400 Bad Request响应:Bad Request in /var/www/html/oauth_call_connect.php:61 栈跟踪:#0 {main} 在 /var/www/html/oauth_call_connect.php 第61行抛出`

此错误发生是因为您在重定向URL中获得的代码仅对一次调用有效。当您再次调用身份验证过程时,使用一个“已使用”的代码。您会得到这个错误。请确保您只使用Exact Online提供的代码一次来获取访问令牌。

代码示例

例如,请参阅:example/example.php

Guzzle版本

从v3开始支持Guzzle 6和7。对于Guzzle 3,请使用v1

待办事项

  • 当前实体不包含所有可用属性。如果您需要它们,请随时提交包含或扩展实体的PR。使用greasemonkey或tampermonkey中的`userscript.js`来生成一致且完整的实体。