AFAS Profit bijlagen exporteren (Powershell)

AFAS Profit bijlagen exporteren is in bulk helaas niet zo eenvoudig. Als je een keer een enkele bijlage uit een AFAS Profit dossier nodig hebt ga je eenvoudig in de applicatie naar het desbetreffende dossieritem en download je de bijlage. Maar wat als je nou van alle abonnementen uit één groep of administratie alle bijlages wil exporteren om deze in een andere applicatie in te lezen?

Dit leek me wel een mooie Powershell oefening en na enig uitstel ben ik er maar eens voor gaan zitten. Ik sla de voorbereidende stappen in AFAS Profit even over in deze post, maar ik zal verwijzen naar de bestaande instructies voor die stappen.

Wat gaan we doen?

Doel: Alle bijlages uit de abonnementsdossiers opslaan buiten AFAS Profit. Per abonnement maken we een map aan en daar worden alle bijlages die bij dat abonnement gevonden worden opgeslagen.

Stappenplan:

  1. Connectoren aanmaken in AFAS Profit
  2. Connectoren autoriseren in AFAS Profit
  3. Powershell script maken met een aantal functies:
  4. Draaien export

Stap 1 en 2 ga ik nu niet al te veel tijd aan besteden, als je de instructies volgt die hier staan moet je hier wel uit komen, als je geen idee hebt hoe dat werkt kan je het beste mij of een consultant even een berichtje sturen 🙂

Wel heb ik de twee getconnectoren die ik in dit voorbeeld gebruik hier staan zodat je deze kan gebruiken en/of aanpassen. (zip)

Powershell

Zoals aangegeven gaan we het exporteren van de files doen middels Powershell en de AFAS Profit getconnectoren. In dit geval heb ik Powershell 6.2 gebruikt in verband met een Invoke-RestMethod functionaliteit.

Het script is opgebouwd uit een aantal functies die ik vervolgens aanroep in een runner die de daadwerkelijke uitvoering doet. Onderaan staat een linkje naar de hele file, maar ik loop hier even door de scriptblokken heen, ik heb overal de url en autorisatie tokens aangepast:

Functie: ProfitAuthorisatie

function ProfitAuthorisatie {
    # Bouw de autorisatie header voor AFAS Profit
    $token = '<token><version>1</version><data>****</data></token>'
    $encodedToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($token))
    $authValue = "AfasToken $encodedToken"
    $Headers = @{
        Authorization = $authValue
    }
    $Headers
    return
}

Deze functie bouwt de autorisatie header op die we nodig hebben voor het aanroepen van de REST API van AFAS Profit. Deze code wordt een aantal keer aangeroepen in de komende functies. Vergeet niet je eigen Autorisatie token in te voeren.

Functie: GetAbo

function GetAbo {
    param (
        [int32]$administratie
    )
    # Definieer de URL voor de Profit omgeving
    $url = 'https://****/profitrestservices/connectors/DossierBijlagen?filterfieldids=AdministratieAbo%3BAdministratieProject&filtervalues=' + $administratie + '%3B' + $administratie + '&operatortypes=1%3B1&skip=-1&take=200'
    # Connect and retrieve the table
    $auth = ProfitAuthorisatie
    ((Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $auth).content | ConvertFrom-Json).Rows
}

Deze functie haalt een lijst op van alle abonnementen die dossieritems hebben. Ik haal direct ook de verschillende dossieritem ID’s op die ik later nodig heb om de bijlagen op te sporen. De functie heeft een parameter nodig om te kunnen werken, dit is de administratiecode waarvan je de abonnementen op wil halen. Je geeft deze code als parameter mee wanneer je het uiteindelijke script aanroept.

Functie: GetBijlagen

function GetBijlagen {
    param (
        [int32]$DossieritemID
    )
    # Definieer de URL voor de Profit omgeving
    $url_dossier = 'https://****/profitrestservices/connectors/AbonnementenExport?filterfieldids=DossieritemID&filtervalues=' + $DossieritemID + '&operatortypes=1&skip=-1&take=-1'
    # Connect and retrieve the table
    $auth = ProfitAuthorisatie
    ((Invoke-WebRequest -Uri $url_dossier -UseBasicParsing -Headers $auth).content | ConvertFrom-Json).Rows
}

