Skip to content

Intégration NAV / Business Central

Contexte

Buchard utilise Microsoft Dynamics NAV (anciennement) / Business Central (récent) comme ERP comptable. Horizon dialogue avec NAV/BC en SOAP via 25+ "Connected Services" qui exposent des web services NAV (codeunits, pages, queries publiées en SOAP).

Structure code

Application/Connected Services/Services.Nav.*

25 références SOAP auto-générées (un dossier par WSDL). Notables :

  • Services.Nav.FicheClient — fiche client BC
  • Services.Nav.FicheArticle — articles
  • Services.Nav.FicheFournisseur — fournisseurs
  • Services.Nav.ListeContactsHorizon — contacts
  • Services.Nav.ListSalesHead / Services.Nav.ListSalesLines — entêtes/lignes de ventes
  • Services.Nav.SalesInvoice / Services.Nav.SalesCreditMemo — factures et avoirs
  • Services.Nav.SalesDocuments / Services.Nav.PostDocument — documents et postage
  • Services.Nav.PostedSalesInvoices / Services.Nav.PostedSalesInvoiceLines — factures postées
  • Services.Nav.AppliedCustomerEntries / Services.Nav.DetailedCustomerLedgerEntries / Services.Nav.EcrituresClients / Services.Nav.ListCustLedgEntry / Services.Nav.ListDetCustEntry — écritures comptables clients
  • Services.Nav.Payments — encaissements
  • Services.Nav.Voyages / Services.Nav.ListeVoyages / Services.Nav.BOMLigneVoyage — entités voyages
  • Services.Nav.Resources — ressources NAV
  • Services.Nav.DimValues — dimensions analytiques
  • Services.Nav.Providers — fournisseurs

Services.SwissBillingV3 — passerelle SwissBilling (déprécié, remplacé par CembraPay).

Ne jamais modifier les Reference.cs dans ces dossiers. Régénération via update-wsdl-files.ps1 (script PowerShell à la racine) qui invoque dotnet-svcutil.

tools/BcSmokeTest

Projet console lancé à la main après une régénération de WSDL (ex. v27 → v28) pour vérifier que les web services BC répondent toujours. Il est dans Horizon.sln (dossier solution tools) mais exclu du CI via Horizon-CI.slnf.

bash
dotnet run --project tools/BcSmokeTest          # lectures + CRUD client
dotnet run --project tools/BcSmokeTest -- --full  # sonde aussi facture/paiement (ECRIT dans BC !)

Config lue depuis src/Web/appsettings*.json, section BusinessCentral — ⚠ pointer sur une instance BC de TEST.

Application/Services/ERP/Nav/

Services wrapper qui appellent les Connected Services et exposent une API .NET propre :

  • NavCustomerService — création/MAJ fiche client
  • NavSalesInvoiceService — création des factures
  • NavPostDocument — postage de documents
  • NavPostedInvoiceService — lecture factures postées
  • NavPostedSalesInvoiceLinesService — lignes de factures postées
  • NavCustomerLedgerService, NavCustLedgEntryService, NavDetCustEntryService — écritures
  • NavAppliedCustomerEntriesService, NavDetailedCustomerLedgerService — applications/détails
  • NavPaymentService — encaissements
  • NavInvoiceItemService — articles/lignes
  • NavTravelService, NavTravelDetailsService — voyages NAV
  • NavCreditMemoService — avoirs
  • NavResourceService — ressources
  • NavDimValueService — dimensions
  • NavContactService, NavProviderService — contacts, fournisseurs

Application/Gateways/Bc/BcSoapService.cs

Service bas-niveau qui fournit le HttpClient configuré avec auth NTLM/Windows pour les appels SOAP.

Configuration

appsettings.json section BusinessCentral (mapping BcOptions) :

  • URLs des web services NAV
  • Identifiants Windows / NTLM

Quand Horizon écrit dans NAV

