createSupportReport.ps1
<#
.SYNOPSIS Creates invoices based on timecockpit bookings on support projects .DESCRIPTION Workpackages are created as invoice position, which are closed, billable and not billed. The workpackage have no bookings later than the given date (to avoid having closed booking from the next month already in the invoice). .PARAMETER tcCustomerNames Customer to create output for. If no customer is passed, then all customers are returned. .PARAMETER from Bookings before this date will not be considered. If not given (which is the default behaviour) all bookings into the past are considered as long as they are not marked as billed. .PARAMETER to Bookings after this date will not be considered. Defaults to last day of previous month .PARAMETER includeBilled If also Bookings which are already billed should be included. Defaults to $False .PARAMETER tcDBCredential Credential for Timecockpit DB. If not given they will be requested and stored, if used, in CredentialManager. .PARAMETER useCredentialsManager If credentialmanager should be used. Default is $True. .EXAMPLE Creates invoices up to january .\createSupportInvoices 2019-01-31 #> [CmdletBinding(SupportsShouldProcess=$true)] param( [string[]] $tcCustomerNames, [DateTime] $from, [DateTime] $to, [Boolean] $includeBilled = $False, [PSCredential] $tcDBCredential, [Boolean] $useCredentialsManager = $True ) # check prerequisites $tcModule = Get-Module -ListAvailable -Name "TimeCockpit"; if (!$tcModule) { throw "Module 'TimeCockpit' needed. Please install executing 'Install-Module -Name TimeCockpit' as Administrator."; } Write-Information "Using TimeCockpit Module v$($tcModule.Version)." $fdModule = Get-Module -ListAvailable -Name "Freshdesk"; if (!$fdModule) { throw "Module 'Freshdesk' needed. Please install executing 'Install-Module -Name Freshdesk' as Administrator."; } if(!$from) { $from = [datetime]::MinValue } if(!$to) { $now = Get-Date $to = Get-Date -Year $now.Year -Month $now.Month -Day 1 -Hour 0 -Minute 0 -Second 0 $to = $to.AddSeconds(-1) } else { $to = Get-Date -Year $to.Year -Month $to.Month -Day $to.Day -Hour 23 -Minute 59 -Second 59 } if($tcCustomerNames) { $tcCustomerNamesResolved = $tcCustomerNames | foreach { "'" + $_ + "'" } $tcCustomerNamesQuery = "and Customer.APP_Code in ($($tcCustomerNamesResolved -join ','))" } if($includeBilled) { $tcIncludeBilledQuery = "" } else { $tcIncludeBilledQuery = "and Timesheet.APP_HourlyRateBilled is null" } if($from -eq [datetime]::MinValue) { Write-Information "Will create Report for not billed timerecords older than $($to.ToString('yyyy-MM-dd'))..."; } else { Write-Information "Will create Report for not billed timerecords ($($from.ToString('yyyy-MM-dd')) - $($to.ToString('yyyy-MM-dd')))..."; } $timesheetQuery = @" select Customer.APP_Code as 'CustomerCode', Project.APP_Code as 'ProjectCode', Task.APP_TaskUuid as 'TaskUuid', Task.APP_Code as 'TaskCode', Task.APP_Description as 'TaskDescription', Timesheet.APP_Description as 'Description', Timesheet.APP_BeginTime as 'BeginTime', Timesheet.APP_EndTime as 'EndTime', UserDetail.APP_Username as 'User', Task.APP_Closed as 'TaskClosed', case when Task.APP_NoBilling = 1 then cast(1 as bit) else Timesheet.APP_NoBilling end as 'TimesheetNoBilling', Timesheet.APP_HourlyRateBilled as 'TimesheetBilled', isnull(cast(DATEDIFF(minute, Timesheet.APP_BeginTime, Timesheet.APP_EndTime) as float), 0) as 'Duration', case when Task.APP_NoBilling = 1 or Timesheet.APP_NoBilling = 1 then 0 else isnull(cast(DATEDIFF(minute, Timesheet.APP_BeginTime, Timesheet.APP_EndTime) as float), 0) end as 'DurationBilled', case when Task.APP_NoBilling = 1 or Timesheet.APP_NoBilling = 1 then isnull(cast(DATEDIFF(minute, Timesheet.APP_BeginTime, Timesheet.APP_EndTime) as float), 0) else 0 end as 'DurationUnBilled', case when Timesheet.APP_HourlyRate is null and Task.APP_HourlyRate is null and Project.APP_HourlyRate is null then Customer.APP_HourlyRate when Timesheet.APP_HourlyRate is null and Task.APP_HourlyRate is null then Project.APP_HourlyRate when Timesheet.APP_HourlyRate is null then Task.APP_HourlyRate else Timesheet.APP_HourlyRate end as 'HourlyRate', Timesheet.USR_CostRate as 'TimesheetCostrate' from edhrqnfxua.APP_Timesheet as Timesheet join edhrqnfxua.APP_Project as Project on Project.APP_ProjectUuid = Timesheet.Relation_APP_Project join edhrqnfxua.APP_Customer as Customer on Customer.APP_CustomerUuid = Project.Relation_APP_Customer join edhrqnfxua.APP_Task as Task on Task.APP_TaskUuid = Timesheet.Relation_APP_Task join edhrqnfxua.APP_UserDetail as UserDetail on UserDetail.APP_UserDetailUuid = Timesheet.Relation_APP_UserDetail where Project.APP_Code like 'Support%' and Customer.APP_Code not in ('GuidNew') ${tcCustomerNamesQuery} and Project.APP_Billable = 1 and Task.APP_NoBilling = 0 and Timesheet.APP_NoBilling = 0 ${tcIncludeBilledQuery} and Timesheet.APP_BeginTime >= '$($from.ToString('yyyy-MM-dd'))' and Timesheet.APP_EndTime <= '$($to.ToString('yyyy-MM-dd HH:mm'))' "@ $query = @" with ResolvedTimesheet as ( ${timesheetQuery} ) select CustomerCode, ProjectCode, TaskCode, TaskDescription, TaskClosed, min(BeginTime) BeginTime, max(EndTime) EndTime, sum(Duration) / 60 Duration, sum(DurationBilled) / 60 as DurationBilled, sum(DurationUnbilled) / 60 as DurationUnbilled, sum((DurationBilled / 60.0) * HourlyRate) as AmountBilled, sum(TimesheetCostrate) TimesheetCostrate, max(HourlyRate) HourlyRate from ResolvedTimesheet TS group by CustomerCode, ProjectCode, TaskUuid, TaskCode, TaskDescription, TaskClosed order by CustomerCode, TaskClosed desc, max(EndTime) "@ Connect-TCDB -Credential $tcDBCredential -UseCredentialsManager $useCredentialsManager; Write-Information "Fetching Tasks from DB..."; $tasks = (Invoke-TCDBSqlCmd -Query $query) Write-Information "Fetched $($tasks.Count) records."; Write-Information "Fetching Bookings from DB..."; $bookings = (Invoke-TCDBSqlCmd -Query $timesheetQuery) Write-Information "Fetched $($bookings.Count) records."; $resultRaw = @(); foreach($item in $tasks) { $resultRow = @{} $resultRow.Add("customerCode", $item.CustomerCode) $resultRow.Add("taskCode", $item.TaskCode) $resultRow.Add("taskDescription", $item.TaskDescription) $resultRow.Add("beginDate", $item.BeginTime.ToString('yyyy-MM-dd')) $resultRow.Add("endDate", $item.EndTime.ToString('yyyy-MM-dd')) $resultRow.Add("isClosed", $item.TaskClosed) $resultRow.Add("durationBilled", $item.DurationBilled) $resultRow.Add("durationUnbilled", $item.DurationUnbilled) $resultRow.Add("hourlyRate", $item.HourlyRate) $resultRow.Add("amountBilled", $item.AmountBilled) if($item.TaskCode -match '^\d+$' -and [int]$item.TaskCode -lt 5000) { try { $ticket = Get-FDTicket $item.TaskCode } catch { throw "Failed to get Ticket having Id '$($item.TaskCode)'" } $resultRow.Add("type", "Ticket") $resultRow.Add("subType", $ticket.type) switch($ticket.priority) { 1 { $category = "Inquiry" } 2 { $category = "Minor" } 3 { $category = "Major" } 4 { $category = "Blocker" } } $resultRow.Add("categoryNumber", $ticket.priority) $resultRow.Add("categoryName", $category) $resultRow.Add("firstResponseSLAViolation", $ticket.fr_escalated) $resultRow.Add("resolutionSLAViolation", $ticket.is_escalated) } else { $resultRow.Add("type", "Other") } # append bookings $taskBookings = $bookings | Where-Object { $_.TaskCode -eq $item.TaskCode } $resultTasks = @() foreach($taskBooking in $taskBookings) { $taskRow = [PSCustomObject]@{ beginTime = $taskBooking.BeginTime.ToString('yyyy-MM-dd HH:mm') endTime = $taskBooking.EndTime.ToString('yyyy-MM-dd HH:mm') description = $taskBooking.Description user = $taskBooking.User } $resultTasks += $taskRow } $resultRow.Add("bookings", $resultTasks) $resultRaw += $resultRow; } $resultRaw | ConvertTo-Json -Depth 16 #$resultRaw | ConvertTo-Json | ConvertFrom-Json #$new = $raw.values | Where-Object { $_.type -eq "Ticket" } | Group-Object -Property subType #$new[2].Group | Meassure-Object -Property durationBilled -Sum |