Deze functie heeft het ID van het dossieritem als input parameter nodig. Dit is een van de velden die als resultaat uit de GetAbo functie is gekomen. De reden dat we los de bijlagen op moeten halen is dat de GUID en de filename niet direct in het dossieritem beschikbaar zijn. Daar komt bij dat een enkel dossieritem weer meerdere bijlagen kan hebben. De output hier is een object/array met alle bijlages die beschikbaar zijn bij dit dossieritem.

Functie: DownloadBijlagen

function DownloadBijlagen {
    param (
        [string]$filename,
        [string]$guid,
        [string]$map,
        [string]$abo
    )

    # Definieer de URL voor de Profit omgeving
    $url_download = 'https://****/profitrestservices/fileconnector/' + $guid + '/' + $filename 
    # Connect and retrieve the table
    $auth = ProfitAuthorisatie
    $fileout = $map + '\' + $abo + '-' + $filename 
    $download_error = 0

    # Voorkomen dat we lege files wegschrijven omdat de download niet uitgevoerd kan worden.
    try {
        $tempfile = Invoke-RestMethod -Uri $url_download -Method Get -Headers $auth 
    }
    catch {
        Write-Host $_ -fore Red
        $download_error = 1
    }
    
    if ($download_error -eq 0) {
        $bytes = [System.Convert]::FromBase64String($tempfile.filedata)
        [IO.File]::WriteAllBytes($fileout, $bytes)
        Write-Host $filename "- Succesvol weggeschreven in"  $fileout -ForegroundColor green
    } 
}

Op basis van de items in de lijst die uit GetBijlagen gekomen is kunnen we de files gaan downloaden. Je hebt sowieso de filename en de guid nodig om de fileconnector (subjectconnector) aan te kunnen roepen. Daarnaast geef ik de map mee waar we de file op gaan slaan en het abonnementsnummer (deze neem ik op in de filename, dit is wel een punt, het is niet uitgesloten dat dit dubbele bestanden op kan leveren, in ons geval komt dat niet voor, maar technisch zou je hier bestanden kunnen overschrijven).

In dit stukje code zit ook een error check, heel af en toe kan een file niet goed gedownload worden, dit leverde lege bestanden op. Om dat te voorkomen vang ik die error af (en meld dat in de output) en doe dan dus geen download.

Op dit moment levert dit wel nog lege mappen op als de enkele bijlage bij een abonnement niet goed gedownload kan worden, maar dat zou je simpel op kunnen lossen als dat een probleem is.

Functie: ScriptUitvoeren

function ScriptUitvoeren {
    param (
        [int32]$administratie
    )
    
    # Eerst Getabo
    $records = GetAbo $administratie

    # Per Abo dossieritems ophalen
    foreach ($record in $records) {
        
        # Map bepalen voor het eventueel opslaan van bijlagen
        $map = "C:\Exports\" + $record.Abonnement  

        # Bijlagenlijst per dossieritem maken
        $bijlagenlijst = GetBijlagen $record.DossieritemID

        # Als de map voor het abonnement nog niet bestaat deze aanmaken
        If (!(test-path $map) -And $bijlagenlijst.count -gt 0) {
            New-Item -ItemType Directory -Force -Path $map
        }

        # Per bijlage file downloaden
        foreach ($bijlage in $bijlagenlijst) {
            DownloadBijlagen $bijlage.Naam $bijlage.Bijlage $map $record.Abonnement
        }
    }
}

Dit is de functie die we aanroepen als we het script gaan draaien, je zou deze functie ook direct in de code aan kunnen roepen. Gezien ik het script per administratie wil kunnen draaien heb ik er voor gekozen om hem los aan te roepen. De functie heeft het administratienummer als parameter nodig.

In dit geval is de map waar we de bijlagen opslaan c:\exports\ maar daar kan je van maken wat je wil.

Je ziet in deze funtie dat alle eerdere functies stap voor stap opgeroepen worden en de output daarvan weer als input in een andere functie gestopt wordt. Er worden twee nested foreach loops gebruikt om door alle abonnementen en dossieritems heen te lopen.

Tot slot

Het script is zeker niet perfect, ik zou de lege mappen nog op kunnen ruimen en wat betere logging toe kunnen passen, maar voor ons doel was dit voldoende. Als je eventueel vragen hebt over het script kan je die in de comments stellen.

Het script is hier te downloaden en in te zien. Nadat je de ps1 file gedraaid hebt kan je met “ScriptUitvoeren #” het script starten (vervang # door het administratienummer).

Documenten

Azure function apps in combinatie met AFAS Profit

