Par J.F. Robichaud
14 avril 2025

Connecter Acomba à d'autres logiciels via le SDK : Une solution transitoire

Le logiciel comptable Acomba est bien connu et apprécié des PME au Québec. Conçu pour les réalités d'ici, il offre une bonne gestion des fiscalités canadienne et québécoise. Même s'il reste largement utilisé, Acomba commence à prendre de l'âge, et la concurrence ne manque pas. D'ailleurs, Acceo, l'entreprise derrière le logiciel, encourage activement la transition vers Acomba GO, sa version cloud plus moderne.

Toutefois, pour les entreprises qui utilisent encore Acomba, il est possible de le connecter aux autres systèmes via le SDK, qui est disponible via un abonnement au programme pour développeurs.

Dans cet article, je vous explique comment interfacer le SDK via une API REST, avec un exemple hyper simple pour obtenir un produit par son numéro (SKU) en C# avec .NET Core. On va ensuite exposer l'API via IIS.

Créer le projet dans Visual Studio

On commence par créer un projet de type "ASP.NET Core Web API":


Attention, ici ça risque de piquer les yeux un peu, on va sortir la bonne vieille techno COM (Component Object Model) des boules à mites. Il nous faut en effet ajouter une référence COM au SDK d'Acomba (« AcoSDK Library ») :


Ce n'est pas fini, on a aussi besoin de créer une configuration « x86 » parce qu'on va rouler en 32 bits (je vous avais averti!):


C'est bon, on peut coder!

Invoquer le SDK à partir de C#

Pour fins de démo, on va faire un aller-retour minimaliste en partant d'un contrôleur qui va appeller un service, lequel va retourner un produit.


Création d'un service pour interagir avec le SDK

Le service va être responsable d'encapsuler les appels au SDK. La méthode GetProductBySku() ci-dessous montre comment aller chercher le produit dans Acomba. La méthode FindKey() du SDK retourne la position de la fiche du produit ("CardPos"). Une valeur de zéro signifie que le produit est introuvable.

                
public ProductModel GetProductBySku(string sku)
{
    EnsureInitialized();

    var productInterface = new Product();
    productInterface.PKey_PrNumber = sku;

    var result = productInterface.FindKey(1, true);
    if (result != 0)
    {
        return null;
    }

    var model = _mapper.MapProduct(productInterface);
    return model;
}
                
            

Si le produit est trouvé, on va récupérer les champs qui nous intéressent.

            
public ProductModel MapProduct(Product productInterface)
{
    var model = new ProductModel();

    model.Sku = productInterface.PrNumber;
    model.Description = productInterface.PrDescription[1];
    model.Price = productInterface.PrSellingPrice[0, 1];
    model.AvailableQty = productInterface.PrQtyOnHand;
    model.Comments = productInterface.PrComments;

    return model;
}
            
          

On va appeler GetProductBySku() à partir d'une méthode HttpGet dans le contrôleur, dans lequel on aura injecté le service.

            
[ApiController]
[Route("[controller]")]
public class ProductController : ControllerBase
{
    private readonly IAcombaService _acombaService;

    public ProductController(IAcombaService acombaService)
    {
        ArgumentNullException.ThrowIfNull(acombaService);
        _acombaService = acombaService;
    }

    [HttpGet]
    [Route("{sku}")]
    public IActionResult GetProductBySku(string sku)
    {
        var product = _acombaService.GetProductBySku(sku);
        if (product == null)
        {
            return NotFound();
        }

        return Ok(product);
    }
}                        
              
          
Connexion à Acomba

La méthode EnsureInitialized() vue précédemment est responsable de se connecter à Acomba, si ce n'est pas déjà fait, en appelant OpenCompany() que voici:

            
public void OpenCompany(string execPath, string companyPath)
{
    var acoSdk = new AcoSDKX();    

    int result = acoSdk.Start(acoSdk.VaVersionSDK);
    if (result != 0)
    {
        throw new AcombaException(result);
    }

    var acomba = new AcombaX();

    result = acomba.CompanyExists(companyPath);
    if (result == 0)
    {
        throw new Exception($"Invalid company file: {companyPath}");
    }

    result = acomba.OpenCompany(execPath, companyPath);
    if (result != 0)
    {
        throw new AcombaException(result);
    }

    var userInterface = new User();
    userInterface.PKey_UsNumber = _acombaSettings.User;
    result = userInterface.FindKey(1, false);
    if (result != 0)
    {
        throw new AcombaException("User not found", result);
    }

    result = acomba.LogCurrentUser(userInterface.Key_UsCardPos, _acombaSettings.Pwd);
    if (result != 0)
    {
        throw new AcombaException(result);
    }
}
            
          

