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 BCServices.Nav.FicheArticle— articlesServices.Nav.FicheFournisseur— fournisseursServices.Nav.ListeContactsHorizon— contactsServices.Nav.ListSalesHead/Services.Nav.ListSalesLines— entêtes/lignes de ventesServices.Nav.SalesInvoice/Services.Nav.SalesCreditMemo— factures et avoirsServices.Nav.SalesDocuments/Services.Nav.PostDocument— documents et postageServices.Nav.PostedSalesInvoices/Services.Nav.PostedSalesInvoiceLines— factures postéesServices.Nav.AppliedCustomerEntries/Services.Nav.DetailedCustomerLedgerEntries/Services.Nav.EcrituresClients/Services.Nav.ListCustLedgEntry/Services.Nav.ListDetCustEntry— écritures comptables clientsServices.Nav.Payments— encaissementsServices.Nav.Voyages/Services.Nav.ListeVoyages/Services.Nav.BOMLigneVoyage— entités voyagesServices.Nav.Resources— ressources NAVServices.Nav.DimValues— dimensions analytiquesServices.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.
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 clientNavSalesInvoiceService— création des facturesNavPostDocument— postage de documentsNavPostedInvoiceService— lecture factures postéesNavPostedSalesInvoiceLinesService— lignes de factures postéesNavCustomerLedgerService,NavCustLedgEntryService,NavDetCustEntryService— écrituresNavAppliedCustomerEntriesService,NavDetailedCustomerLedgerService— applications/détailsNavPaymentService— encaissementsNavInvoiceItemService— articles/lignesNavTravelService,NavTravelDetailsService— voyages NAVNavCreditMemoService— avoirsNavResourceService— ressourcesNavDimValueService— dimensionsNavContactService,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 :
- Création / mise à jour fiche client —
NavCustomerService.CreateAsync/UpdateAsync. Déclenché à la création d'un Customer ou modification de ses données. - Post de chaque facture —
NavSalesInvoiceServicepuisNavPostDocument. Déclenché à la génération de toute Invoice (Confirmation, Balance, Simple, Cancellation, CancellationInsurance, BuchardPart, Compensation). Aucun filtre — tout part. - Post de chaque paiement —
NavPaymentService. 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 :
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 selonCustomerType),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 émission3010et annulation3011— incohérence existante non corrigée),Assurance→ 3021/ZERO,Bon cadeau N°etBalance à zéro→ 2061/ZERO,Utilisation Bon Croisitour→ 2062/ZERO. Une factureCancellationInsuranceenvoie 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.CreateNavInvoicey 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 sesInvoiceItemportent 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
SettingVatRatescontient deux lignes aux plages qui se chevauchent (2024-2026 et 2026-2028), etGetByDatefait unFirstOrDefault: 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.csest 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_TravelIDporté par les lignes de facture est résolu parBookingService.ResolveTravelNavNo— cf.domain/business-rules.md > NavNo — source du NAV_TravelID facturé.