Als je wil proberen om zo min mogelijk eigen infrastructuur te gebruiken zonder dat dat meteen veel geld moet kosten zijn er talloze opties te bedenken. Een van die opties is Microsoft Azure. De eerste stap die je zal moeten zetten is gaan denken in de daadwerkelijke handelingen die er uitgevoerd moeten worden. Deze handelingen wil je vervolgens in logische blokken opdelen, met als voorwaarde dat deze blokken moeten onafhankelijk van elkaar kunnen functioneren. Vervolgens kan je per blok gaan kijken wat de beste manier is om de gewenste functionaliteit te realiseren.

Het “Probleem”

In dit voorbeeld gaan we voor nieuwe medewerkers in AFAS Profit een aantal velden vullen. Omwille van de eenvoud van het voorbeeld heb ik het gebruikte script even eenvoudig gehouden. Gezien het in Profit niet mogelijk is om logica toe te passen bij het automatisch vullen van velden hebben we een externe oplossing nodig om dit te doen. Het heeft mijn voorkeur om dit soort zaken met Powershell en de connectoren in Profit op te lossen. Voorheen gebruikte ik hiervoor altijd een van onze applicatieservers voor (om het script te draaien). We kunnen dit echter ook prima in Azure oplossen

De oplossing (Microsoft Azure)

Als je in Microsoft Azure scripts wil laten draaien die gebruik kunnen maken van web connectiviteit zijn de “Function Apps” een goede optie. Binnen dit systeem heb je bijvoorbeeld ook Web Proxies beschikbaar en die gaan we in een latere post gebruiken :). We gaan dus een Function App aanmaken waarbinnen we één of meerdere scripts op een tijdschema laten draaien. In dit voorbeeld gaan we een lijst ophalen van medewerkers die nog geen licenties toegekend gekregen hebben. Deze lijst met medewerkers krijgt vervolgens de juiste licenties toegekend op hun medewerkerkaart. Voor deze stappen heb je een actieve Azure Subscription nodig (je moet dus of in de trial zitten of een betaalmethode gekoppeld hebben). De kosten van het draaien van dit script (15 uur per dag) zijn 6 eurocent per maand (afhankelijk van de gebruikte opslag / data). Ik zou Azure altijd instellen op de Engelse taal gezien de vertalingen naar het Nederlands erg verwarrend kunnen zijn. Daarnaast zijn niet alle vertalingen correct.

Stappenplan (functie)

We starten op de Azure portal en kiezen daar voor het aanmaken van een nieuwe resource:
Azure portal "Create Resource"
In het volgende venster zoek je naar “Function app”:
Azure function app aanmaken
Je krijgt een nieuw menu te zien waar je een aantal instellingen in kan geven. De “App name” is voor dit stappenplan niet zo van belang. Mocht je later de Proxy functionaliteit wil gaan gebruiken wordt dit de URL waarop je de proxy kan benaderen. In ons geval moeten we een nieuwe resoursegroup aanmaken waar alle elementen voor deze functionaliteit gebundeld worden. als locatie kiezen we voor West Europe gezien die geografisch het dichtste bij onze AFAS Profit omgeving zal staan. De naam van de storagegroup is niet heel belangrijk in dit geval, als je het maar eenvoudig terug kan vinden. Uiteindelijk heb je dan grofweg het onderstaande. Als je nu op “Create” klikt wordt de Function App aangemaakt, dit kan ongeveer een minuut duren.
Na een minuutje wachten kan je de Function App openen en krijg je een overzicht zoals dit te zien:
Function app overzicht.
We kunnen nu daadwerkelijk de functie aan gaan maken die het script gaat draaien. We klikken hiervoor bij “Funtions” op het plusje om de wizzard te openen. Er zijn een groot aantal standaard functies beschikbaar, maar in ons geval willen we een custom function bouwen:
In het volgende venster zal je de beta talen aan moeten zetten. Powershell is nu nog in beta (al tijden), maar werkt prima. Om deze mogelijkheid te krijgen moet je deze switch even omzetten:
Vervolgens kan je de “Time Trigger” optie kiezen:
Azure function Time Trigger
In het volgende venster zal je de taal moeten kiezen (in ons geval PowerShell) en het schema moeten definiëren. Dit schema maakt gebruik van dezelfde notatie als CRON en is niet heel erg gemakkelijk leesbaar als je er niet veel mee werkt. Je kan hiervoor eventueel een generator gebruiken. In het voorbeeld hier onder draaien we het script ieder uur vanaf 7.00 in de ochtend tot 20.00 in de avond. (let wel op dat je je tijdzone goed ingesteld hebt).
Na het aanmaken van de functie krijg je een leeg venster te zien waar het script geplaatst kan worden. Je kan dit allemaal integreren met bijvoorbeeld Github (of andere repositories), maar dat gaat voor dit voorbeeld even te ver.