D'après l'interview, 3 events principaux :

  1. Création / mise à jour fiche clientNavCustomerService.CreateAsync / UpdateAsync. Déclenché à la création d'un Customer ou modification de ses données.
  2. Post de chaque factureNavSalesInvoiceService puis NavPostDocument. Déclenché à la génération de toute Invoice (Confirmation, Balance, Simple, Cancellation, CancellationInsurance, BuchardPart, Compensation). Aucun filtre — tout part.
  3. Post de chaque paiementNavPaymentService. Déclenché à la création d'un Payment (cash, CB, CembraPay, etc.).

D'autres écritures existent (voyages, articles) mais elles sont moins fréquentes / plus structurelles.

Quand Horizon lit depuis NAV

  • NavPostedInvoiceService.GetBatch — lecture des factures postées (réconciliation).
  • NavCustomerLedgerService — écritures client pour le compte client / suivi paiements.
  • NavService<FicheClient/FicheFournisseur/...> — lookups divers.

DI : mode mock pour Integration

Dans ApplicationModule.ConfigureServices, l'environnement Integration (CI / staging sans NAV accessible) mocke tous les services NAV avec Moq. Voir lignes 142+ de ApplicationModule.cs :

csharp
if (!environment.IsIntegration())
{
    services.AddScoped<INavCustomerService, NavCustomerService>();
    // ... vraies impls
}
else
{
    var mockProviderService = new Mock<INavService<FicheFournisseur>>();
    mockProviderService.Setup(...);
    services.AddSingleton(mockProviderService.Object);
    // ... mocks pour tous les autres
}

⚠ Conséquence : en environnement Integration, Horizon ne tape pas BC. Aucun risque d'effet de bord, mais aussi aucun test réel de l'intégration.

Avoirs — reconstruction du traitement TVA

