Add project files.

This commit is contained in:
2026-02-21 10:30:14 -07:00
parent 398161da53
commit f662d576d1
28 changed files with 1127 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
public class Correspondent : ResultBase
{
public string slug { get; set; }
public string name { get; set; }
public int document_count { get; set; }
public override string ToString()
{
return name;
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
public enum CustomField_DataType
{
select,
date,
@string
}
public class CustomField_ExtraData
{
public CustomField_Select_Option[] select_options { get; set; }
public string? default_currency { get; set; }
}
public class CustomField_Select_Option
{
public string id { get; set; }
public string label { get; set; }
public override string ToString()
{
return $"{label} ({id})";
}
}
public class CustomField : ResultBase
{
public string name { get; set; }
public CustomField_DataType data_type { get; set; }
public int? document_count { get; set; }
public CustomField_ExtraData? extra_data { get; set; }
public override string ToString()
{
return $"{name} ({data_type})";
}
}
}

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
public class CustomFieldKeyValuePair
{
public int field { get; set; }
public string value { get; set; }
public override string ToString()
{
return $"{field}: {value}";
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
public class CustomFieldParsed
{
public string Name { get; set; }
public object? Value { get; set; }
public Type DataType { get; set; }
public string StringValue => (string)Value;
public DateTime DateValue => Value != null && DataType == typeof(DateTime) ? (DateTime)Value : DateTime.MinValue;
public static CustomFieldParsed FromCustomFieldKeyValuePair(CustomFieldKeyValuePair pair, IEnumerable<CustomField> customFields)
{
if (pair.value != null && customFields.FirstOrDefault(cf => cf.Id == pair.field) is CustomField field)
{
try
{
switch (field.data_type)
{
case CustomField_DataType.date:
return new CustomFieldParsed
{
Name = field.name,
Value = DateTime.Parse(pair.value),
DataType = typeof(DateTime)
};
break;
case CustomField_DataType.select:
return new CustomFieldParsed
{
Name = field.name,
Value = field.extra_data.select_options.First(so => so.id == pair.value).label,
DataType = typeof(string)
};
break;
default:
return new CustomFieldParsed
{
Name = field.name,
Value = pair.value,
DataType = typeof(string)
};
break;
}
}
catch (Exception ex)
{
;
}
}
return null;
}
public override string ToString()
{
return $"{Name}: {Value} ({DataType.Name})";
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
public class Document : ResultBase
{
public int? correspondent { get; set; }
public int? document_type { get; set; }
public int? storage_path { get; set; }
public string title { get; set; }
public string content { get; set; }
public int[] tags { get; set; }
public DateTime created { get; set; }
public DateTime created_date { get; set; }
public DateTimeOffset modified { get; set; }
public DateTimeOffset added { get; set; }
public DateTimeOffset? deleted_at { get; set; }
public string? archive_serial_number { get; set; }
public string original_file_name { get; set; }
public string archived_file_name { get; set; }
public int owner { get; set; }
public string[] notes { get; set; }
public CustomFieldKeyValuePair[] custom_fields { get; set; }
public int? page_count { get; set; }
public string mime_type { get; set; }
}
}

View File

@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
public class DocumentParsed
{
#region static arrays
public static Tag[] _tags { get; set; } = new Tag[0];
public static Correspondent[] _correspondents { get; set; } = new Correspondent[0];
public static CustomField[] _customFields { get; set; } = new CustomField[0];
public static DocumentType[] _documentTypes { get; set; } = new DocumentType[0];
#endregion
public string Correspondent { get; protected set; }
public string DocumentType { get; protected set; } = "Unsorted";
public string Title { get; protected set; }
public string Content { get; protected set; }
public List<Tag> Tags { get; protected set; } = new List<Tag>();
public DateTime Created { get; protected set; }
public DateTimeOffset Modified { get; protected set; }
public DateTimeOffset Added { get; protected set; }
public DateTimeOffset? DeletedAt { get; protected set; }
public List<string> Notes{ get; protected set; }=new List<string>();
public string MIMEType { get; protected set; }
public int? PageCount { get; protected set; }
public List<CustomFieldParsed> CustomFields { get; protected set; } = new List<CustomFieldParsed>();
public override string ToString()
{
return $"{Correspondent} {DocumentType}";
}
public static DocumentParsed FromDocument(Document doc)
{
DocumentParsed d = new DocumentParsed();
d.Correspondent = _correspondents.First(c => c.Id == doc.correspondent).name;
d.DocumentType = _documentTypes.FirstOrDefault(dt => dt.Id == doc.document_type)?.name ?? "Unsorted";
d.Title=doc.title.Trim();
d.Content = doc.content.Trim();
_tags.Where(_t=>doc.tags.Any(dt=>_t.Id == dt)).ToList().ForEach(t => d.Tags.Add(t));
d.Created=doc.created;
d.Modified=doc.modified;
d.Added=doc.added;
d.DeletedAt = doc.deleted_at;
d.Notes = doc.notes.ToList(); ;
d.MIMEType = doc.mime_type;
d.PageCount = doc.page_count;
_customFields.Where(_cf => doc.custom_fields.Any(dcf => _cf.Id == dcf.field)).ToList()
.ForEach(cf =>
{
var cfp = CustomFieldParsed.FromCustomFieldKeyValuePair(
doc.custom_fields.First(dcf => dcf.field == cf.Id),
_customFields);
if (!(cfp is null))
{
d.CustomFields.Add(cfp);
}
});
return d;
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
public class DocumentType : ResultBase
{
public string slug { get; set; }
public string name { get; set; }
public int? document_count { get; set; }
public override string ToString()
{
return name;
}
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
public class ResultBase
{
public int Id { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
internal class RootObject<T> where T : ResultBase
{
public int Count { get; set; }
public List<T> Results { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export.Models
{
public class Tag : ResultBase
{
public string slug { get; set; }
public string name { get; set; }
public string color { get; set; }
public string text_color { get; set; }
public override string ToString()
{
return $"{name} ({Id})";
}
}
}

310
paperless-ngx-export/api.cs Normal file
View File

@@ -0,0 +1,310 @@
using Newtonsoft.Json;
using paperless_ngx_export.Models;
using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Nodes;
namespace paperless_ngx_export
{
public static class api
{
public static bool isInitialized { get; private set; } = false;
static string paperless_api_base;
static string token;
public static void init(string token, string paperless_api_base)
{
api.paperless_api_base = paperless_api_base.Trim();
setToken(token.Trim());
isInitialized = !string.IsNullOrWhiteSpace(api.paperless_api_base) && !string.IsNullOrEmpty(api.token);
}
private static void setToken(string token)
{
api.token = token.Trim().Trim('/');
}
private static HttpRequestMessage getHttpRequestMessage(string query, int page = -1)
{
if (page > 0)
{
var pagedQuery = query.Contains('?') ? $"{query}&page={page}" : $"{query}?page={page}";
return getHttpRequestMessage(pagedQuery, HttpMethod.Get);
}
else
return getHttpRequestMessage(query, HttpMethod.Get);
}
private static HttpRequestMessage getHttpRequestMessage(string query, HttpMethod httpMethod)
{
HttpRequestMessage message = new HttpRequestMessage(httpMethod, $"{api.paperless_api_base}{query}");
//message.Headers.Authorization = getAuthenticationHeaderValue();
message.Headers.Add("Authorization", $"Token {api.token}");
return message;
}
private static AuthenticationHeaderValue getAuthenticationHeaderValue()
{
var header = new AuthenticationHeaderValue("Token", api.token);
return header;
}
public static async Task<IEnumerable<Tag>> getTagsAsync()
{
var query = "tags/";
var tags = new List<Tag>();
if (!isInitialized) { throw new NotSupportedException("API base URL or token not initialized"); }
using (HttpClient client = new HttpClient())
{
var count = -1;
var page = 1;
while (count != 0)
{
count = 0;
var response = await client.SendAsync(getHttpRequestMessage(query, page));
if (response != null && response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync();
var root = JsonConvert.DeserializeObject<RootObject<Tag>>(responseString);
if (root.Results.Any())
{
count = root.Results.Count();
tags.AddRange(root.Results);
}
}
page++;
}
}
return tags;
}
public static async Task<IEnumerable<Document>> getCurrentDocumentsAsync(bool includeNonExpirableDocuments = true)
{
var query = "documents/";
var excludedTagIds = (await getTagsAsync()).Where(x => (includeNonExpirableDocuments && x.name == "No Expiration") || x.name == "Action:Renewed" || x.name == "HR:Inactive").Select(x => x.Id).ToList();
excludedTagIds.ForEach(id => query += query.Contains('?') ? $"&tags__id__none={id}" : $"?tags__id__none={id}");
var documents = new List<Document>();
if (!isInitialized) { throw new NotSupportedException("API base URL or token not initialized"); }
using (HttpClient client = new HttpClient())
{
var count = -1;
var page = 1;
while (count != 0)
{
count = 0;
var response = await client.SendAsync(getHttpRequestMessage(query, page));
if (response != null && response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync();
var root = JsonConvert.DeserializeObject<RootObject<Document>>(responseString);
if (root.Results.Any())
{
count = root.Results.Count();
foreach (var document in root.Results)
{
bool isValid = true;
foreach (var t in document.tags)
if (excludedTagIds.Contains(t))
isValid = false;
if(isValid)
documents.Add(document);
}
}
}
page++;
}
}
return documents.OrderBy(d=>d.correspondent).ToList();
}
public static async Task<IEnumerable<Document>> getAllDocumentsAsync()
{
var query = "documents/";
var documents = new List<Document>();
if (!isInitialized) { throw new NotSupportedException("API base URL or token not initialized"); }
using (HttpClient client = new HttpClient())
{
var count = -1;
var page = 1;
while (count != 0)
{
count = 0;
var response = await client.SendAsync(getHttpRequestMessage(query, page));
if (response != null && response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync();
var root = JsonConvert.DeserializeObject<RootObject<Document>>(responseString);
if (root.Results.Any())
{
count = root.Results.Count();
documents.AddRange(root.Results);
}
}
page++;
}
}
return documents;
}
public static async Task<IEnumerable<Correspondent>> getCorrespondentsAsync()
{
var query = "correspondents/";
if (!isInitialized) { throw new NotSupportedException("API base URL or token not initialized"); }
var correspondents = new List<Correspondent>();
using (HttpClient client = new HttpClient())
{
var count = -1;
var page = 1;
while (count != 0)
{
count = 0;
var response = await client.SendAsync(getHttpRequestMessage(query, page));
if (response != null && response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync();
var root = JsonConvert.DeserializeObject<RootObject<Correspondent>>(responseString);
if (root.Results.Any())
{
count = root.Results.Count();
correspondents.AddRange(root.Results);
}
}
page++;
}
}
return correspondents;
}
public static async Task<IEnumerable<CustomField>> getCustomFieldsAsync()
{
var query = "custom_fields/";
var fields = new List<CustomField>();
if (!isInitialized) { throw new NotSupportedException("API base URL or token not initialized"); }
using (HttpClient client = new HttpClient())
{
var count = -1;
var page = 1;
while (count != 0)
{
count = 0;
var response = await client.SendAsync(getHttpRequestMessage(query, page));
if (response != null && response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync();
var root = JsonConvert.DeserializeObject<RootObject<CustomField>>(responseString);
if (root.Results.Any())
{
count = root.Results.Count();
fields.AddRange(root.Results);
}
}
page++;
}
}
return fields;
}
public static async Task<IEnumerable<DocumentType>> getDocumentTypesAsync()
{
var query = "document_types/";
var doctypes = new List<DocumentType>();
if (!isInitialized) { throw new NotSupportedException("API base URL or token not initialized"); }
using (HttpClient client = new HttpClient())
{
var count = -1;
var page = 1;
while (count != 0)
{
count = 0;
var response = await client.SendAsync(getHttpRequestMessage(query, page));
if (response != null && response.IsSuccessStatusCode)
{
var responseString = await response.Content.ReadAsStringAsync();
var root = JsonConvert.DeserializeObject<RootObject<DocumentType>>(responseString);
if (root.Results.Any())
{
count = root.Results.Count();
doctypes.AddRange(root.Results);
}
}
page++;
}
}
return doctypes;
}
public static async Task<string> getExpirationDatesCalDAVAsync(bool onlyCurrentDocuments = true)
{
string caldav = @"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CAMCASS//Expiring Documents Calendar//EN
X-WR-CALNAME:Expiring Documents
X-WR-CALDESC:Onboarded staff's document expiration dates
X-PUBLISHED-TTL:PT12H";
if (!isInitialized) { throw new NotSupportedException("API base URL or token not initialized"); }
var documents = (await getMergedDocumentMetadataAsync(onlyCurrentDocuments, false)).OrderBy(x=>x.Correspondent).ToList();
documents.Where(d => d.CustomFields.Any(f => f.Name.StartsWith("Expiration", StringComparison.OrdinalIgnoreCase)))
.ToList()
.ForEach(doc =>
{
var calDAVEvent = getCalDAVEvent(doc, doc.CustomFields.First(f => f.Name.StartsWith("expir", StringComparison.OrdinalIgnoreCase)).DateValue);
caldav += @$"
{calDAVEvent}";
});
caldav += @"
END:VCALENDAR";
return caldav;
}
private static string getCalDAVEvent(DocumentParsed document, DateTime date)
{
var nextDay = date.AddDays(1);
var DTSTAMP = $"{DateTime.UtcNow.Year:0000}{DateTime.UtcNow.Month:00}{DateTime.UtcNow.Day:00}T{DateTime.UtcNow.Hour:00}{DateTime.UtcNow.Minute:00}00Z";
return $@"BEGIN:VEVENT
UID:{Guid.NewGuid().ToString().Replace("-","")}
DTSTAMP:{DTSTAMP}
DTSTART;VALUE=DATE:{date.Year:0000}{date.Month:00}{date.Day:00}
DTEND;VALUE=DATE:{nextDay.Year:0000}{nextDay.Month:00}{nextDay.Day:00}
SUMMARY:{document.DocumentType} - {document.Correspondent}
END:VEVENT";
}
public static async Task<IEnumerable<DocumentParsed>> getMergedDocumentMetadataAsync(bool onlyCurrentDocuments = true, bool includeNonExpirableDocuments=true)
{
if (!isInitialized) { throw new NotSupportedException("API base URL or token not initialized"); }
var docs = onlyCurrentDocuments ? await getCurrentDocumentsAsync(includeNonExpirableDocuments) : await getAllDocumentsAsync();
var tags = await getTagsAsync();
var correspondents = await getCorrespondentsAsync();
var customFields = await getCustomFieldsAsync();
var documenttypes = await getDocumentTypesAsync();
DocumentParsed._tags = tags.ToArray();
DocumentParsed._correspondents = correspondents.ToArray();
DocumentParsed._customFields = customFields.ToArray();
DocumentParsed._documentTypes = documenttypes.ToArray();
var documents = new List<DocumentParsed>();
docs.ToList().ForEach(document => documents.Add(DocumentParsed.FromDocument(document)));
return documents;
}
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>paperless_ngx_export</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,212 @@
using ClosedXML.Excel;
using DocumentFormat.OpenXml.Drawing.Charts;
using DocumentFormat.OpenXml.Spreadsheet;
using paperless_ngx_export.Models;
using RBush;
using System;
using System.Collections.Generic;
using System.Text;
namespace paperless_ngx_export
{
public static class spreadsheet
{
public static async Task<byte[]> GetSummarySpreadsheetAsByteArrayAsync()
{
byte[] bytes = null;
using (var stream = new MemoryStream())
{
var workbook = await GetSummaryAsXLWorkbookAsync();
workbook.SaveAs(stream);
bytes = stream.ToArray();
stream.Close();
}
return bytes ?? new byte[0];
}
public static void PopulateDocumentTypeWorksheets(
IEnumerable<DocumentType> documentTypes,
IEnumerable<Tag> tags,
IEnumerable<CustomField> customFields,
IEnumerable<DocumentParsed> docs,
XLWorkbook workbook,
bool onlyCurrentDocuments = true)
{
var headings = new string[]
{
"Correspondent",
"Role",
"Specialty",
"Document Type",
"Expiration Date",
"Tags"
};
var documents = docs.Where(x =>
{
if (!x.CustomFields.Any(xx => xx.Name.Contains("expir", StringComparison.OrdinalIgnoreCase)))
return true;
return onlyCurrentDocuments ? !x.Tags.Any(xx => xx.name.Contains("renewed", StringComparison.OrdinalIgnoreCase)) : true;
}).ToList();
foreach (var documentType in documentTypes.OrderBy(x=>!documents.Any(d=>d.DocumentType == x.name && d.CustomFields.Any(cf=>cf.Name.Contains("expir", StringComparison.OrdinalIgnoreCase)))).
ThenBy(x=>x.name))
{
var sheetName = documentType.name.Replace('/', '-');
var sheet = workbook.Worksheets.FirstOrDefault(x => x.Name == sheetName);
if (sheet == null)
{
sheet = workbook.Worksheets.Add(sheetName);
sheet.TabColor = XLColor.Green;
for (int i = 0; i < headings.Length; i++)
{
sheet.Cell(1, i + 1).Value = headings[i];
}
sheet.SheetView.Freeze(1, 2);
sheet.SetAutoFilter(true);
sheet.Cells("A1:ZZ1").Style.Font.Bold = true;
}
foreach (var document in documents.Where(x => x.DocumentType == documentType.name).OrderBy(x => x.Correspondent))
{
var rowColour = XLColor.Transparent;
var expirationDate = document.CustomFields.FirstOrDefault(x => x.Name.Contains("expir", StringComparison.OrdinalIgnoreCase))?.DateValue;
if (expirationDate != null && expirationDate <= DateTime.Today)
{
rowColour = XLColor.Red;
}
else if (expirationDate != null && expirationDate?.AddDays(-30) < DateTime.Today)
{
rowColour = XLColor.Orange;
}
if(sheet.TabColor == XLColor.Green
&& rowColour != XLColor.Transparent)
{
sheet.TabColor = sheet.TabColor == XLColor.Red ? XLColor.Red : rowColour;
}
var rowIndex = sheet.LastRowUsed()?.RowNumber() + 1 ?? 2;
sheet.Cell(rowIndex, 1).Value = document.Correspondent;
sheet.Cell(rowIndex, 2).Value = document.CustomFields.FirstOrDefault(x=>x.Name == "Role")?.StringValue;
sheet.Cell(rowIndex, 3).Value = document.CustomFields.FirstOrDefault(x => x.Name == "Specialty")?.StringValue;
sheet.Cell(rowIndex, 4).Value = document.DocumentType;
sheet.Cell(rowIndex, 5).Value = expirationDate == null ? Blank.Value : expirationDate;
sheet.Cell(rowIndex, 5).Style.Fill.BackgroundColor = rowColour == XLColor.Transparent ? sheet.Cell(rowIndex, 5).Style.Fill.BackgroundColor : rowColour;
sheet.Cell(rowIndex, 6).Value = String.Concat(document.Tags.Select(x=>x.name + ' ').ToArray()).Trim();
}
sheet.Columns().AdjustToContents();
}
}
public static void PopulateSummaryWorksheet(
IEnumerable<DocumentType> documentTypes,
IEnumerable<Tag> tags,
IEnumerable<CustomField> customFields,
IEnumerable<DocumentParsed> docs,
XLWorkbook workbook,
bool onlyCurrentDocuments = true)
{
var headers = new string[]
{
"Correspondent",
"Role (Specialty)",
"Professional Registration",
"Liability Insurance",
"ACLS",
"BLS",
"PALS",
"Vaccination Record",
"CV / Resumé",
"Misc Certificate",
"Credentialing / Privileges",
"Reference Letter"
};
var documents = docs.Where(x =>
{
if (!x.CustomFields.Any(xx => xx.Name.Contains("expir", StringComparison.OrdinalIgnoreCase)))
return true;
return onlyCurrentDocuments ? !x.Tags.Any(xx => xx.name.Contains("renewed", StringComparison.OrdinalIgnoreCase)) : true;
}).ToList();
var summarySheet = workbook.Worksheets.Add("Summary");
summarySheet.SheetView.Freeze(1, 2); //Freeze header row and, correspondent and role columns
for (int i = 1; i <= headers.Length; i++)
{
summarySheet.Cell(1, i).Value = headers[i - 1];
}
summarySheet.Cells("A1:ZZ1").Style.Font.Bold = true;
summarySheet.SetAutoFilter();
var row = 2;
foreach (var correspondent in documents.Select(x => x.Correspondent).Distinct())
{
summarySheet.Cell(row, 1).Value = correspondent;
foreach (var doc in documents.Where(x => x.Correspondent == correspondent))
{
if (summarySheet.Cell(row, 2).Value.Type == XLDataType.Blank)
{
var role = doc.CustomFields.FirstOrDefault(x => x.Name == "Role")?.Value?.ToString() ?? "";
var specialty = doc.CustomFields.FirstOrDefault(x => x.Name == "Specialty")?.Value?.ToString() ?? "";
role = $"{role} {(!string.IsNullOrWhiteSpace(specialty) ? $"({specialty})" : "")}".Trim();
summarySheet.Cell(row, 2).Value = role;
}
if (headers.IndexOf(doc.DocumentType.ToString()) + 1 is int column && column > 0)
{
XLCellValue cellValue = summarySheet.Cell(row, column).Value;
if (!cellValue.IsBlank)
{
continue;
}
if (doc.DocumentType == "Reference Letter")
{
cellValue = documents.Where(x => x.Correspondent == correspondent && x.DocumentType == "Reference Letter").Count();
;
}
else if (doc.CustomFields.FirstOrDefault(x => x.Name == "Expiration Date") is CustomFieldParsed customFieldParsed &&
customFieldParsed.DateValue > DateTime.MinValue && customFieldParsed.DateValue < DateTime.MaxValue)
{
cellValue = XLCellValue.FromObject(customFieldParsed.DateValue);
}
else if (!doc.CustomFields.Any(x => x.Name == "Expiration Date"))
{
cellValue = "Present";
}
summarySheet.Cell(row, column).Value = cellValue;
}
}
row++;
}
summarySheet.Columns().AdjustToContents();
}
public static async Task<XLWorkbook> GetSummaryAsXLWorkbookAsync()
{
var documenttypes = await api.getDocumentTypesAsync();
var tags = await api.getTagsAsync();
var customFields = await api.getCustomFieldsAsync();
var documents = (await api.getMergedDocumentMetadataAsync(false, true)).Where(x => !x.CustomFields.Any(xx => xx.Name == "Action:Renewed")).OrderBy(x => x.Correspondent).ToList();
var workbook = new XLWorkbook();
PopulateSummaryWorksheet(documenttypes, tags, customFields, documents, workbook, true);
PopulateDocumentTypeWorksheets(documenttypes, tags, customFields, documents, workbook, true);
return workbook;
}
}
}