Het script

De laatste stap is het toevoegen van het script zelf. Ik ga niet al te veel in op de werking van het script, maar kort samengevat gebeurt er het volgende:
  • Roep een getconnector aan die een gefilterde lijst met gebruikers zonder licenties ophaalt.
  • Loop door deze lijst en voer de gewenste licentiecodes in bij de diverse users.
Om het voorbeeld een beetje overzichtelijk te houden staat hier onder een versimpelde versie van ons script. We gebruiken zelf een complexer script met een aantal randvoorwaarden om te bepalen welke licentie nodig is voor welke gebruiker.
begin {

    # define the access token for the AFAS service
    $token = '<token><version>1</version><data>***</data></token>'
    $encodedToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($token))
    $authValue = "AfasToken $encodedToken"
    $Headers = @{
        Authorization = $authValue
    }

    # Connect and retrieve the table
    $url = 'https://**/profitrestservices/connectors/***?skip=-1&take=-1'
    $records = ((Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers).content  | ConvertFrom-Json).Rows
}

process {
    # Specify update authorization
    $token = '<token><version>1</version><data>***</data></token>'
    $encodedToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($token))
    $authValue = "AfasToken $encodedToken"
    $Headers = @{
        Authorization = $authValue
    }
    $connectorname = "KnEmployee"
    
    # Process records 
    foreach ($record in $records) {
        $url = "https://***/profitrestservices/connectors/KnEmployee"

        Write-Host $record.profituser
        $medewerker = $record.profituser

        $messagecontent = '{"AfasEmployee": {"Element": {"@EmId": "' + $medewerker + '", "Fields": { "***": "02", "***": "01"}}}}'

        $results = ((Invoke-WebRequest -Uri $url -UseBasicParsing -ContentType 'application/json;charset=utf-8' -Method PUT -Headers $Headers -Body $messagecontent).content | ConvertFrom-Json)
        Write-Host $results
    }
}
Na het invoeren van het script kan je op “save en run” klikken en daarna zal het script volgens het voorgestelde schema draaien. Je kan eventuele fouten opsporen door de logging na te kijken, deze is voor de losse run direct onder in het scherm beschikbaar:
En later via het monitor menu:

Tot slot

Dit is natuurlijk maar een eenvoudig voorbeeld, maar je kan je voorstellen dat je op deze manier veel zaken kan automatiseren zonder dat je hier eigen infrastructuur voor nodig hebt. Het script wat ik gebruikt heb in dit voorbeeld is gemaakt samen met Jaap de Koning, lees ook eens zijn blog als je geïnteresseerd bent in automatisering. Laat even een berichtje achter als je meer wil weten over deze manier van automatiseren rondom AFAS Profit of het gebruik van Azure functions. De komende tijd zal ik meer in gaan op deze aanpak. Daarbij kijken we naar de veranderingen die nodig zijn in de manier van denken om goed gebruik te maken van cloud en hybride oplossingen.

AFAS Profit: Analyses mailen

Op dit moment spendeer ik 90% van mijn werkuren aan onze (Detron) AFAS Profit inrichting. De afgelopen jaren hebben we delen van het bedrijf gesplitst, andere delen samengevoegd, en bovenal de werkwijze zoveel mogelijk geharmoniseerd. Nu die zaken langzaamaan minder van onze tijd kosten hebben we meer tijd om aan de kleinere projectjes te kunnen werken.

Zo was er al een tijdje de wens om analyses uit AFAS Profit te kunnen mailen naar een of meerdere gebruikers. Dat was voor mij een mooie gelegenheid om eens te kijken of ik behalve met AutoIT dit soort “problemen” ook op kan lossen met het native aanwezige Powershell (Windows servers). Gezien ik daar helemaal zo goed als geen ervaring mee heb was het vooral een aardig uitzoekwerk, maar op een kleine stap na is het me uiteindelijk gelukt om zo goed als het hele proces vanuit Powershell te realiseren.