La page BC PostedSalesInvoiceLines n'expose ni VAT Prod. Posting Group ni Gen. Prod. Posting Group (vérifié dans le WSDL : elle donne No, Description, Quantity, Unit_Price, Amount, Amount_Including_VAT). Un avoir ne peut donc pas recopier le traitement TVA de la facture qu'il annule. Côté Horizon non plus : Invoice.VatType / VatRate / VatIncluded et InvoiceItem.Account / NAV_VAT_Prod_Posting_Group / NAV_Gen_Prod_Posting_Group / ForceNoVAT sont tous [NotMapped] — une facture relue en base a perdu son identité fiscale (seul Invoice.VatGroup est persisté, et il n'est jamais assigné : toujours "NATIONAL" par défaut).

Conséquence historique (bug corrigé août 2026, Asana 1217533476137921) : NavCreditMemoService posait VAT_Prod_Posting_Group = _settingVatRateService.GetCurrent().Group — soit NORM24, le groupe suisse taxable — sur toutes les lignes de tous les avoirs, depuis cb1bcf9b (juillet 2023). Tout avoir sur un voyage zone rose ou export sortait donc avec 8.1 % de TVA que la facture ne portait pas. Cas type : résa 6458 / 26.1 OMAN (départ en OutsidePinkArea), avoir BC 26128241 émis avec 338.99 de TVA sur 4'185.— HT. Portée mesurée sur la copie de prod : ~6 800 factures sur voyages non-suisses avec CreditMemoCreated = 1 (2026 : 839 zone rose + 4 613 export ; 2025 : 236 + 1 130).

Correctif retenu : CreditMemoVatMapping (statique, testable, à côté du service) recalcule le couple compte + groupe depuis le départ, comme BookingService.CreateOrUpdateInvoice à l'émission — OccurrenceCost.VatType + SettingVatRate.GetByDate(occurrence.Start) (taux daté sur le départ, plus GetCurrent()).

Document_Type.Credit_Memo n'existe que dans NavCreditMemoService : tous les avoirs de l'application héritent donc du correctif. Points d'entrée vivants — modification d'une résa facturée avec changement de montant (InvoiceService.InternalUpdateAsync:483), passage Confirmé→Facturé avec montant changé (BookingService:2689), annulation totale (BookingService:3068/3071/3084), annulation partielle (CreateForCancelledPassengerAsync), bons cadeaux (GiftService:190/504/518/526), annulation de facture (InvoiceService.Delete:598). Sur le flux de réédition, avoir et nouvelle facture dérivent désormais du même VatType et du même taux daté, donc ne peuvent plus diverger — c'était la cause du symptôme « TVA ajoutée car modification de la réservation ».

  • Lignes qui suivent le VatType : transport/hébergement, suppléments, Rabais (…) (compte selon CustomerType), Points fidélité (3916 suisse / 3917 sinon).
  • Lignes à traitement fixe, quel que soit le voyage : Forfait TVA (taxable, compte conservé car il diffère entre émission 3010 et annulation 3011 — incohérence existante non corrigée), Assurance → 3021/ZERO, Bon cadeau N° et Balance à zéro → 2061/ZERO, Utilisation Bon Croisitour → 2062/ZERO. Une facture CancellationInsurance envoie toutes ses lignes sur 3022/ZERO.
  • Le classement se fait par préfixe de description — seule prise disponible. Les descriptions BC ne sont pas celles d'Horizon : InvoiceService.CreateNavInvoice y concatène - {Nom} {Prénom}. Matcher sur préfixe, jamais sur égalité. Tests : CreditMemoVatMappingTest.
  • Catégories sans voyage (Gift, Membership) : BuildWithoutTrip → taux suisse, comptes de la facture conservés.
  • CreateForCancelledPassengerAsync (annulation partielle) reçoit une facture encore en mémoire, donc ses InvoiceItem portent toujours leur groupe : il est désormais transmis tel quel au lieu de laisser BC retomber sur son paramétrage.

Effet de bord assumé : quand le VatType d'un départ a été corrigé après l'émission de la facture, l'avoir recalculé ne contrepasse plus le montant de la facture (OMAN : 4'524 → 4'185). C'est voulu — la compta reprend la TVA d'origine à la main une fois — mais ça doit être connu.

Correction propre à terme : persister l'identité fiscale (InvoiceItem.Account / NAV_*, Invoice.VatType / VatRate) pour pouvoir rejouer la facture au lieu de la reconstruire. Prévu avec la refonte Booking/Invoice, cf. ADR 0011.

Pièges connus

  • SettingVatRates contient deux lignes aux plages qui se chevauchent (2024-2026 et 2026-2028), et GetByDate fait un FirstOrDefault : la ligne retournée pour une date 2026 n'est pas déterministe. Sans effet aujourd'hui (les deux valent 8.10 / NORM24), mais à assainir avant tout changement de taux.
  • Latence SOAP : un appel NAV peut prendre plusieurs secondes. Pour les opérations en série (post de plusieurs factures), prévoir async + parallélisation prudente.
  • Reference.cs auto-générées : si on ajoute un champ côté NAV, il faut regénérer toutes les références concernées via le script. Ne pas éditer à la main.
  • Services.Nav.ListeVoyages/Reference.cs est explicitement exclu de la compilation (<Compile Remove="..."/> dans le csproj). À investiguer si on a besoin de ce service.
  • Authentification : les Web Services NAV utilisent typiquement NTLM, ce qui pose des problèmes hors environnement Windows / domaine. À garder en tête pour le dev local.
  • NavNo : c'est l'identifiant côté BC (numéro de document). Stocké sur Occurrence, SeasideDate, OneDayTravelDriveOccurrences (par trajet et par date, courses d'un jour — sert aux compteurs passagers Visual Planning, pas à la facturation), Invoice, Payment. C'est ce qui permet à Horizon de retrouver une entité dans BC. Le NAV_TravelID porté par les lignes de facture est résolu par BookingService.ResolveTravelNavNo — cf. domain/business-rules.md > NavNo — source du NAV_TravelID facturé.

Contributors

No contributors

Changelog

No recent changes