Le SDK utilise abondamment les codes d'erreur, mais en C# (ou autre langage objet), il peut s'avérer judicieux de se créer une classe d'exception (sans trop en abuser, ce que j'ai peut-être fait ci-haut, si on considère qu'une erreur de mot de passe est plus un flot normal qu'une exception).

Avec le code d'erreur en main, on peut appeler la fonction GetErrorMessage() du SDK pour obtenir le message correspondant.

Concernant les paramètres, je les conserve dans appsettings.json:

            
"Acomba": {
  "ExecPath": "C:\\Aco_SDK",
  "CompanyPath": "C:\\F1000.dta\\DemoSDK_FR",
  "User": "API",
  "Pwd": "API"
}
            
          

Et je les mappe dans un POCO comme suit (dans Program.cs):

            
builder.Services.Configure(builder.Configuration.GetSection("Acomba"));              
            
          

On peut ensuite les injecter avec IOptions<T>:

              
public AcombaService(IOptions<AcombaSettings> acombaSettings, IMapper mapper)
{
  ...
}     
              
            

ATTENTION, ici j'utilise les credentials directement à partir des appsettings, mais dans du code de production, la bonne approche serait de les demander à l'utilisateur de manière interactive.

Alright, on peut maintenant rouler ça (avec Postman, Swagger...) En tant que fan de grillades, je choisis minutieusement le produit "BBQ-65" de la fameuse société démo "Le Magasin de la Rénovation inc.":

On est maintenant prêt à exécuter le tout à l'extérieur de Visual Studio, après avoir fait un Publish vers un répertoire de destination.

Exposer l'API via IIS

Étant donné qu'Acomba roule sur Windows, IIS se présente comme un choix naturel pour faire un reverse proxy, puisqu'on demeure dans l'écosystème Microsoft.

Tout d'abord, pour rouler .NET Core dans IIS, il faut installer le .NET Core Module/Hosting Bundle.

Ensuite, dans inetmgr.exe, on se crée un Application Pool dédié. Étant donné qu'on est en .NET Core, on choisit "No Managed Code" au lieu de spécifier la version du .NET CLR.


Rappellons-nous qu'on roule en 32 bits, donc il faut mettre "Enable 32-Bit Applications" à True (dans Advanced Settings):


On crée un nouveau site, en utilisant l'Application Pool tout juste créé, et on fait pointer Physical path vers le répertoire cible du Publish executé précédemment.

Toujours dans l'optique d'une démo, on peut se simplifier la vie et utiliser le port 5000 dans IIS, afin de le faire correspondre au port par défaut de Kestrel (pour http, sinon ça serait plutôt 5001 pour https), de manière à ce que le forwarding fonctionne directement.


Done! Maintenant, retourner dans Postman, mais cette fois-ci avec l'URL http://localhost:5000/Product/BBQ-65.

Recommendations pour les prochaines étapes

Ce n'est évidemment qu'un début. Voici ce qui vous attend encore (entre autres!)

  • - Utiliser une machine dédiée comme proxy (i.e. ne pas installer l'API sur le même serveur qu'Acomba!) À noter que CompanyPath supporte les chemins UNC.
  • - S'assurer d'être en HTTPS
  • - Authentifier les utilisateurs (avec Entra peut-être?)
  • - Autoriser seulement les appels provenant d'une certaine IP ou plage d'IP (IP whitelisting)
  • - Implémenter le rate limiting (code HTTP 429)
  • - ...
En conclusion

Bien des entreprises qui utilisent Acomba ne sont pas nécessairement prêtes à faire la transition vers un système comptable plus moderne (Acomba GO ou autre). En attendant, on a une solution temporaire. Mais attention! On le sait tous, ce qui est "temporaire" finit souvent par devenir permanent dans notre milieu! Assurons-nous donc d'aider ces entreprises à réussir pleinement leur transition.

Scroll