Het alternatief zou overigens zijn om analyses te publiceren op InSite, maar gezien deze analyses voor een hele kleine groep gebruikers bedoeld zijn en ook maar een zeer beperkte houdbaarheid hebben is dit geen goede optie. Daarbij kan het ook zo zijn dat een deel van deze analyses als “snapshots” bewaard worden. Ook dat zou niet nodig moeten zijn, maar soms is het niet anders.

Het systeem bestaat uit een aantal delen:

  • Een analyse in AFAS Profit bepaald wie welke analyse wanneer gemaild moet krijgen. Deze lijst wordt bijgehouden door het functioneel beheer team.
  • Deel 1 in Powershell: Publiceren en interpreteren bovenstaande analyse. Deze analyse wordt opgeslagen als CSV en omvat alleen het overzichttabblad, min of meer een to-do lijst dus.
  • Deel 2 in Powershell: Check of er regels in de eerder genoemde analyse binnen het huidige tijdvak vallen, regels die niet aan die voorwaarden voldoen worden verder genegeerd

Hier had ik wel een kleine uitdaging, op zich is het bepalen of een regel in het tijdvak valt nog niet zo lastig:

ControleTijdvak

Alleen die laatste regel bracht wel een probleem met zich mee, hoe bepaald je nou of de huidige dag de laatste dag van de maand is? Het zoekwerk voor oplossingen bracht ook eigenlijk alleen maar opties met zich mee die veel complexer waren dan dat ik nodig had.

Uiteindelijk heb ik dat als volgt opgelost (de naam van de variabelen verraad het wel een beetje):

ControleLaatsteDag

Ik gebruik hier twee variabelen, één met de huidge maand, en de andere welke de maand van één dag later bevat. Als die niet gelijk zijn is vandaag dus de laatste dag van de maand. Overigens had ik deze vergelijking ook wel in één regel kunnen stoppen, maar dit vond ik voor nu zelf even overzichtelijker.

We gaan weer verder..

  • Deel 3 in Powershell / AFAS Profit: Regels die in het huidige tijdvak worden opgepakt, er wordt een batch file (jammer..) aangeroepen die via de commandline de desbetreffende analyse laat publiceren. Dit publiceren gebeurt overigens met een macro van 4 regels die toegevoegd moet worden aan de analyse. De macro:
Sub PublishAnalysis()
Dim fileName As String
ActiveWorkbook.SaveAs fileName:="C:Powershelltestanalyse.xlsx", FileFormat:=xlOpenXMLWorkbook
End Sub

De reden dat de macro PublishAnalysis heet heeft te maken met het feit dat ik het publiceren laat doen door AFAS Profit via de commandline, de publicatie wordt getriggerd via een batchfile en uitgevoerd door deze macro.

  • Deel 4 in Powershell: Na het genereren van de analyse (als .xlsx) wordt deze verstuurd naar het mailadres dat op de regel in de eerste analyse is ingevoerd.
  • Deel 5 in Powershell: Logging wordt bijgewerkt, als de log meer dan 2Mb groot is wordt deze opzij gezet, als er al oude logging aanwezig is wordt deze overschreven.

Op dit moment is het mogelijk om analyses te verzenden op een specifieke dag van de week, een specifieke dag van de maand, iedere dag, de laatste dag van de maand of tot slot de eerste dag van de maand. Dit lijkt me nu voldoende, maar gebruikers kennende is er vast nog een optie die ik nu nog niet bedacht heb ;).

De oplossing die ik nu geschreven heb zal qua efficientie nog wel het nodige te wensen over laten, maar het werk naar behoren. Ik heb nog wel een aantal wensen die ik in de komende tijd wil doorvoeren om vooral de kans op fouten bij de invoer te verkleinen. Daarnaast moet ik nog een filter maken die voorkomt dat we files proberen te mailen die groter zijn dan onze Exchange omgeving toelaat.

Mocht iemand interesse hebben in het script zie ik wel een berichtje verschijnen, als iemand nettere of andere oplossingen heeft hoor ik het natuurlijk ook graag . Verder is het voor mij zelf wel een mooie eerste echte ervaring met Powershell. Gezien dit een native systeem is zie ik wel wat voordelen die het waard maken om te gaan kijken of ik (een deel van ) de andere Profit koppelingen om kan zetten naar Powershell scripts. Deze worden nu over het algemeen ondersteund door AutoIT “applicaties” die ik in de afgelopen jaren gemaakt heb.