<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-GB">
	<id>https://wiki.zinform.co.nz/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Mike</id>
	<title>Zinform - User contributions [en-gb]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.zinform.co.nz/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Mike"/>
	<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Special:Contributions/Mike"/>
	<updated>2026-04-28T15:19:21Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.42.1</generator>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_6_Software&amp;diff=161</id>
		<title>Zinform 6 Software</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_6_Software&amp;diff=161"/>
		<updated>2026-03-16T19:52:09Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Current Downloads ==&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccounts_6_1_16_2.exe Zinform 6 (6.1.16.2)]&lt;br /&gt;
&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccountsSetup_5.3.40.exe Zinform 5 (5.3.40)]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; These two versions rely on each other.  Once Z6.1.16.2 has been run it &#039;&#039;&#039;will require&#039;&#039;&#039; Z5.3.40 to operate correctly.&lt;br /&gt;
&lt;br /&gt;
== Previous Downloads ==&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccounts_6_1_14_1.exe Zinforrm 6 (6.1.14.1)]&lt;br /&gt;
&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccountsSetup_5.3.38.exe Zinform 5 (5.3.38)]&lt;br /&gt;
&lt;br /&gt;
[https://dl.zinform.co.nz/emPOWER_5_3_24.zip emPOWER (5.3.24)]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=160</id>
		<title>Current Release Features</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=160"/>
		<updated>2026-03-12T20:57:52Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Release Details ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Current Release:&#039;&#039;&#039; 6.3.0.0&lt;br /&gt;
&lt;br /&gt;
=== Version 6.3.0.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Web Services and Scheduled Services Module&#039;&#039;&#039; (New):&lt;br /&gt;
** Full RESTful data API (&#039;&#039;GET/POST/PUT/DELETE&#039;&#039;) with configurable entity endpoints&lt;br /&gt;
** Scripted API system — custom C# endpoints compiled at runtime via Roslyn&lt;br /&gt;
** Scheduled background services with orchestrated lifecycle management&lt;br /&gt;
** Subscriber access control and per-endpoint authentication&lt;br /&gt;
** Anonymous endpoint support for public-facing integrations&lt;br /&gt;
** Built on .NET 10&lt;br /&gt;
** See [[#Web Services and Scheduled Services]] section below for full details&lt;br /&gt;
&lt;br /&gt;
=== Version 6.2.0.2 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Department Enhancements&#039;&#039;&#039;: Improved department code handling and error reporting&lt;br /&gt;
* &#039;&#039;&#039;BCTI Document Function&#039;&#039;&#039;: Updated document function to better handle Buyer Created Tax Invoice generation&lt;br /&gt;
* &#039;&#039;&#039;Chromium Removal&#039;&#039;&#039;: Removed embedded Chromium dependency; critical software versions bumped&lt;br /&gt;
&lt;br /&gt;
=== Version 6.2.0.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;SyncFusion Removal&#039;&#039;&#039;: Removed SyncFusion component library&lt;br /&gt;
** Migrated GridView controls to WPF DataGrid&lt;br /&gt;
** Integrated [https://github.com/lepoco/wpfui WPFUI] library for modern UI components&lt;br /&gt;
** Updated NumberBox usage to align with new library&lt;br /&gt;
** Updated GemBox license keys&lt;br /&gt;
* &#039;&#039;&#039;Debug Email Mode&#039;&#039;&#039;: Added debug email mode for troubleshooting email delivery&lt;br /&gt;
* &#039;&#039;&#039;Department User Fields&#039;&#039;&#039;: Additional user fields available at department level&lt;br /&gt;
* &#039;&#039;&#039;UI and Wizard Options&#039;&#039;&#039;: General UI and wizard option refinements&lt;br /&gt;
* &#039;&#039;&#039;Supplier Category/Type Support&#039;&#039;&#039;: Added supplier category and type classification with UI improvements&lt;br /&gt;
* &#039;&#039;&#039;Other Party Contact Methods&#039;&#039;&#039;: Refactored Other Party contact methods with ComboBox support&lt;br /&gt;
* &#039;&#039;&#039;Statement Aging&#039;&#039;&#039;: Refactored statement aging logic for out-of-period allocations&lt;br /&gt;
* &#039;&#039;&#039;Registry Item Handling&#039;&#039;&#039;: Refactored AddRegistryItem to return BoolReturn with status for improved error handling&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.3 Pre Release ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Graph Updates&#039;&#039;&#039;: Updated graph components&lt;br /&gt;
* &#039;&#039;&#039;Warning Cleanup&#039;&#039;&#039;: General codebase warning cleanup and tidy-up&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.2 ===&lt;br /&gt;
&lt;br /&gt;
* [[File:TriButtonZinMsgBox.png|thumb|170px]]&lt;br /&gt;
&#039;&#039;&#039;ZinMessageBox&#039;&#039;&#039;: Added custom message box component for runtime scripting to allow for custom &#039;tri button&#039; layouts and new custom icons.&lt;br /&gt;
* &#039;&#039;&#039;Document Scripting&#039;&#039;&#039;: Message box now available to runtime document scripts via Business Layer&lt;br /&gt;
* General tweaks and refinements&lt;br /&gt;
* &#039;&#039;&#039;Statement Generation Improvements&#039;&#039;&#039;:&lt;br /&gt;
** Major SQL and CTE optimizations for faster, more accurate financial statements&lt;br /&gt;
** Fixed transaction allocation logic to properly handle bidirectional allocations&lt;br /&gt;
** Improved running balance calculations for aged arrears&lt;br /&gt;
** Enhanced deduplication strategies to prevent duplicate transactions in statements&lt;br /&gt;
** Better handling of current month transactions vs historical aging buckets&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.1 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Progress Bar Improvements&#039;&#039;&#039;: Updated progress bar to handle child loops and display status correctly for better user feedback during long operations (Allows for more complex looping within runtime scripting)&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Departments Launch&#039;&#039;&#039;: Added department management functionality (moved away from Zinform 5)&lt;br /&gt;
* &#039;&#039;&#039;BCTI Enhancements&#039;&#039;&#039;: Changes to enhance BCTI (Buyer Created Tax Invoice) Generation&lt;br /&gt;
* &#039;&#039;&#039;Document Functions&#039;&#039;&#039;: Updated and improved document functions&lt;br /&gt;
* &#039;&#039;&#039;Customer/Supplier Display Fixes&#039;&#039;&#039;: Fixed display of customers and suppliers for statements and documents&lt;br /&gt;
* &#039;&#039;&#039;License Management Fix&#039;&#039;&#039;: Fixed opening organization license on first requirement&lt;br /&gt;
* &#039;&#039;&#039;Connection Handling&#039;&#039;&#039;: Fixed cancel connection functionality&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.15.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;General Document Wizard Enhancements&#039;&#039;&#039;: Added features to publish general documents with improved functionality&lt;br /&gt;
* &#039;&#039;&#039;Invoice Statement Support&#039;&#039;&#039;: Updates to support comprehensive invoice statements&lt;br /&gt;
* &#039;&#039;&#039;Code Cleanup&#039;&#039;&#039;: Tidied warnings throughout the codebase&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.14.1 Release Features: ===&lt;br /&gt;
&lt;br /&gt;
Document Manager:&lt;br /&gt;
&lt;br /&gt;
* Advanced Scripting via new Business Layer.&lt;br /&gt;
&lt;br /&gt;
User Manager:&lt;br /&gt;
&lt;br /&gt;
* Additional Roles and Permissions available to the new Business Layer&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* Logo&lt;br /&gt;
* Postal and Physical Address for Documents&lt;br /&gt;
* Default Bank Account&lt;br /&gt;
* Advanced Invoice Settings&lt;br /&gt;
* Microsoft Graph Email Sending via Entra App.&lt;br /&gt;
* SMTP Settings&lt;br /&gt;
&lt;br /&gt;
Bank Manager:&lt;br /&gt;
&lt;br /&gt;
* Management of Bank Accounts&lt;br /&gt;
* Bank Account Import Settings&lt;br /&gt;
** ANZ&lt;br /&gt;
** BNZ&lt;br /&gt;
** Westpac&lt;br /&gt;
** ASB (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
** Kiwibank (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
&lt;br /&gt;
Import File Admin&lt;br /&gt;
&lt;br /&gt;
* Generic CSV Import To allow for CSV/XLSX/JSON/XML file import to AP Invoice.&lt;br /&gt;
&lt;br /&gt;
Import PDF Admin (AI Enabled - Special License Required)&lt;br /&gt;
&lt;br /&gt;
* Reading of PDF AP Invoices Direct to AP Batch.&lt;br /&gt;
* Scripting to Control Every Aspect.&lt;br /&gt;
* Multi Page, Complex or Simple Invoices are handled quickly saving time on data import.&lt;br /&gt;
&lt;br /&gt;
Departments Admin&lt;br /&gt;
&lt;br /&gt;
* Management of Departments away from Zinform 5&lt;br /&gt;
* Additional Code Support to allow for up to 5 Codes&lt;br /&gt;
** Eg: Could be used to track Registration Number of a Vehicle to A Department, Trailers, ID Numbers etc for AP Invoices etc.&lt;br /&gt;
&lt;br /&gt;
== Web Services and Scheduled Services ==&lt;br /&gt;
&lt;br /&gt;
ASP.NET Core Web API built on .NET 10, hosted as a Windows Service, providing external API access to Zinform data and a runtime scripting platform for custom integrations and scheduled tasks.&lt;br /&gt;
&lt;br /&gt;
=== Data API ===&lt;br /&gt;
&lt;br /&gt;
Full CRUD RESTful API with configurable entity endpoints:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Entity Endpoints&#039;&#039;&#039;: Expose any Zinform database entity via configurable route aliases (e.g. /api/data/customers, /api/data/invoices)&lt;br /&gt;
* &#039;&#039;&#039;Per-Endpoint Access Control&#039;&#039;&#039;: Each endpoint individually configurable for Read, Create, Update, and Delete permissions&lt;br /&gt;
* &#039;&#039;&#039;Paging and Sorting&#039;&#039;&#039;: Built-in pagination with configurable max page sizes, multi-field ordering (ascending/descending)&lt;br /&gt;
* &#039;&#039;&#039;Filtering&#039;&#039;&#039;: Query string filters (e.g. ?filter.Name=Smith) and advanced POST query with operators including eq, neq, gt, gte, lt, lte, contains, startswith, endswith, between, in, isnull, isnotnull&lt;br /&gt;
* &#039;&#039;&#039;Nested Filter Logic&#039;&#039;&#039;: AND/OR filter groups with nested sub-groups for complex queries&lt;br /&gt;
* &#039;&#039;&#039;Field Selection&#039;&#039;&#039;: Request specific fields to reduce payload size&lt;br /&gt;
* &#039;&#039;&#039;Endpoint Discovery&#039;&#039;&#039;: GET /api/data lists all available enabled endpoints with their permissions&lt;br /&gt;
&lt;br /&gt;
=== Scripted API ===&lt;br /&gt;
&lt;br /&gt;
Custom API endpoints powered by Roslyn C# runtime compilation:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Custom Endpoints&#039;&#039;&#039;: Define custom routes (e.g. /api/script/myintegration) with C# script bodies compiled and cached at runtime&lt;br /&gt;
* &#039;&#039;&#039;ImportScripter Pattern&#039;&#039;&#039;: Scripts follow the same ImportScripter class pattern used in the Zinform desktop application for consistency&lt;br /&gt;
* &#039;&#039;&#039;Full Business Layer Access&#039;&#039;&#039;: Scripts receive a BusinessLayerMain instance providing full access to all Zinform business logic&lt;br /&gt;
* &#039;&#039;&#039;Request/Response Model&#039;&#039;&#039;: Custom ZinHttpRequest / ZinHttpResponse objects with access to query parameters, JSON body, headers, and authenticated user identity&lt;br /&gt;
* &#039;&#039;&#039;Per-Method Control&#039;&#039;&#039;: Each scripted endpoint configurable for specific HTTP methods (GET, POST, PUT, DELETE)&lt;br /&gt;
* &#039;&#039;&#039;Script Caching&#039;&#039;&#039;: Compiled scripts are cached with SHA256 hash-based invalidation — scripts recompile automatically when updated&lt;br /&gt;
* &#039;&#039;&#039;Endpoint Discovery&#039;&#039;&#039;: GET /api/script lists all available scripted endpoints with descriptions&lt;br /&gt;
&lt;br /&gt;
=== Script Execution API ===&lt;br /&gt;
&lt;br /&gt;
Direct script execution endpoint for ad-hoc or remote scripting:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Remote Script Execution&#039;&#039;&#039;: POST C# scripts to /api/script/execute for server-side execution&lt;br /&gt;
* &#039;&#039;&#039;ZinParameters Support&#039;&#039;&#039;: Pass key/value parameters to scripts, matching the desktop runner interface&lt;br /&gt;
* &#039;&#039;&#039;Timeout Control&#039;&#039;&#039;: Configurable execution timeout (up to 5 minutes) with cancellation support&lt;br /&gt;
* &#039;&#039;&#039;Structured Results&#039;&#039;&#039;: Returns success/failure status, output, error detail, and execution duration&lt;br /&gt;
&lt;br /&gt;
=== Scheduled Background Services ===&lt;br /&gt;
&lt;br /&gt;
Orchestrated background task system driven by database-stored service definitions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Service Orchestrator&#039;&#039;&#039;: Master hosted service that manages the lifecycle of all background workers&lt;br /&gt;
* &#039;&#039;&#039;Dynamic Configuration&#039;&#039;&#039;: Service definitions stored in the database — add, update, enable, or disable services without restarting the application&lt;br /&gt;
* &#039;&#039;&#039;Automatic Change Detection&#039;&#039;&#039;: Polls DB every 60 seconds for definition changes; workers are started, stopped, or restarted as needed using RowVersion tracking&lt;br /&gt;
* &#039;&#039;&#039;Roslyn Script Execution&#039;&#039;&#039;: Each service runs a C# script body with full Business Layer access&lt;br /&gt;
* &#039;&#039;&#039;Run Tracking&#039;&#039;&#039;: Automatic tracking of run count, fail count, last run time, last output/error, and next scheduled run&lt;br /&gt;
* &#039;&#039;&#039;User Data Fields&#039;&#039;&#039;: Five custom user data fields (User0-User4) per service definition for script-accessible configuration&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Trigger Types:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Timer Trigger&#039;&#039;&#039;: Configurable interval-based triggers with:&lt;br /&gt;
** Interval in seconds (e.g. every 5 minutes)&lt;br /&gt;
** Optional initial delay before first execution&lt;br /&gt;
** Business hours window (e.g. only fire between 8am-6pm)&lt;br /&gt;
* &#039;&#039;&#039;Folder Watch Trigger&#039;&#039;&#039;: Monitor a folder for new or changed files and trigger service execution automatically&lt;br /&gt;
* &#039;&#039;&#039;Email Trigger&#039;&#039;&#039;: Monitor an inbox for incoming emails and trigger service execution on receipt&lt;br /&gt;
&lt;br /&gt;
=== Authentication ===&lt;br /&gt;
&lt;br /&gt;
Multi-provider authentication supporting:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Local JWT&#039;&#039;&#039;: Username/password authentication via /api/auth/login&lt;br /&gt;
* &#039;&#039;&#039;Microsoft Entra ID&#039;&#039;&#039;: Azure AD / Entra ID OAuth2&lt;br /&gt;
* &#039;&#039;&#039;Google OAuth&#039;&#039;&#039;: Google account authentication&lt;br /&gt;
* &#039;&#039;&#039;Generic OAuth&#039;&#039;&#039;: Configurable custom OAuth2 provider&lt;br /&gt;
* &#039;&#039;&#039;API Key&#039;&#039;&#039;: Static API key authentication via X-Api-Key header for legacy/simple integrations&lt;br /&gt;
* &#039;&#039;&#039;Anonymous&#039;&#039;&#039;: Unauthenticated access for public endpoints&lt;br /&gt;
* &#039;&#039;&#039;Per-Endpoint Auth Schemes&#039;&#039;&#039;: Each data and scripted endpoint can specify which authentication schemes are accepted&lt;br /&gt;
&lt;br /&gt;
=== Subscriber Access Control ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Subscriber Middleware&#039;&#039;&#039;: Validates subscriber keys and stores subscriber context for downstream use&lt;br /&gt;
* &#039;&#039;&#039;Per-Endpoint Subscriber Permissions&#039;&#039;&#039;: Subscribers can be granted access to specific data endpoints and scripted endpoints independently&lt;br /&gt;
* &#039;&#039;&#039;Encrypted Subscriber Keys&#039;&#039;&#039;: Subscriber authentication keys with encryption support&lt;br /&gt;
&lt;br /&gt;
=== Administration ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Admin Console&#039;&#039;&#039;: Localhost-only admin interface for service management&lt;br /&gt;
* &#039;&#039;&#039;Swagger / OpenAPI&#039;&#039;&#039;: Full Swagger UI documentation with all authentication schemes documented&lt;br /&gt;
* &#039;&#039;&#039;Service Definition Admin&#039;&#039;&#039;: Admin UI in Zinform desktop for managing service definitions, scripted endpoints, API endpoints, and request metrics&lt;br /&gt;
* &#039;&#039;&#039;Request Metrics&#039;&#039;&#039;: Built-in request tracking for monitoring API activity&lt;br /&gt;
* &#039;&#039;&#039;Windows Service Hosting&#039;&#039;&#039;: Runs as a Windows Service or under IIS&lt;br /&gt;
&lt;br /&gt;
=== Coming Soon: ===&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* IMAP Settings (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
CRM Module (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Email Processing (Coming Soon)&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=159</id>
		<title>Current Release Features</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=159"/>
		<updated>2026-03-12T20:56:59Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Release Details ==&lt;br /&gt;
&#039;&#039;&#039;Current Release:&#039;&#039;&#039; 6.1.16.2&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.2 ===&lt;br /&gt;
&lt;br /&gt;
* [[File:TriButtonZinMsgBox.png|thumb|170x170px]]&#039;&#039;&#039;ZinMessageBox&#039;&#039;&#039;: Added custom message box component for runtime scripting to allow for custom &#039;tri button&#039; layouts and new custom icons.&lt;br /&gt;
* &#039;&#039;&#039;Document Scripting&#039;&#039;&#039;: Message box now available to runtime document scripts via Business Layer&lt;br /&gt;
* General tweaks and refinements&lt;br /&gt;
* &#039;&#039;&#039;Statement Generation Improvements&#039;&#039;&#039;:&lt;br /&gt;
** Major SQL and CTE optimizations for faster, more accurate financial statements&lt;br /&gt;
** Fixed transaction allocation logic to properly handle bidirectional allocations&lt;br /&gt;
** Improved running balance calculations for aged arrears&lt;br /&gt;
** Enhanced deduplication strategies to prevent duplicate transactions in statements&lt;br /&gt;
** Better handling of current month transactions vs historical aging buckets&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.1 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Progress Bar Improvements&#039;&#039;&#039;: Updated progress bar to handle child loops and display status correctly for better user feedback during long operations (Allows for more complex looping within runtime scripting)&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Departments Launch&#039;&#039;&#039;: Added department management functionality (moved away from Zinform 5)&lt;br /&gt;
* &#039;&#039;&#039;BCTI Enhancements&#039;&#039;&#039;: Changes to enhance BCTI (Buyer Created Tax Invoice) Generation&lt;br /&gt;
* &#039;&#039;&#039;Document Functions&#039;&#039;&#039;: Updated and improved document functions&lt;br /&gt;
* &#039;&#039;&#039;Customer/Supplier Display Fixes&#039;&#039;&#039;: Fixed display of customers and suppliers for statements and documents&lt;br /&gt;
* &#039;&#039;&#039;License Management Fix&#039;&#039;&#039;: Fixed opening organization license on first requirement&lt;br /&gt;
* &#039;&#039;&#039;Connection Handling&#039;&#039;&#039;: Fixed cancel connection functionality&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.15.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;General Document Wizard Enhancements&#039;&#039;&#039;: Added features to publish general documents with improved functionality&lt;br /&gt;
* &#039;&#039;&#039;Invoice Statement Support&#039;&#039;&#039;: Updates to support comprehensive invoice statements&lt;br /&gt;
* &#039;&#039;&#039;Code Cleanup&#039;&#039;&#039;: Tidied warnings throughout the codebase&lt;br /&gt;
&lt;br /&gt;
=== Version  6.1.14.1 Release Features: ===&lt;br /&gt;
Document Manager:&lt;br /&gt;
&lt;br /&gt;
* Advanced Scripting via new Business Layer.&lt;br /&gt;
&lt;br /&gt;
User Manager:&lt;br /&gt;
&lt;br /&gt;
* Additional Roles and Permissions available to the new Business Layer&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* Logo&lt;br /&gt;
* Postal and Physical Address for Documents&lt;br /&gt;
* Default Bank Account&lt;br /&gt;
* Advanced Invoice Settings&lt;br /&gt;
* Microsoft Graph Email Sending via Entra App.&lt;br /&gt;
* SMTP Settings&lt;br /&gt;
&lt;br /&gt;
Bank Manager:&lt;br /&gt;
&lt;br /&gt;
* Management of Bank Accounts&lt;br /&gt;
* Bank Account Import Settings&lt;br /&gt;
** ANZ&lt;br /&gt;
** BNZ&lt;br /&gt;
** Westpac&lt;br /&gt;
** ASB (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
** Kiwibank (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
Import File Admin&lt;br /&gt;
&lt;br /&gt;
* Generic CSV Import To allow for CSV/XLSX/JSON/XML file import to AP Invoice.&lt;br /&gt;
&lt;br /&gt;
Import PDF Admin (AI Enabled - Special License Required)&lt;br /&gt;
&lt;br /&gt;
* Reading of PDF AP Invoices Direct to AP Batch.&lt;br /&gt;
* Scripting to Control Every Aspect.&lt;br /&gt;
* Multi Page, Complex or Simple Invoices are handled quickly saving time on data import.&lt;br /&gt;
&lt;br /&gt;
Departments Admin&lt;br /&gt;
&lt;br /&gt;
* Management of Departments away from Zinform 5&lt;br /&gt;
* Additional Code Support to allow for up to 5 Codes&lt;br /&gt;
** Eg: Could be used to track Registration Number of a Vehicle to A Department, Trailers, ID Numbers etc for AP Invoices etc.&lt;br /&gt;
&lt;br /&gt;
=== Coming Soon: ===&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* IMAP Settings (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
CRM Module (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Email Processing (Coming Soon)&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=158</id>
		<title>Current Release Features</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=158"/>
		<updated>2026-03-12T20:47:03Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Release Details ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Current Release:&#039;&#039;&#039; 6.3.0.0&lt;br /&gt;
&lt;br /&gt;
=== Version 6.3.0.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Web Services &amp;amp; Scheduled Services Module&#039;&#039;&#039; (New):&lt;br /&gt;
** Full RESTful data API (&#039;&#039;GET/POST/PUT/DELETE&#039;&#039;) with configurable entity endpoints&lt;br /&gt;
** Scripted API system — custom C# endpoints compiled at runtime via Roslyn&lt;br /&gt;
** Scheduled background services with orchestrated lifecycle management&lt;br /&gt;
** Subscriber access control and per-endpoint authentication&lt;br /&gt;
** Anonymous endpoint support for public-facing integrations&lt;br /&gt;
** Built on .NET 10&lt;br /&gt;
** See [[#Web Services &amp;amp; Scheduled Services]] section below for full details&lt;br /&gt;
&lt;br /&gt;
=== Version 6.2.0.2 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Department Enhancements&#039;&#039;&#039;: Improved department code handling and error reporting&lt;br /&gt;
* &#039;&#039;&#039;BCTI Document Function&#039;&#039;&#039;: Updated document function to better handle Buyer Created Tax Invoice generation&lt;br /&gt;
* &#039;&#039;&#039;Chromium Removal&#039;&#039;&#039;: Removed embedded Chromium dependency; critical software versions bumped&lt;br /&gt;
&lt;br /&gt;
=== Version 6.2.0.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;SyncFusion Removal&#039;&#039;&#039;: Removed SyncFusion component library&lt;br /&gt;
** Migrated GridView controls to WPF DataGrid&lt;br /&gt;
** Integrated [https://github.com/lepoco/wpfui WPFUI] library for modern UI components&lt;br /&gt;
** Updated NumberBox usage to align with new library&lt;br /&gt;
** Updated GemBox license keys&lt;br /&gt;
* &#039;&#039;&#039;Debug Email Mode&#039;&#039;&#039;: Added debug email mode for troubleshooting email delivery&lt;br /&gt;
* &#039;&#039;&#039;Department User Fields&#039;&#039;&#039;: Additional user fields available at department level&lt;br /&gt;
* &#039;&#039;&#039;UI &amp;amp; Wizard Options&#039;&#039;&#039;: General UI and wizard option refinements&lt;br /&gt;
* &#039;&#039;&#039;Supplier Category/Type Support&#039;&#039;&#039;: Added supplier category and type classification with UI improvements&lt;br /&gt;
* &#039;&#039;&#039;Other Party Contact Methods&#039;&#039;&#039;: Refactored Other Party contact methods with ComboBox support&lt;br /&gt;
* &#039;&#039;&#039;Statement Aging&#039;&#039;&#039;: Refactored statement aging logic for out-of-period allocations&lt;br /&gt;
* &#039;&#039;&#039;Registry Item Handling&#039;&#039;&#039;: Refactored AddRegistryItem to return BoolReturn with status for improved error handling&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.3 Pre Release ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Graph Updates&#039;&#039;&#039;: Updated graph components&lt;br /&gt;
* &#039;&#039;&#039;Warning Cleanup&#039;&#039;&#039;: General codebase warning cleanup and tidy-up&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.2 ===&lt;br /&gt;
&lt;br /&gt;
* [[File:TriButtonZinMsgBox.png|thumb|170px]]&lt;br /&gt;
&#039;&#039;&#039;ZinMessageBox&#039;&#039;&#039;: Added custom message box component for runtime scripting to allow for custom &#039;tri button&#039; layouts and new custom icons.&lt;br /&gt;
* &#039;&#039;&#039;Document Scripting&#039;&#039;&#039;: Message box now available to runtime document scripts via Business Layer&lt;br /&gt;
* General tweaks and refinements&lt;br /&gt;
* &#039;&#039;&#039;Statement Generation Improvements&#039;&#039;&#039;:&lt;br /&gt;
** Major SQL and CTE optimizations for faster, more accurate financial statements&lt;br /&gt;
** Fixed transaction allocation logic to properly handle bidirectional allocations&lt;br /&gt;
** Improved running balance calculations for aged arrears&lt;br /&gt;
** Enhanced deduplication strategies to prevent duplicate transactions in statements&lt;br /&gt;
** Better handling of current month transactions vs historical aging buckets&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.1 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Progress Bar Improvements&#039;&#039;&#039;: Updated progress bar to handle child loops and display status correctly for better user feedback during long operations (Allows for more complex looping within runtime scripting)&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Departments Launch&#039;&#039;&#039;: Added department management functionality (moved away from Zinform 5)&lt;br /&gt;
* &#039;&#039;&#039;BCTI Enhancements&#039;&#039;&#039;: Changes to enhance BCTI (Buyer Created Tax Invoice) Generation&lt;br /&gt;
* &#039;&#039;&#039;Document Functions&#039;&#039;&#039;: Updated and improved document functions&lt;br /&gt;
* &#039;&#039;&#039;Customer/Supplier Display Fixes&#039;&#039;&#039;: Fixed display of customers and suppliers for statements and documents&lt;br /&gt;
* &#039;&#039;&#039;License Management Fix&#039;&#039;&#039;: Fixed opening organization license on first requirement&lt;br /&gt;
* &#039;&#039;&#039;Connection Handling&#039;&#039;&#039;: Fixed cancel connection functionality&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.15.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;General Document Wizard Enhancements&#039;&#039;&#039;: Added features to publish general documents with improved functionality&lt;br /&gt;
* &#039;&#039;&#039;Invoice Statement Support&#039;&#039;&#039;: Updates to support comprehensive invoice statements&lt;br /&gt;
* &#039;&#039;&#039;Code Cleanup&#039;&#039;&#039;: Tidied warnings throughout the codebase&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.14.1 Release Features: ===&lt;br /&gt;
&lt;br /&gt;
Document Manager:&lt;br /&gt;
&lt;br /&gt;
* Advanced Scripting via new Business Layer.&lt;br /&gt;
&lt;br /&gt;
User Manager:&lt;br /&gt;
&lt;br /&gt;
* Additional Roles and Permissions available to the new Business Layer&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* Logo&lt;br /&gt;
* Postal and Physical Address for Documents&lt;br /&gt;
* Default Bank Account&lt;br /&gt;
* Advanced Invoice Settings&lt;br /&gt;
* Microsoft Graph Email Sending via Entra App.&lt;br /&gt;
* SMTP Settings&lt;br /&gt;
&lt;br /&gt;
Bank Manager:&lt;br /&gt;
&lt;br /&gt;
* Management of Bank Accounts&lt;br /&gt;
* Bank Account Import Settings&lt;br /&gt;
** ANZ&lt;br /&gt;
** BNZ&lt;br /&gt;
** Westpac&lt;br /&gt;
** ASB (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
** Kiwibank (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
&lt;br /&gt;
Import File Admin&lt;br /&gt;
&lt;br /&gt;
* Generic CSV Import To allow for CSV/XLSX/JSON/XML file import to AP Invoice.&lt;br /&gt;
&lt;br /&gt;
Import PDF Admin (AI Enabled - Special License Required)&lt;br /&gt;
&lt;br /&gt;
* Reading of PDF AP Invoices Direct to AP Batch.&lt;br /&gt;
* Scripting to Control Every Aspect.&lt;br /&gt;
* Multi Page, Complex or Simple Invoices are handled quickly saving time on data import.&lt;br /&gt;
&lt;br /&gt;
Departments Admin&lt;br /&gt;
&lt;br /&gt;
* Management of Departments away from Zinform 5&lt;br /&gt;
* Additional Code Support to allow for up to 5 Codes&lt;br /&gt;
** Eg: Could be used to track Registration Number of a Vehicle to A Department, Trailers, ID Numbers etc for AP Invoices etc.&lt;br /&gt;
&lt;br /&gt;
== Web Services &amp;amp; Scheduled Services ==&lt;br /&gt;
&lt;br /&gt;
ASP.NET Core Web API built on .NET 10, hosted as a Windows Service, providing external API access to Zinform data and a runtime scripting platform for custom integrations and scheduled tasks.&lt;br /&gt;
&lt;br /&gt;
=== Data API ===&lt;br /&gt;
&lt;br /&gt;
Full CRUD RESTful API with configurable entity endpoints:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Entity Endpoints&#039;&#039;&#039;: Expose any Zinform database entity via configurable route aliases (e.g. &amp;lt;code&amp;gt;/api/data/customers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;/api/data/invoices&amp;lt;/code&amp;gt;)&lt;br /&gt;
* &#039;&#039;&#039;Per-Endpoint Access Control&#039;&#039;&#039;: Each endpoint individually configurable for Read, Create, Update, and Delete permissions&lt;br /&gt;
* &#039;&#039;&#039;Paging &amp;amp; Sorting&#039;&#039;&#039;: Built-in pagination with configurable max page sizes, multi-field ordering (ascending/descending)&lt;br /&gt;
* &#039;&#039;&#039;Filtering&#039;&#039;&#039;: Query string filters (&amp;lt;code&amp;gt;?filter.Name=Smith&amp;lt;/code&amp;gt;) and advanced POST query with operators including eq, neq, gt, gte, lt, lte, contains, startswith, endswith, between, in, isnull, isnotnull&lt;br /&gt;
* &#039;&#039;&#039;Nested Filter Logic&#039;&#039;&#039;: AND/OR filter groups with nested sub-groups for complex queries&lt;br /&gt;
* &#039;&#039;&#039;Field Selection&#039;&#039;&#039;: Request specific fields to reduce payload size&lt;br /&gt;
* &#039;&#039;&#039;Endpoint Discovery&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/data&amp;lt;/code&amp;gt; lists all available enabled endpoints with their permissions&lt;br /&gt;
&lt;br /&gt;
=== Scripted API ===&lt;br /&gt;
&lt;br /&gt;
Custom API endpoints powered by Roslyn C# runtime compilation:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Custom Endpoints&#039;&#039;&#039;: Define custom routes (e.g. &amp;lt;code&amp;gt;/api/script/myintegration&amp;lt;/code&amp;gt;) with C# script bodies compiled and cached at runtime&lt;br /&gt;
* &#039;&#039;&#039;ImportScripter Pattern&#039;&#039;&#039;: Scripts follow the same &amp;lt;code&amp;gt;ImportScripter&amp;lt;/code&amp;gt; class pattern used in the Zinform desktop application for consistency&lt;br /&gt;
* &#039;&#039;&#039;Full Business Layer Access&#039;&#039;&#039;: Scripts receive a &amp;lt;code&amp;gt;BusinessLayerMain&amp;lt;/code&amp;gt; instance providing full access to all Zinform business logic&lt;br /&gt;
* &#039;&#039;&#039;Request/Response Model&#039;&#039;&#039;: Custom &amp;lt;code&amp;gt;ZinHttpRequest&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ZinHttpResponse&amp;lt;/code&amp;gt; objects with access to query parameters, JSON body, headers, and authenticated user identity&lt;br /&gt;
* &#039;&#039;&#039;Per-Method Control&#039;&#039;&#039;: Each scripted endpoint configurable for specific HTTP methods (GET, POST, PUT, DELETE)&lt;br /&gt;
* &#039;&#039;&#039;Script Caching&#039;&#039;&#039;: Compiled scripts are cached with SHA256 hash-based invalidation — scripts recompile automatically when updated&lt;br /&gt;
* &#039;&#039;&#039;Endpoint Discovery&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/script&amp;lt;/code&amp;gt; lists all available scripted endpoints with descriptions&lt;br /&gt;
&lt;br /&gt;
=== Script Execution API ===&lt;br /&gt;
&lt;br /&gt;
Direct script execution endpoint for ad-hoc or remote scripting:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Remote Script Execution&#039;&#039;&#039;: POST C# scripts to &amp;lt;code&amp;gt;/api/script/execute&amp;lt;/code&amp;gt; for server-side execution&lt;br /&gt;
* &#039;&#039;&#039;ZinParameters Support&#039;&#039;&#039;: Pass key/value parameters to scripts, matching the desktop runner interface&lt;br /&gt;
* &#039;&#039;&#039;Timeout Control&#039;&#039;&#039;: Configurable execution timeout (up to 5 minutes) with cancellation support&lt;br /&gt;
* &#039;&#039;&#039;Structured Results&#039;&#039;&#039;: Returns success/failure status, output, error detail, and execution duration&lt;br /&gt;
&lt;br /&gt;
=== Scheduled Background Services ===&lt;br /&gt;
&lt;br /&gt;
Orchestrated background task system driven by database-stored service definitions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Service Orchestrator&#039;&#039;&#039;: Master hosted service that manages the lifecycle of all background workers&lt;br /&gt;
* &#039;&#039;&#039;Dynamic Configuration&#039;&#039;&#039;: Service definitions stored in the database — add, update, enable, or disable services without restarting the application&lt;br /&gt;
* &#039;&#039;&#039;Automatic Change Detection&#039;&#039;&#039;: Polls DB every 60 seconds for definition changes; workers are started, stopped, or restarted as needed using RowVersion tracking&lt;br /&gt;
* &#039;&#039;&#039;Roslyn Script Execution&#039;&#039;&#039;: Each service runs a C# script body with full Business Layer access&lt;br /&gt;
* &#039;&#039;&#039;Run Tracking&#039;&#039;&#039;: Automatic tracking of run count, fail count, last run time, last output/error, and next scheduled run&lt;br /&gt;
* &#039;&#039;&#039;User Data Fields&#039;&#039;&#039;: Five custom user data fields (User0–User4) per service definition for script-accessible configuration&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Trigger Types:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Timer Trigger&#039;&#039;&#039;: Configurable interval-based triggers with:&lt;br /&gt;
** Interval in seconds (e.g. every 5 minutes)&lt;br /&gt;
** Optional initial delay before first execution&lt;br /&gt;
** Business hours window (e.g. only fire between 8am–6pm)&lt;br /&gt;
* &#039;&#039;&#039;Folder Watch Trigger&#039;&#039;&#039;: Monitor a folder for new or changed files and trigger service execution automatically&lt;br /&gt;
* &#039;&#039;&#039;Email Trigger&#039;&#039;&#039;: Monitor an inbox for incoming emails and trigger service execution on receipt&lt;br /&gt;
&lt;br /&gt;
=== Authentication ===&lt;br /&gt;
&lt;br /&gt;
Multi-provider authentication supporting:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Local JWT&#039;&#039;&#039;: Username/password authentication via &amp;lt;code&amp;gt;/api/auth/login&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Microsoft Entra ID&#039;&#039;&#039;: Azure AD / Entra ID OAuth2&lt;br /&gt;
* &#039;&#039;&#039;Google OAuth&#039;&#039;&#039;: Google account authentication&lt;br /&gt;
* &#039;&#039;&#039;Generic OAuth&#039;&#039;&#039;: Configurable custom OAuth2 provider&lt;br /&gt;
* &#039;&#039;&#039;API Key&#039;&#039;&#039;: Static API key authentication via &amp;lt;code&amp;gt;X-Api-Key&amp;lt;/code&amp;gt; header for legacy/simple integrations&lt;br /&gt;
* &#039;&#039;&#039;Anonymous&#039;&#039;&#039;: Unauthenticated access for public endpoints&lt;br /&gt;
* &#039;&#039;&#039;Per-Endpoint Auth Schemes&#039;&#039;&#039;: Each data and scripted endpoint can specify which authentication schemes are accepted&lt;br /&gt;
&lt;br /&gt;
=== Subscriber Access Control ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Subscriber Middleware&#039;&#039;&#039;: Validates subscriber keys and stores subscriber context for downstream use&lt;br /&gt;
* &#039;&#039;&#039;Per-Endpoint Subscriber Permissions&#039;&#039;&#039;: Subscribers can be granted access to specific data endpoints and scripted endpoints independently&lt;br /&gt;
* &#039;&#039;&#039;Encrypted Subscriber Keys&#039;&#039;&#039;: Subscriber authentication keys with encryption support&lt;br /&gt;
&lt;br /&gt;
=== Administration ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Admin Console&#039;&#039;&#039;: Localhost-only admin interface for service management&lt;br /&gt;
* &#039;&#039;&#039;Swagger / OpenAPI&#039;&#039;&#039;: Full Swagger UI documentation with all authentication schemes documented&lt;br /&gt;
* &#039;&#039;&#039;Service Definition Admin&#039;&#039;&#039;: Admin UI in Zinform desktop for managing service definitions, scripted endpoints, API endpoints, and request metrics&lt;br /&gt;
* &#039;&#039;&#039;Request Metrics&#039;&#039;&#039;: Built-in request tracking for monitoring API activity&lt;br /&gt;
* &#039;&#039;&#039;Windows Service Hosting&#039;&#039;&#039;: Runs as a Windows Service or under IIS&lt;br /&gt;
&lt;br /&gt;
=== Coming Soon: ===&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* IMAP Settings (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
CRM Module (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Email Processing (Coming Soon)&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=157</id>
		<title>Current Release Features</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=157"/>
		<updated>2026-01-20T20:20:43Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Release Details ==&lt;br /&gt;
&#039;&#039;&#039;Current Release:&#039;&#039;&#039; 6.1.16.2&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.2 ===&lt;br /&gt;
&lt;br /&gt;
* [[File:TriButtonZinMsgBox.png|thumb|170x170px]]&#039;&#039;&#039;ZinMessageBox&#039;&#039;&#039;: Added custom message box component for runtime scripting to allow for custom &#039;tri button&#039; layouts and new custom icons.&lt;br /&gt;
* &#039;&#039;&#039;Document Scripting&#039;&#039;&#039;: Message box now available to runtime document scripts via Business Layer&lt;br /&gt;
* General tweaks and refinements&lt;br /&gt;
* &#039;&#039;&#039;Statement Generation Improvements&#039;&#039;&#039;:&lt;br /&gt;
** Major SQL and CTE optimizations for faster, more accurate financial statements&lt;br /&gt;
** Fixed transaction allocation logic to properly handle bidirectional allocations&lt;br /&gt;
** Improved running balance calculations for aged arrears&lt;br /&gt;
** Enhanced deduplication strategies to prevent duplicate transactions in statements&lt;br /&gt;
** Better handling of current month transactions vs historical aging buckets&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.1 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Progress Bar Improvements&#039;&#039;&#039;: Updated progress bar to handle child loops and display status correctly for better user feedback during long operations (Allows for more complex looping within runtime scripting)&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Departments Launch&#039;&#039;&#039;: Added department management functionality (moved away from Zinform 5)&lt;br /&gt;
* &#039;&#039;&#039;BCTI Enhancements&#039;&#039;&#039;: Changes to enhance BCTI (Buyer Created Tax Invoice) Generation&lt;br /&gt;
* &#039;&#039;&#039;Document Functions&#039;&#039;&#039;: Updated and improved document functions&lt;br /&gt;
* &#039;&#039;&#039;Customer/Supplier Display Fixes&#039;&#039;&#039;: Fixed display of customers and suppliers for statements and documents&lt;br /&gt;
* &#039;&#039;&#039;License Management Fix&#039;&#039;&#039;: Fixed opening organization license on first requirement&lt;br /&gt;
* &#039;&#039;&#039;Connection Handling&#039;&#039;&#039;: Fixed cancel connection functionality&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.15.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;General Document Wizard Enhancements&#039;&#039;&#039;: Added features to publish general documents with improved functionality&lt;br /&gt;
* &#039;&#039;&#039;Invoice Statement Support&#039;&#039;&#039;: Updates to support comprehensive invoice statements&lt;br /&gt;
* &#039;&#039;&#039;Code Cleanup&#039;&#039;&#039;: Tidied warnings throughout the codebase&lt;br /&gt;
&lt;br /&gt;
=== Version  6.1.14.1 Release Features: ===&lt;br /&gt;
Document Manager:&lt;br /&gt;
&lt;br /&gt;
* Advanced Scripting via new Business Layer.&lt;br /&gt;
&lt;br /&gt;
User Manager:&lt;br /&gt;
&lt;br /&gt;
* Additional Roles and Permissions available to the new Business Layer&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* Logo&lt;br /&gt;
* Postal and Physical Address for Documents&lt;br /&gt;
* Default Bank Account&lt;br /&gt;
* Advanced Invoice Settings&lt;br /&gt;
* Microsoft Graph Email Sending via Entra App.&lt;br /&gt;
* SMTP Settings&lt;br /&gt;
&lt;br /&gt;
Bank Manager:&lt;br /&gt;
&lt;br /&gt;
* Management of Bank Accounts&lt;br /&gt;
* Bank Account Import Settings&lt;br /&gt;
** ANZ&lt;br /&gt;
** BNZ&lt;br /&gt;
** Westpac&lt;br /&gt;
** ASB (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
** Kiwibank (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
Import File Admin&lt;br /&gt;
&lt;br /&gt;
* Generic CSV Import To allow for CSV/XLSX/JSON/XML file import to AP Invoice.&lt;br /&gt;
&lt;br /&gt;
Import PDF Admin (AI Enabled - Special License Required)&lt;br /&gt;
&lt;br /&gt;
* Reading of PDF AP Invoices Direct to AP Batch.&lt;br /&gt;
* Scripting to Control Every Aspect.&lt;br /&gt;
* Multi Page, Complex or Simple Invoices are handled quickly saving time on data import.&lt;br /&gt;
&lt;br /&gt;
Departments Admin&lt;br /&gt;
&lt;br /&gt;
* Management of Departments away from Zinform 5&lt;br /&gt;
* Additional Code Support to allow for up to 5 Codes&lt;br /&gt;
** Eg: Could be used to track Registration Number of a Vehicle to A Department, Trailers, ID Numbers etc for AP Invoices etc.&lt;br /&gt;
&lt;br /&gt;
=== Coming Soon: ===&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* IMAP Settings (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
CRM Module (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Email Processing (Coming Soon)&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=156</id>
		<title>Current Release Features</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=156"/>
		<updated>2026-01-20T20:12:47Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Release Details ==&lt;br /&gt;
&#039;&#039;&#039;Current Release:&#039;&#039;&#039; 6.1.16.2&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.2 ===&lt;br /&gt;
&lt;br /&gt;
* [[File:TriButtonZinMsgBox.png|thumb|170x170px]]&#039;&#039;&#039;ZinMessageBox&#039;&#039;&#039;: Added custom message box component for runtime scripting to allow for custom &#039;tri button&#039; layouts and new custom icons.&lt;br /&gt;
* &#039;&#039;&#039;Document Scripting&#039;&#039;&#039;: Message box now available to runtime document scripts via Business Layer&lt;br /&gt;
* General tweaks and refinements&lt;br /&gt;
* Enhanced SQL and CTE&#039;s for pulling data for Statements on GetMonthlyRunningTransactions2 with improvement on speed and data returned including details for allocations future and past dated.&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.1 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Progress Bar Improvements&#039;&#039;&#039;: Updated progress bar to handle child loops and display status correctly for better user feedback during long operations (Allows for more complex looping within runtime scripting)&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Departments Launch&#039;&#039;&#039;: Added department management functionality (moved away from Zinform 5)&lt;br /&gt;
* &#039;&#039;&#039;BCTI Enhancements&#039;&#039;&#039;: Changes to enhance BCTI (Buyer Created Tax Invoice) Generation&lt;br /&gt;
* &#039;&#039;&#039;Document Functions&#039;&#039;&#039;: Updated and improved document functions&lt;br /&gt;
* &#039;&#039;&#039;Customer/Supplier Display Fixes&#039;&#039;&#039;: Fixed display of customers and suppliers for statements and documents&lt;br /&gt;
* &#039;&#039;&#039;License Management Fix&#039;&#039;&#039;: Fixed opening organization license on first requirement&lt;br /&gt;
* &#039;&#039;&#039;Connection Handling&#039;&#039;&#039;: Fixed cancel connection functionality&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.15.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;General Document Wizard Enhancements&#039;&#039;&#039;: Added features to publish general documents with improved functionality&lt;br /&gt;
* &#039;&#039;&#039;Invoice Statement Support&#039;&#039;&#039;: Updates to support comprehensive invoice statements&lt;br /&gt;
* &#039;&#039;&#039;Code Cleanup&#039;&#039;&#039;: Tidied warnings throughout the codebase&lt;br /&gt;
&lt;br /&gt;
=== Version  6.1.14.1 Release Features: ===&lt;br /&gt;
Document Manager:&lt;br /&gt;
&lt;br /&gt;
* Advanced Scripting via new Business Layer.&lt;br /&gt;
&lt;br /&gt;
User Manager:&lt;br /&gt;
&lt;br /&gt;
* Additional Roles and Permissions available to the new Business Layer&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* Logo&lt;br /&gt;
* Postal and Physical Address for Documents&lt;br /&gt;
* Default Bank Account&lt;br /&gt;
* Advanced Invoice Settings&lt;br /&gt;
* Microsoft Graph Email Sending via Entra App.&lt;br /&gt;
* SMTP Settings&lt;br /&gt;
&lt;br /&gt;
Bank Manager:&lt;br /&gt;
&lt;br /&gt;
* Management of Bank Accounts&lt;br /&gt;
* Bank Account Import Settings&lt;br /&gt;
** ANZ&lt;br /&gt;
** BNZ&lt;br /&gt;
** Westpac&lt;br /&gt;
** ASB (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
** Kiwibank (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
Import File Admin&lt;br /&gt;
&lt;br /&gt;
* Generic CSV Import To allow for CSV/XLSX/JSON/XML file import to AP Invoice.&lt;br /&gt;
&lt;br /&gt;
Import PDF Admin (AI Enabled - Special License Required)&lt;br /&gt;
&lt;br /&gt;
* Reading of PDF AP Invoices Direct to AP Batch.&lt;br /&gt;
* Scripting to Control Every Aspect.&lt;br /&gt;
* Multi Page, Complex or Simple Invoices are handled quickly saving time on data import.&lt;br /&gt;
&lt;br /&gt;
Departments Admin&lt;br /&gt;
&lt;br /&gt;
* Management of Departments away from Zinform 5&lt;br /&gt;
* Additional Code Support to allow for up to 5 Codes&lt;br /&gt;
** Eg: Could be used to track Registration Number of a Vehicle to A Department, Trailers, ID Numbers etc for AP Invoices etc.&lt;br /&gt;
&lt;br /&gt;
=== Coming Soon: ===&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* IMAP Settings (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
CRM Module (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Email Processing (Coming Soon)&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=File:TriButtonZinMsgBox.png&amp;diff=155</id>
		<title>File:TriButtonZinMsgBox.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=File:TriButtonZinMsgBox.png&amp;diff=155"/>
		<updated>2026-01-20T20:03:55Z</updated>

		<summary type="html">&lt;p&gt;Mike: TriButtonZinMsgBox&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
TriButtonZinMsgBox&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=154</id>
		<title>Current Release Features</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=154"/>
		<updated>2026-01-20T20:02:57Z</updated>

		<summary type="html">&lt;p&gt;Mike: Updated&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Release Details ==&lt;br /&gt;
&#039;&#039;&#039;Current Release:&#039;&#039;&#039; 6.1.16.2&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.2 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ZinMessageBox&#039;&#039;&#039;: Added custom message box component for runtime scripting to allow for custom &#039;tri button&#039; layouts and new custom icons.&lt;br /&gt;
* &#039;&#039;&#039;Document Scripting&#039;&#039;&#039;: Message box now available to runtime document scripts via Business Layer&lt;br /&gt;
* General tweaks and refinements&lt;br /&gt;
* Enhanced SQL and CTE&#039;s for pulling data for Statements on GetMonthlyRunningTransactions2 with improvement on speed and data returned including details for allocations future and past dated.&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.1 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Progress Bar Improvements&#039;&#039;&#039;: Updated progress bar to handle child loops and display status correctly for better user feedback during long operations (Allows for more complex looping within runtime scripting)&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.16.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Departments Launch&#039;&#039;&#039;: Added department management functionality (moved away from Zinform 5)&lt;br /&gt;
* &#039;&#039;&#039;BCTI Enhancements&#039;&#039;&#039;: Changes to enhance BCTI (Buyer Created Tax Invoice) Generation&lt;br /&gt;
* &#039;&#039;&#039;Document Functions&#039;&#039;&#039;: Updated and improved document functions&lt;br /&gt;
* &#039;&#039;&#039;Customer/Supplier Display Fixes&#039;&#039;&#039;: Fixed display of customers and suppliers for statements and documents&lt;br /&gt;
* &#039;&#039;&#039;License Management Fix&#039;&#039;&#039;: Fixed opening organization license on first requirement&lt;br /&gt;
* &#039;&#039;&#039;Connection Handling&#039;&#039;&#039;: Fixed cancel connection functionality&lt;br /&gt;
&lt;br /&gt;
=== Version 6.1.15.0 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;General Document Wizard Enhancements&#039;&#039;&#039;: Added features to publish general documents with improved functionality&lt;br /&gt;
* &#039;&#039;&#039;Invoice Statement Support&#039;&#039;&#039;: Updates to support comprehensive invoice statements&lt;br /&gt;
* &#039;&#039;&#039;Code Cleanup&#039;&#039;&#039;: Tidied warnings throughout the codebase&lt;br /&gt;
&lt;br /&gt;
=== Version  6.1.14.1 Release Features: ===&lt;br /&gt;
Document Manager:&lt;br /&gt;
&lt;br /&gt;
* Advanced Scripting via new Business Layer.&lt;br /&gt;
&lt;br /&gt;
User Manager:&lt;br /&gt;
&lt;br /&gt;
* Additional Roles and Permissions available to the new Business Layer&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* Logo&lt;br /&gt;
* Postal and Physical Address for Documents&lt;br /&gt;
* Default Bank Account&lt;br /&gt;
* Advanced Invoice Settings&lt;br /&gt;
* Microsoft Graph Email Sending via Entra App.&lt;br /&gt;
* SMTP Settings&lt;br /&gt;
&lt;br /&gt;
Bank Manager:&lt;br /&gt;
&lt;br /&gt;
* Management of Bank Accounts&lt;br /&gt;
* Bank Account Import Settings&lt;br /&gt;
** ANZ&lt;br /&gt;
** BNZ&lt;br /&gt;
** Westpac&lt;br /&gt;
** ASB (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
** Kiwibank (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
Import File Admin&lt;br /&gt;
&lt;br /&gt;
* Generic CSV Import To allow for CSV/XLSX/JSON/XML file import to AP Invoice.&lt;br /&gt;
&lt;br /&gt;
Import PDF Admin (AI Enabled - Special License Required)&lt;br /&gt;
&lt;br /&gt;
* Reading of PDF AP Invoices Direct to AP Batch.&lt;br /&gt;
* Scripting to Control Every Aspect.&lt;br /&gt;
* Multi Page, Complex or Simple Invoices are handled quickly saving time on data import.&lt;br /&gt;
&lt;br /&gt;
Departments Admin&lt;br /&gt;
&lt;br /&gt;
* Management of Departments away from Zinform 5&lt;br /&gt;
* Additional Code Support to allow for up to 5 Codes&lt;br /&gt;
** Eg: Could be used to track Registration Number of a Vehicle to A Department, Trailers, ID Numbers etc for AP Invoices etc.&lt;br /&gt;
&lt;br /&gt;
=== Coming Soon: ===&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* IMAP Settings (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
CRM Module (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Email Processing (Coming Soon)&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=153</id>
		<title>Current Release Features</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=153"/>
		<updated>2026-01-20T19:49:13Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Release Details ==&lt;br /&gt;
&#039;&#039;&#039;Current Release:&#039;&#039;&#039; 6.1.16.2&lt;br /&gt;
&lt;br /&gt;
=== Current Release Features: ===&lt;br /&gt;
Document Manager:&lt;br /&gt;
&lt;br /&gt;
* Advanced Scripting via new Business Layer.&lt;br /&gt;
&lt;br /&gt;
User Manager:&lt;br /&gt;
&lt;br /&gt;
* Additional Roles and Permissions available to the new Business Layer&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* Logo&lt;br /&gt;
* Postal and Physical Address for Documents&lt;br /&gt;
* Default Bank Account&lt;br /&gt;
* Advanced Invoice Settings&lt;br /&gt;
* Microsoft Graph Email Sending via Entra App.&lt;br /&gt;
* SMTP Settings&lt;br /&gt;
&lt;br /&gt;
Bank Manager:&lt;br /&gt;
&lt;br /&gt;
* Management of Bank Accounts&lt;br /&gt;
* Bank Account Import Settings&lt;br /&gt;
** ANZ&lt;br /&gt;
** BNZ&lt;br /&gt;
** Westpac&lt;br /&gt;
** ASB (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
** Kiwibank (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
Import File Admin&lt;br /&gt;
&lt;br /&gt;
* Generic CSV Import To allow for CSV/XLSX/JSON/XML file import to AP Invoice.&lt;br /&gt;
&lt;br /&gt;
Import PDF Admin (AI Enabled - Special License Required)&lt;br /&gt;
&lt;br /&gt;
* Reading of PDF AP Invoices Direct to AP Batch.&lt;br /&gt;
* Scripting to Control Every Aspect.&lt;br /&gt;
* Multi Page, Complex or Simple Invoices are handled quickly saving time on data import.&lt;br /&gt;
&lt;br /&gt;
Departments Admin&lt;br /&gt;
&lt;br /&gt;
* Management of Departments away from Zinform 5&lt;br /&gt;
* Additional Code Support to allow for up to 5 Codes&lt;br /&gt;
** Eg: Could be used to track Registration Number of a Vehicle to A Department, Trailers, ID Numbers etc for AP Invoices etc.&lt;br /&gt;
&lt;br /&gt;
=== Coming Soon: ===&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* IMAP Settings (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
CRM Module (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Email Processing (Coming Soon)&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_6_Software&amp;diff=152</id>
		<title>Zinform 6 Software</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_6_Software&amp;diff=152"/>
		<updated>2026-01-20T19:47:48Z</updated>

		<summary type="html">&lt;p&gt;Mike: /* Downloads */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Current Downloads ==&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccounts_6_1_16_2.exe Zinform 6 (6.1.16.2)]&lt;br /&gt;
&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccountsSetup_5.3.40.exe Zinform 5 (5.3.40)]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; These two versions rely on each other.  Once Z6.1.16.2 has been run it &#039;&#039;&#039;will require&#039;&#039;&#039; Z5.3.40 to operate correctly.&lt;br /&gt;
&lt;br /&gt;
== Previous Downloads ==&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccounts_6_1_14_1.exe Zinforrm 6 (6.1.14.1)]&lt;br /&gt;
&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccountsSetup_5.3.38.exe Zinform 5 (5.3.38)]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=151</id>
		<title>Zinform Wiki Home</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=151"/>
		<updated>2025-11-27T22:25:22Z</updated>

		<summary type="html">&lt;p&gt;Mike: /* Getting started */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== &amp;lt;strong&amp;gt;Zinform Main Help Page&amp;lt;/strong&amp;gt; ==&lt;br /&gt;
&lt;br /&gt;
== Getting started ==&lt;br /&gt;
[[Minimum System Requirements]]&lt;br /&gt;
&lt;br /&gt;
[[Guide to installing Zinform Accounts V6|Installation of Zinform Accounts]]&lt;br /&gt;
&lt;br /&gt;
[[Invoice Printing Direct via New Zinform]]&lt;br /&gt;
&lt;br /&gt;
[[Installing SQL and Zinform]]&lt;br /&gt;
&lt;br /&gt;
[[Entra Emailing Setup|Entra Setup for Emailing Direct]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Features ==&lt;br /&gt;
[[Current Release Features|List of features in current release]]&lt;br /&gt;
&lt;br /&gt;
[[Floating Views]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Scripting ==&lt;br /&gt;
[[Document Scripting|Document scripting]]&lt;br /&gt;
&lt;br /&gt;
[[Import File Scripting|File Import Scripting]]&lt;br /&gt;
&lt;br /&gt;
[[Import PDF File Scripting - AI Importing|PDF Import Scripting - Ai Importing]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Permissions and Roles ==&lt;br /&gt;
[[Permissions and Roles]]&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
Enable Debugging under Views and in Scripting, then with Visual Studio 2019 or new installed Debugging of Scripts will be available.&lt;br /&gt;
&lt;br /&gt;
== Downloads ==&lt;br /&gt;
[[Zinform 6 Software]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Entra_Emailing_Setup&amp;diff=150</id>
		<title>Entra Emailing Setup</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Entra_Emailing_Setup&amp;diff=150"/>
		<updated>2025-11-27T22:24:25Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== &#039;&#039;&#039;Setup Entra for Zinform 6 to Send Email&#039;&#039;&#039; ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Login to Entra portal: &amp;lt;nowiki&amp;gt;https://portal.azure.com/&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Quick link after logging in &amp;lt;nowiki&amp;gt;https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 1)     New Registration ===&lt;br /&gt;
[[File:Entra1.png]]&lt;br /&gt;
&lt;br /&gt;
=== 2)     Register Page ===&lt;br /&gt;
[[File:Entra2.png]]&lt;br /&gt;
&lt;br /&gt;
=== 3)     Record the Client and Tenant Id’s ===&lt;br /&gt;
[[File:Entra3.png]]&lt;br /&gt;
&lt;br /&gt;
=== 4)     Generate and record a Secret’s details (Value Only Shows Once) ===&lt;br /&gt;
a.     Give it a Description&lt;br /&gt;
&lt;br /&gt;
b.    Set the Expires appropriately for you&lt;br /&gt;
[[File:Entra4.png|none|thumb|589x589px]]&lt;br /&gt;
&lt;br /&gt;
=== 5)     Add Api Permissions ===&lt;br /&gt;
a.     User.Read (Should be there as default)&lt;br /&gt;
&lt;br /&gt;
b.    Microsoft Graph &amp;gt; Delegated &amp;gt; Mail.Send (For Send as a User)&lt;br /&gt;
&lt;br /&gt;
c.     Microsoft Graph &amp;gt; Application &amp;gt; Mail.Send (To allow the software to Send as any user Important!)&lt;br /&gt;
[[File:Entra5.png|none|thumb|637x637px]]&lt;br /&gt;
You will need Admin Consent for the Application Permission.&lt;br /&gt;
&lt;br /&gt;
Make sure “Grant admin consent for XXXXX” is clicked.&lt;br /&gt;
&lt;br /&gt;
Once saved that is all that is required.&lt;br /&gt;
&lt;br /&gt;
Zinform 6 will require: TenantId, ClientId and Secret for the software to send emails.  From there it can set up as a global sender i.e. accounts@exodesk.com or an individual user john.sample@exodesk.com.&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=File:Entra5.png&amp;diff=149</id>
		<title>File:Entra5.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=File:Entra5.png&amp;diff=149"/>
		<updated>2025-11-27T22:21:49Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Entra Images&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=File:Entra4.png&amp;diff=148</id>
		<title>File:Entra4.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=File:Entra4.png&amp;diff=148"/>
		<updated>2025-11-27T22:20:45Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Entra Images&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=File:Entra3.png&amp;diff=147</id>
		<title>File:Entra3.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=File:Entra3.png&amp;diff=147"/>
		<updated>2025-11-27T22:19:04Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Entra Images&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=File:Entra2.png&amp;diff=146</id>
		<title>File:Entra2.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=File:Entra2.png&amp;diff=146"/>
		<updated>2025-11-27T22:18:27Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Entra Images&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=File:Entra1.png&amp;diff=145</id>
		<title>File:Entra1.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=File:Entra1.png&amp;diff=145"/>
		<updated>2025-11-27T22:13:59Z</updated>

		<summary type="html">&lt;p&gt;Mike: Entra Images&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Entra Images&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Entra_Emailing_Setup&amp;diff=144</id>
		<title>Entra Emailing Setup</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Entra_Emailing_Setup&amp;diff=144"/>
		<updated>2025-11-27T22:11:52Z</updated>

		<summary type="html">&lt;p&gt;Mike: Created page with &amp;quot;&amp;#039;&amp;#039;&amp;#039;Setup Entra for Zinform 6 to Send Email&amp;#039;&amp;#039;&amp;#039;   Login to Entra portal: &amp;lt;nowiki&amp;gt;https://portal.azure.com/&amp;lt;/nowiki&amp;gt;  Quick link after logging in &amp;lt;nowiki&amp;gt;https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps&amp;lt;/nowiki&amp;gt;  1)     New Registration  2)     Register Page  3)     Record the Client and Tenant Id’s  4)     Generate and record a Secret’s details (Value Only Shows Once)  a.     Give it a Description  b.    Set the Exp...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Setup Entra for Zinform 6 to Send Email&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Login to Entra portal: &amp;lt;nowiki&amp;gt;https://portal.azure.com/&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Quick link after logging in &amp;lt;nowiki&amp;gt;https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1)     New Registration&lt;br /&gt;
&lt;br /&gt;
2)     Register Page&lt;br /&gt;
&lt;br /&gt;
3)     Record the Client and Tenant Id’s&lt;br /&gt;
&lt;br /&gt;
4)     Generate and record a Secret’s details (Value Only Shows Once)&lt;br /&gt;
&lt;br /&gt;
a.     Give it a Description&lt;br /&gt;
&lt;br /&gt;
b.    Set the Expires appropriately for you&lt;br /&gt;
&lt;br /&gt;
5)     Add Api Permissions&lt;br /&gt;
&lt;br /&gt;
a.     User.Read (Should be there as default)&lt;br /&gt;
&lt;br /&gt;
b.    Microsoft Graph &amp;gt; Delegated &amp;gt; Mail.Send (For Send as a User)&lt;br /&gt;
&lt;br /&gt;
c.     Microsoft Graph &amp;gt; Application &amp;gt; Mail.Send (To allow the software to Send as any user Important!)&lt;br /&gt;
&lt;br /&gt;
You will need Admin Consent for the Application Permission.&lt;br /&gt;
&lt;br /&gt;
Make sure “Grant admin consent for XXXXX” is clicked.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Once saved that is all that is required.&lt;br /&gt;
&lt;br /&gt;
We will require: TenantId, ClientId and Secret for the software to send emails.  From there we can set up as a global sender ie account@exodesk.com or an individual user john.sample@exodesk.com&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_6_Software&amp;diff=143</id>
		<title>Zinform 6 Software</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_6_Software&amp;diff=143"/>
		<updated>2025-11-11T23:09:08Z</updated>

		<summary type="html">&lt;p&gt;Mike: /* Downloads */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Downloads ==&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccounts_6_1_14_1.exe Zinforrm 6 (6.1.14.1)]&lt;br /&gt;
&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccountsSetup_5.3.38.exe Zinform 5 (5.3.38)]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=142</id>
		<title>Zinform Wiki Home</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=142"/>
		<updated>2025-11-11T23:01:23Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== &amp;lt;strong&amp;gt;Zinform Main Help Page&amp;lt;/strong&amp;gt; ==&lt;br /&gt;
&lt;br /&gt;
== Getting started ==&lt;br /&gt;
[[Minimum System Requirements]]&lt;br /&gt;
&lt;br /&gt;
[[Guide to installing Zinform Accounts V6|Installation of Zinform Accounts]]&lt;br /&gt;
&lt;br /&gt;
[[Invoice Printing Direct via New Zinform]]&lt;br /&gt;
&lt;br /&gt;
[[Installing SQL and Zinform]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Features ==&lt;br /&gt;
[[Current Release Features|List of features in current release]]&lt;br /&gt;
&lt;br /&gt;
[[Floating Views]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Scripting ==&lt;br /&gt;
[[Document Scripting|Document scripting]]&lt;br /&gt;
&lt;br /&gt;
[[Import File Scripting|File Import Scripting]]&lt;br /&gt;
&lt;br /&gt;
[[Import PDF File Scripting - AI Importing|PDF Import Scripting - Ai Importing]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Permissions and Roles ==&lt;br /&gt;
[[Permissions and Roles]]&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
Enable Debugging under Views and in Scripting, then with Visual Studio 2019 or new installed Debugging of Scripts will be available.&lt;br /&gt;
&lt;br /&gt;
== Downloads ==&lt;br /&gt;
[[Zinform 6 Software]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_6_Software&amp;diff=141</id>
		<title>Zinform 6 Software</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_6_Software&amp;diff=141"/>
		<updated>2025-11-11T23:00:21Z</updated>

		<summary type="html">&lt;p&gt;Mike: Created page with &amp;quot;== Downloads == [https://dl.zinform.co.nz/ZinformAccounts_6_1_14_1.exe Zinforrm 6 (6.1.14.1)]&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Downloads ==&lt;br /&gt;
[https://dl.zinform.co.nz/ZinformAccounts_6_1_14_1.exe Zinforrm 6 (6.1.14.1)]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=140</id>
		<title>Zinform Wiki Home</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=140"/>
		<updated>2025-11-10T03:53:59Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== &amp;lt;strong&amp;gt;Zinform Main Help Page&amp;lt;/strong&amp;gt; ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Getting started ==&lt;br /&gt;
[[Minimum System Requirements]]&lt;br /&gt;
&lt;br /&gt;
[[Guide to installing Zinform Accounts V6|Installation of Zinform Accounts]]&lt;br /&gt;
&lt;br /&gt;
[[Invoice Printing Direct via New Zinform]]&lt;br /&gt;
&lt;br /&gt;
[[Installing SQL and Zinform]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Features ==&lt;br /&gt;
[[Current Release Features|List of features in current release]]&lt;br /&gt;
&lt;br /&gt;
[[Floating Views]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Scripting ==&lt;br /&gt;
[[Document Scripting|Document scripting]]&lt;br /&gt;
&lt;br /&gt;
[[Import File Scripting|File Import Scripting]]&lt;br /&gt;
&lt;br /&gt;
[[Import PDF File Scripting - AI Importing|PDF Import Scripting - Ai Importing]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Permissions and Roles ==&lt;br /&gt;
[[Permissions and Roles]]&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
Enable Debugging under Views and in Scripting, then with Visual Studio 2019 or new installed Debugging of Scripts will be available.&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=139</id>
		<title>Document Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=139"/>
		<updated>2025-11-10T03:49:03Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Document Scripting =&lt;br /&gt;
&lt;br /&gt;
Document Scripting allows you to create custom documents in ZinformAccounts by writing C# code that processes templates and data. This guide covers the core functions and patterns you&#039;ll use.&lt;br /&gt;
&lt;br /&gt;
== Getting Started ==&lt;br /&gt;
&lt;br /&gt;
Every document script follows this basic structure:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
    &lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        // Your code here&lt;br /&gt;
        return true; // or false if error&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; Your class must be named &amp;lt;code&amp;gt;DocumentScripter&amp;lt;/code&amp;gt; and have a &amp;lt;code&amp;gt;Main&amp;lt;/code&amp;gt; method with these exact parameters.&lt;br /&gt;
&lt;br /&gt;
== Working with ZinWord Documents ==&lt;br /&gt;
&lt;br /&gt;
=== Creating and Loading Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Create a new document&lt;br /&gt;
ZinWord document = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
// Load from file&lt;br /&gt;
document.LoadDocument(documentFileName);&lt;br /&gt;
&lt;br /&gt;
// Load from stream&lt;br /&gt;
document.LoadStream(memoryStream);&lt;br /&gt;
&lt;br /&gt;
// Clone an existing document (useful when processing multiple records)&lt;br /&gt;
ZinWord clonedDoc = sourceDocument.Clone(true);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Saving Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Save to file - format determined by extension&lt;br /&gt;
document.SaveDocument(saveFileName);&lt;br /&gt;
&lt;br /&gt;
// Examples:&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.pdf&amp;quot;);   // Saves as PDF&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.docx&amp;quot;);  // Saves as Word document&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Checking Document Content ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Check if document has any content (text, tables, or images)&lt;br /&gt;
bool hasContent = zinBL.DocumentFunctions.DocumentHasContent(document);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Attaching Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Combine multiple documents into one (useful for bulk processing)&lt;br /&gt;
ZinWord bulkDocument = new ZinWord(zinBL);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument1);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument2);&lt;br /&gt;
bulkDocument.SaveDocument(&amp;quot;Bulk_Output.pdf&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
Bookmarks are placeholders in your Word templates that you replace with actual data.&lt;br /&gt;
&lt;br /&gt;
=== Setting Bookmark Values ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Set text value&lt;br /&gt;
document.SetBookmarkString(&amp;quot;BookmarkName&amp;quot;, &amp;quot;Value&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Set HTML content (for formatted text, tables, etc.)&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;BookmarkName&amp;quot;, htmlString);&lt;br /&gt;
&lt;br /&gt;
// Set hyperlink&lt;br /&gt;
document.SetBookmarkHyperLink(&amp;quot;BookmarkName&amp;quot;, hyperLinkItem);&lt;br /&gt;
&lt;br /&gt;
// Set image from file&lt;br /&gt;
document.SetBookmarkImageFromFile(&amp;quot;BookmarkName&amp;quot;, pictureFileName);&lt;br /&gt;
&lt;br /&gt;
// Set image from Picture object&lt;br /&gt;
document.SetBookmarkImage(&amp;quot;BookmarkName&amp;quot;, pictureItem);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Deleting Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Remove a bookmark and its content&lt;br /&gt;
document.DeleteBookmark(&amp;quot;BookmarkName&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Getting All Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Retrieve all bookmarks in the document&lt;br /&gt;
var bookmarks = document.GetBookmarks();&lt;br /&gt;
&lt;br /&gt;
foreach (var bookmark in bookmarks)&lt;br /&gt;
{&lt;br /&gt;
    // Process each bookmark&lt;br /&gt;
    string name = bookmark.Name;&lt;br /&gt;
    // ... do something with the bookmark&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Standard Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
ZinformAccounts provides automatic processing for common bookmarks:&lt;br /&gt;
&lt;br /&gt;
=== ProcessStandardBookmarks ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
    document,           // ZinWord document to process&lt;br /&gt;
    otherParty,         // ZOtherParty object (customer/supplier)&lt;br /&gt;
    parameters,         // ZinParameters with document settings&lt;br /&gt;
    zinBL,             // Business layer instance&lt;br /&gt;
    logoWidth,         // Logo width in pixels (e.g., 193)&lt;br /&gt;
    logoHeight,        // Logo height in pixels (e.g., 70)&lt;br /&gt;
    minAddressLines,   // Minimum address lines (default: 4)&lt;br /&gt;
    addresseeFirstLineAddress,  // Use first line as addressee (default: false)&lt;br /&gt;
    stateAsCity        // Treat state as city (NZ DB) (default: false)&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Standard Bookmarks:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationLogo&amp;lt;/code&amp;gt; - Your company logo&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationName&amp;lt;/code&amp;gt; - Your company name&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationAddress&amp;lt;/code&amp;gt; - Your postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationPhone&amp;lt;/code&amp;gt; - Your phone number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationEmail&amp;lt;/code&amp;gt; - Your email address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationWebsite&amp;lt;/code&amp;gt; - Your website URL&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccount&amp;lt;/code&amp;gt; - Bank account name and number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountNumber&amp;lt;/code&amp;gt; - Bank account number only&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountName&amp;lt;/code&amp;gt; - Bank account name only&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentDate&amp;lt;/code&amp;gt; - Current date (formatted as &amp;quot;dd MMMM yyyy&amp;quot;)&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentId&amp;lt;/code&amp;gt; - Document ID from parameters&lt;br /&gt;
* &amp;lt;code&amp;gt;GSTNumber&amp;lt;/code&amp;gt; - Tax registration number&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyId&amp;lt;/code&amp;gt; - Customer/Supplier ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyName&amp;lt;/code&amp;gt; - Customer/Supplier name&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyAddress&amp;lt;/code&amp;gt; - Customer/Supplier postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartySalutation&amp;lt;/code&amp;gt; - Greeting (from Administrator/Manager contact)&lt;br /&gt;
* &amp;lt;code&amp;gt;ReferenceId&amp;lt;/code&amp;gt; - Reference ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationNameFooter&amp;lt;/code&amp;gt; - Company name for footer&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For invoice-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== ProcessInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
Handles delivery address for invoices:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
zinBL.DocumentFunctions.ProcessInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, transaction,&lt;br /&gt;
    minAddressLines, stateAsCity, &amp;quot;DELIVERY&amp;quot;&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Bookmark:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;DeliverToDetails&amp;lt;/code&amp;gt; - Delivery address (deleted if no delivery address exists)&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraStatementBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For statement-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraStatementBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with Parameters ==&lt;br /&gt;
&lt;br /&gt;
Parameters pass data between the UI and your script.&lt;br /&gt;
&lt;br /&gt;
=== Getting Parameter Values ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get string value&lt;br /&gt;
string documentId = parameters.GetItemString(&amp;quot;DocumentId&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get datetime value&lt;br /&gt;
DateTime asAtDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get boolean value&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get output extension&lt;br /&gt;
string extension = parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;);&lt;br /&gt;
string filter = parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Setting Return Parameters ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Return the save location to the caller&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
&lt;br /&gt;
// Return error message if something fails&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== File Operations ==&lt;br /&gt;
&lt;br /&gt;
=== Save File Dialog ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
    document.DocumentOutputFolder,           // Default folder&lt;br /&gt;
    String.Format(&amp;quot;Statement_{0}&amp;quot;, DateTime.Now.ToString(&amp;quot;yyyyMMdd_hhmmss&amp;quot;)), // Default filename&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),       // Default extension (e.g., &amp;quot;pdf&amp;quot;)&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)  // File type filter&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
if (saveLocation != null &amp;amp;&amp;amp; saveLocation.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // User selected a location&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    // User cancelled - handle error&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Creating Directories ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
string folderPath = Path.Combine(baseFolderPath, $&amp;quot;OtherParty_{otherPartyId}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
if (!Directory.Exists(folderPath))&lt;br /&gt;
{&lt;br /&gt;
    Directory.CreateDirectory(folderPath);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
string saveFileName = Path.Combine(folderPath, &amp;quot;Statement.pdf&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with DataTables ==&lt;br /&gt;
&lt;br /&gt;
When processing multiple records (e.g., statements for multiple customers):&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Validate the DataTable parameter&lt;br /&gt;
DataTable otherPartyDataTable;&lt;br /&gt;
if (otherPartyDataTable != null &amp;amp;&amp;amp; otherPartyDataTable is DataTable)&lt;br /&gt;
{&lt;br /&gt;
    otherPartyDataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    throw new InvalidOperationException(&amp;quot;otherPartyDataTable is not a valid DataTable&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Loop through rows&lt;br /&gt;
foreach (DataRow row in otherPartyDataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    // Get values from the row&lt;br /&gt;
    string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
    &lt;br /&gt;
    // Load the full OtherParty object&lt;br /&gt;
    var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
    &lt;br /&gt;
    // Process document for this party&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Progress Bar ==&lt;br /&gt;
&lt;br /&gt;
Show progress when processing multiple records:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, totalCount, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        // Check for cancellation&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Update progress&lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {customerName}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work here...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Mark as finished&lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Financial Functions ==&lt;br /&gt;
&lt;br /&gt;
=== Statement Data ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get statement details (aged balances)&lt;br /&gt;
ZinStatementDetails details = zinBL.DocumentFunctions.GetStatementDetails(&lt;br /&gt;
    zinBL, &lt;br /&gt;
    otherParty, &lt;br /&gt;
    statementDate, &lt;br /&gt;
    DocumentTypes.ARStatement  // or DocumentTypes.APRemittance&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// Access aged balances&lt;br /&gt;
decimal current = details.CurrentBalance;&lt;br /&gt;
decimal oneMonth = details.OneMonthOverdue;&lt;br /&gt;
decimal twoMonths = details.TwoMonthsOverdue;&lt;br /&gt;
decimal threeMonths = details.ThreeMonthsOverdue;&lt;br /&gt;
decimal total = details.TotalDue;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Transaction Data ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get monthly running transactions (preferred method)&lt;br /&gt;
var (transactions, statementDetails) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
    statementDate, &lt;br /&gt;
    otherPartyId, &lt;br /&gt;
    zinBL&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// transactions is List&amp;lt;StatementTransaction2&amp;gt;&lt;br /&gt;
// statementDetails is ZinStatementDetails with aged balances&lt;br /&gt;
&lt;br /&gt;
// Process transactions&lt;br /&gt;
foreach (var tran in transactions)&lt;br /&gt;
{&lt;br /&gt;
    DateTime date = tran.Date;&lt;br /&gt;
    int type = tran.Type;&lt;br /&gt;
    string reference = tran.Reference;&lt;br /&gt;
    decimal value = tran.Value;&lt;br /&gt;
    decimal runningBalance = tran.RunningBalance;&lt;br /&gt;
    decimal subTotal = tran.SubTotal;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Combining Transaction Lists ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Combine and sort multiple transaction lists by date&lt;br /&gt;
var combined = zinBL.DocumentFunctions.CombineTransactions(listOne, listTwo);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Date Utilities ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get last day of month&lt;br /&gt;
DateTime monthEnd = DocumentFunctions.GetEndOfMonth(someDate);&lt;br /&gt;
// Example: 15/03/2024 returns 31/03/2024&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Building HTML Tables ==&lt;br /&gt;
&lt;br /&gt;
For inserting formatted tables into bookmarks:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
var htmlTable = new StringBuilder();&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%; border-collapse: collapse;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Date&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Reference&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Amount&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tbody&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
foreach (var item in dataList)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Amount:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tbody&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Insert into document&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;TableBookmark&amp;quot;, htmlTable.ToString());&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Tips for HTML Tables:&#039;&#039;&#039;&lt;br /&gt;
* Use inline styles (external CSS won&#039;t work)&lt;br /&gt;
* Specify &amp;lt;code&amp;gt;font-family: &amp;quot;Aptos&amp;quot;, sans-serif;&amp;lt;/code&amp;gt; for consistency&lt;br /&gt;
* Use &amp;lt;code&amp;gt;text-align: right&amp;lt;/code&amp;gt; for numbers&lt;br /&gt;
* Add padding for readability: &amp;lt;code&amp;gt;padding: 5px;&amp;lt;/code&amp;gt;&lt;br /&gt;
* Format currency: &amp;lt;code&amp;gt;{value:$#,##0.00}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Email Integration ==&lt;br /&gt;
&lt;br /&gt;
=== Sending Documents via Email ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get email addresses for specific contacts&lt;br /&gt;
var emailAddresses = otherParty.GetEmailContactsAsEmailCompatibleStringById(document.EmailContactIds);&lt;br /&gt;
&lt;br /&gt;
if (emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Replace placeholders in email body&lt;br /&gt;
    string emailBody = document.EmailBody.Replace(&amp;quot;[OtherPartyName]&amp;quot;, otherParty.Name);&lt;br /&gt;
    &lt;br /&gt;
    // Send email with attachment&lt;br /&gt;
    var result = zinBL.Emailing.SendEmailWithImage(&lt;br /&gt;
        emailAddresses,              // To addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // CC addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // BCC addresses&lt;br /&gt;
        document.EmailSubject,       // Subject&lt;br /&gt;
        emailBody,                   // HTML body&lt;br /&gt;
        zinBL,                       // Business layer&lt;br /&gt;
        attachmentFilePath,          // Path to attachment&lt;br /&gt;
        false,                       // Is HTML&lt;br /&gt;
        70L,                         // Image quality&lt;br /&gt;
        444,                         // Max image width&lt;br /&gt;
        &amp;quot;unsubscribe@yourdomain.com&amp;quot; // Unsubscribe email&lt;br /&gt;
    );&lt;br /&gt;
    &lt;br /&gt;
    if (!result.success)&lt;br /&gt;
    {&lt;br /&gt;
        MessageBox.Show(result.errorMessage);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
try&lt;br /&gt;
{&lt;br /&gt;
    // Your code here&lt;br /&gt;
    &lt;br /&gt;
    if (somethingWentWrong)&lt;br /&gt;
    {&lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Clear error message here&amp;quot;);&lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
catch (Exception ex)&lt;br /&gt;
{&lt;br /&gt;
    MessageBox.Show($&amp;quot;An error occurred: {ex.Message}\n{ex.StackTrace}&amp;quot;, &amp;quot;Error&amp;quot;);&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, ex.Message);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return true;  // Success&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Memory Management ===&lt;br /&gt;
&lt;br /&gt;
When processing many documents, clean up:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    var clonedDoc = sourceDoc.Clone(true);&lt;br /&gt;
    &lt;br /&gt;
    // Process document...&lt;br /&gt;
    &lt;br /&gt;
    // If not saving this document, explicitly dispose&lt;br /&gt;
    clonedDoc = null;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Conditional Processing ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Only add to bulk document if not emailed&lt;br /&gt;
bool wasEmailed = false;&lt;br /&gt;
&lt;br /&gt;
if (document.EmailIfPossible &amp;amp;&amp;amp; emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Send email&lt;br /&gt;
    wasEmailed = true;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
if (!wasEmailed)&lt;br /&gt;
{&lt;br /&gt;
    bulkDocument.AttachDocument(processedDoc);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Common Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Individual vs Bulk Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
foreach (var otherParty in otherParties)&lt;br /&gt;
{&lt;br /&gt;
    var doc = ProcessDocument(otherParty);&lt;br /&gt;
    &lt;br /&gt;
    if (individual)&lt;br /&gt;
    {&lt;br /&gt;
        // Save individual file&lt;br /&gt;
        string fileName = $&amp;quot;{folderPath}/Document_{otherParty.OtherPartyId}.pdf&amp;quot;;&lt;br /&gt;
        doc.SaveDocument(fileName);&lt;br /&gt;
    }&lt;br /&gt;
    else&lt;br /&gt;
    {&lt;br /&gt;
        // Add to bulk document&lt;br /&gt;
        bulkDoc.AttachDocument(doc);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Save bulk document if not individual&lt;br /&gt;
if (!individual &amp;amp;&amp;amp; zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
{&lt;br /&gt;
    bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Processing with Progress ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Documents&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {currentItem}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Empty Row Padding ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
int minLines = 13;&lt;br /&gt;
int currentLines = dataList.Count;&lt;br /&gt;
int emptyLines = Math.Max(0, minLines - currentLines);&lt;br /&gt;
&lt;br /&gt;
for (int i = 0; i &amp;lt; emptyLines; i++)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr style=&#039;height: 12px;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Add this using statement at top of your script:&lt;br /&gt;
using ZinformAccounts.Debugger;&lt;br /&gt;
&lt;br /&gt;
// Launch debugger at any point&lt;br /&gt;
DebuggerExtensions.LaunchNow(&amp;quot;Starting Main method&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Show debug information&lt;br /&gt;
MessageBox.Show($&amp;quot;Debug: {variableName}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Complete Example ==&lt;br /&gt;
&lt;br /&gt;
Here&#039;s a simplified but complete statement script:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using System;&lt;br /&gt;
using System.Text;&lt;br /&gt;
using System.Windows;&lt;br /&gt;
using System.Data;&lt;br /&gt;
using ZinBusinessLayer;&lt;br /&gt;
using ZinBusinessLayer.HelperClasses;&lt;br /&gt;
using ZinBusinessLayer.Enums;&lt;br /&gt;
using ZinBusinessLayer.Word;&lt;br /&gt;
&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
&lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        &lt;br /&gt;
        if (!(documentTemplate is ZinWord sourceDoc))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Invalid document template&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        document = zinBL.DocumentFunctions.GetDocument(parameters.GetItemString(&amp;quot;DocumentId&amp;quot;));&lt;br /&gt;
        DateTime statementDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
            document.DocumentOutputFolder,&lt;br /&gt;
            $&amp;quot;Statement_{DateTime.Now:yyyyMMdd_hhmmss}&amp;quot;,&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)&lt;br /&gt;
        );&lt;br /&gt;
        &lt;br /&gt;
        if (string.IsNullOrEmpty(saveLocation))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No save location chosen&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        DataTable dataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
        ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
        &lt;br /&gt;
        ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
        {&lt;br /&gt;
            int count = 1;&lt;br /&gt;
            &lt;br /&gt;
            foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
            {&lt;br /&gt;
                if (progressBar.CancellationPending) break;&lt;br /&gt;
                &lt;br /&gt;
                string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
                var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
                &lt;br /&gt;
                progressBar.ReportProgress(count, $&amp;quot;Processing: {otherParty.Name}&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                var doc = sourceDoc.Clone(true);&lt;br /&gt;
                &lt;br /&gt;
                // Process bookmarks&lt;br /&gt;
                zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
                    doc, otherParty, parameters, zinBL, 193, 70, 5&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Get statement data&lt;br /&gt;
                var (transactions, details) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
                    statementDate, otherPartyId, zinBL&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Build transaction table&lt;br /&gt;
                var html = new StringBuilder();&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
                foreach (var tran in transactions)&lt;br /&gt;
                {&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td style=&#039;text-align: right;&#039;&amp;gt;{tran.Value:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                doc.SetBookmarkHtml(&amp;quot;Transactions&amp;quot;, html.ToString());&lt;br /&gt;
                doc.SetBookmarkString(&amp;quot;TotalDue&amp;quot;, details.TotalDue.ToString(&amp;quot;#,##0.00&amp;quot;));&lt;br /&gt;
                &lt;br /&gt;
                bulkDoc.AttachDocument(doc);&lt;br /&gt;
                count++;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            ProgressBarManager.Finished();&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        if (zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
        {&lt;br /&gt;
            bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Problem !! Solution&lt;br /&gt;
|-&lt;br /&gt;
| Bookmark not found error || Check spelling - bookmarks are case-sensitive&lt;br /&gt;
|-&lt;br /&gt;
| HTML not rendering properly || Use inline styles only, no external CSS&lt;br /&gt;
|-&lt;br /&gt;
| Progress bar not showing || Ensure you call &amp;lt;code&amp;gt;ProgressBarManager.Finished()&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Document won&#039;t save || Check file path and ensure extension matches format&lt;br /&gt;
|-&lt;br /&gt;
| Changes not appearing || Make sure you&#039;re modifying the correct document instance (not the template)&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=138</id>
		<title>Document Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=138"/>
		<updated>2025-11-10T03:47:28Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Document Scripting =&lt;br /&gt;
&lt;br /&gt;
Document Scripting allows you to create custom documents in ZinformAccounts by writing C# code that processes templates and data. This guide covers the core functions and patterns you&#039;ll use.&lt;br /&gt;
&lt;br /&gt;
== Getting Started ==&lt;br /&gt;
&lt;br /&gt;
Every document script follows this basic structure:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	public class DocumentScripter&lt;br /&gt;
	{&lt;br /&gt;
		public static BusinessLayerMain zinBL;&lt;br /&gt;
		public static Document document;&lt;br /&gt;
		&lt;br /&gt;
		public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
						 ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
		{&lt;br /&gt;
			zinBL = bl;&lt;br /&gt;
			// Your code here&lt;br /&gt;
			return true; // or false if error&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; Your class must be named &amp;lt;code&amp;gt;DocumentScripter&amp;lt;/code&amp;gt; and have a &amp;lt;code&amp;gt;Main&amp;lt;/code&amp;gt; method with these exact parameters.&lt;br /&gt;
&lt;br /&gt;
== Working with ZinWord Documents ==&lt;br /&gt;
&lt;br /&gt;
=== Creating and Loading Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Create a new document&lt;br /&gt;
	ZinWord document = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
// Load from file&lt;br /&gt;
	document.LoadDocument(documentFileName);&lt;br /&gt;
&lt;br /&gt;
// Load from stream&lt;br /&gt;
	document.LoadStream(memoryStream);&lt;br /&gt;
&lt;br /&gt;
// Clone an existing document (useful when processing multiple records)&lt;br /&gt;
ZinWord clonedDoc = sourceDocument.Clone(true);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Saving Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Save to file - format determined by extension&lt;br /&gt;
		document.SaveDocument(saveFileName);&lt;br /&gt;
&lt;br /&gt;
// Examples:&lt;br /&gt;
		document.SaveDocument(&amp;quot;MyFile.pdf&amp;quot;);   // Saves as PDF&lt;br /&gt;
		document.SaveDocument(&amp;quot;MyFile.docx&amp;quot;);  // Saves as Word document&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Checking Document Content ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Check if document has any content (text, tables, or images)&lt;br /&gt;
	bool hasContent = zinBL.DocumentFunctions.DocumentHasContent(document);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Attaching Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Combine multiple documents into one (useful for bulk processing)&lt;br /&gt;
	ZinWord bulkDocument = new ZinWord(zinBL);&lt;br /&gt;
	bulkDocument.AttachDocument(sourceDocument1);&lt;br /&gt;
	bulkDocument.AttachDocument(sourceDocument2);&lt;br /&gt;
	bulkDocument.SaveDocument(&amp;quot;Bulk_Output.pdf&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
Bookmarks are placeholders in your Word templates that you replace with actual data.&lt;br /&gt;
&lt;br /&gt;
=== Setting Bookmark Values ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Set text value&lt;br /&gt;
	document.SetBookmarkString(&amp;quot;BookmarkName&amp;quot;, &amp;quot;Value&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Set HTML content (for formatted text, tables, etc.)&lt;br /&gt;
	document.SetBookmarkHtml(&amp;quot;BookmarkName&amp;quot;, htmlString);&lt;br /&gt;
&lt;br /&gt;
// Set hyperlink&lt;br /&gt;
	document.SetBookmarkHyperLink(&amp;quot;BookmarkName&amp;quot;, hyperLinkItem);&lt;br /&gt;
&lt;br /&gt;
// Set image from file&lt;br /&gt;
	document.SetBookmarkImageFromFile(&amp;quot;BookmarkName&amp;quot;, pictureFileName);&lt;br /&gt;
&lt;br /&gt;
// Set image from Picture object&lt;br /&gt;
	document.SetBookmarkImage(&amp;quot;BookmarkName&amp;quot;, pictureItem);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Deleting Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Remove a bookmark and its content&lt;br /&gt;
	document.DeleteBookmark(&amp;quot;BookmarkName&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Getting All Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Retrieve all bookmarks in the document&lt;br /&gt;
	var bookmarks = 	document.GetBookmarks();&lt;br /&gt;
&lt;br /&gt;
	foreach (var bookmark in bookmarks)&lt;br /&gt;
	{&lt;br /&gt;
		// Process each bookmark&lt;br /&gt;
		string name = bookmark.Name;&lt;br /&gt;
		// ... do something with the bookmark&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Standard Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
	ZinformAccounts provides automatic processing for common bookmarks:&lt;br /&gt;
&lt;br /&gt;
=== ProcessStandardBookmarks ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
		document,           // ZinWord document to process&lt;br /&gt;
		otherParty,         // ZOtherParty object (customer/supplier)&lt;br /&gt;
		parameters,         // ZinParameters with document settings&lt;br /&gt;
		zinBL,             // Business layer instance&lt;br /&gt;
		logoWidth,         // Logo width in pixels (e.g., 193)&lt;br /&gt;
		logoHeight,        // Logo height in pixels (e.g., 70)&lt;br /&gt;
		minAddressLines,   // Minimum address lines (default: 4)&lt;br /&gt;
		addresseeFirstLineAddress,  // Use first line as addressee (default: false)&lt;br /&gt;
		stateAsCity        // Treat state as city (NZ DB) (default: false)&lt;br /&gt;
	);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Standard Bookmarks:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationLogo&amp;lt;/code&amp;gt; - Your company logo&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationName&amp;lt;/code&amp;gt; - Your company name&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationAddress&amp;lt;/code&amp;gt; - Your postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationPhone&amp;lt;/code&amp;gt; - Your phone number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationEmail&amp;lt;/code&amp;gt; - Your email address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationWebsite&amp;lt;/code&amp;gt; - Your website URL&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccount&amp;lt;/code&amp;gt; - Bank account name and number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountNumber&amp;lt;/code&amp;gt; - Bank account number only&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountName&amp;lt;/code&amp;gt; - Bank account name only&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentDate&amp;lt;/code&amp;gt; - Current date (formatted as &amp;quot;dd MMMM yyyy&amp;quot;)&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentId&amp;lt;/code&amp;gt; - Document ID from parameters&lt;br /&gt;
* &amp;lt;code&amp;gt;GSTNumber&amp;lt;/code&amp;gt; - Tax registration number&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyId&amp;lt;/code&amp;gt; - Customer/Supplier ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyName&amp;lt;/code&amp;gt; - Customer/Supplier name&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyAddress&amp;lt;/code&amp;gt; - Customer/Supplier postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartySalutation&amp;lt;/code&amp;gt; - Greeting (from Administrator/Manager contact)&lt;br /&gt;
* &amp;lt;code&amp;gt;ReferenceId&amp;lt;/code&amp;gt; - Reference ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationNameFooter&amp;lt;/code&amp;gt; - Company name for footer&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For invoice-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	zinBL.DocumentFunctions.ProcessExtraInvoiceBookmarks(&lt;br /&gt;
		document, otherParty, parameters, zinBL, &lt;br /&gt;
		logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
		addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
	);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== ProcessInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
Handles delivery address for invoices:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	zinBL.DocumentFunctions.ProcessInvoiceBookmarks(&lt;br /&gt;
		document, otherParty, parameters, zinBL, transaction,&lt;br /&gt;
		minAddressLines, stateAsCity, &amp;quot;DELIVERY&amp;quot;&lt;br /&gt;
	);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Bookmark:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;DeliverToDetails&amp;lt;/code&amp;gt; - Delivery address (deleted if no delivery address exists)&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraStatementBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For statement-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	zinBL.DocumentFunctions.ProcessExtraStatementBookmarks(&lt;br /&gt;
		document, otherParty, parameters, zinBL, &lt;br /&gt;
		logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
		addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
	);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with Parameters ==&lt;br /&gt;
&lt;br /&gt;
Parameters pass data between the UI and your script.&lt;br /&gt;
&lt;br /&gt;
=== Getting Parameter Values ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get string value&lt;br /&gt;
	string documentId = parameters.GetItemString(&amp;quot;DocumentId&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get datetime value&lt;br /&gt;
	DateTime asAtDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get boolean value&lt;br /&gt;
	bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get output extension&lt;br /&gt;
	string extension = parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;);&lt;br /&gt;
	string filter = parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Setting Return Parameters ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Return the save location to the caller&lt;br /&gt;
	returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
&lt;br /&gt;
// Return error message if something fails&lt;br /&gt;
	returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== File Operations ==&lt;br /&gt;
&lt;br /&gt;
=== Save File Dialog ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
			document.DocumentOutputFolder,           // Default folder&lt;br /&gt;
		String.Format(&amp;quot;Statement_{0}&amp;quot;, DateTime.Now.ToString(&amp;quot;yyyyMMdd_hhmmss&amp;quot;)), // Default filename&lt;br /&gt;
		parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),       // Default extension (e.g., &amp;quot;pdf&amp;quot;)&lt;br /&gt;
		parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)  // File type filter&lt;br /&gt;
	);&lt;br /&gt;
&lt;br /&gt;
	if (saveLocation != null &amp;amp;&amp;amp; saveLocation.Length &amp;gt; 0)&lt;br /&gt;
	{&lt;br /&gt;
		// User selected a location&lt;br /&gt;
	}&lt;br /&gt;
	else&lt;br /&gt;
	{&lt;br /&gt;
		// User cancelled - handle error&lt;br /&gt;
		returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
		return false;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Creating Directories ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	string folderPath = Path.Combine(baseFolderPath, $&amp;quot;OtherParty_{otherPartyId}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
	if (!Directory.Exists(folderPath))&lt;br /&gt;
	{&lt;br /&gt;
		Directory.CreateDirectory(folderPath);&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	string saveFileName = Path.Combine(folderPath, &amp;quot;Statement.pdf&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with DataTables ==&lt;br /&gt;
&lt;br /&gt;
When processing multiple records (e.g., statements for multiple customers):&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Validate the DataTable parameter&lt;br /&gt;
	DataTable otherPartyDataTable;&lt;br /&gt;
	if (otherPartyDataTable != null &amp;amp;&amp;amp; otherPartyDataTable is DataTable)&lt;br /&gt;
	{&lt;br /&gt;
		otherPartyDataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
	}&lt;br /&gt;
	else&lt;br /&gt;
	{&lt;br /&gt;
		throw new InvalidOperationException(&amp;quot;otherPartyDataTable is not a valid DataTable&amp;quot;);&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
// Loop through rows&lt;br /&gt;
	foreach (DataRow row in otherPartyDataTable.Rows)&lt;br /&gt;
	{&lt;br /&gt;
		// Get values from the row&lt;br /&gt;
		string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
		&lt;br /&gt;
		// Load the full OtherParty object&lt;br /&gt;
		var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
		&lt;br /&gt;
		// Process document for this party&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Progress Bar ==&lt;br /&gt;
&lt;br /&gt;
Show progress when processing multiple records:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, totalCount, progressBar =&amp;gt;&lt;br /&gt;
	{&lt;br /&gt;
		int currentCount = 1;&lt;br /&gt;
		&lt;br /&gt;
		foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
		{&lt;br /&gt;
			// Check for cancellation&lt;br /&gt;
			if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
			{&lt;br /&gt;
				break;&lt;br /&gt;
			}&lt;br /&gt;
			&lt;br /&gt;
			// Update progress&lt;br /&gt;
			progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {customerName}&amp;quot;);&lt;br /&gt;
			&lt;br /&gt;
			// Do work here...&lt;br /&gt;
			&lt;br /&gt;
			currentCount++;&lt;br /&gt;
		}&lt;br /&gt;
		&lt;br /&gt;
		// Mark as finished&lt;br /&gt;
		ProgressBarManager.Finished();&lt;br /&gt;
	});&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Financial Functions ==&lt;br /&gt;
&lt;br /&gt;
=== Statement Data ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get statement details (aged balances)&lt;br /&gt;
	ZinStatementDetails details = zinBL.DocumentFunctions.GetStatementDetails(&lt;br /&gt;
		zinBL, &lt;br /&gt;
		otherParty, &lt;br /&gt;
		statementDate, &lt;br /&gt;
		DocumentTypes.ARStatement  // or DocumentTypes.APRemittance&lt;br /&gt;
	);&lt;br /&gt;
&lt;br /&gt;
// Access aged balances&lt;br /&gt;
	decimal current = details.CurrentBalance;&lt;br /&gt;
	decimal oneMonth = details.OneMonthOverdue;&lt;br /&gt;
	decimal twoMonths = details.TwoMonthsOverdue;&lt;br /&gt;
	decimal threeMonths = details.ThreeMonthsOverdue;&lt;br /&gt;
	decimal total = details.TotalDue;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Transaction Data ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get monthly running transactions (preferred method)&lt;br /&gt;
	var (transactions, statementDetails) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
		statementDate, &lt;br /&gt;
		otherPartyId, &lt;br /&gt;
		zinBL&lt;br /&gt;
	);&lt;br /&gt;
&lt;br /&gt;
// transactions is List&amp;lt;StatementTransaction2&amp;gt;&lt;br /&gt;
// statementDetails is ZinStatementDetails with aged balances&lt;br /&gt;
&lt;br /&gt;
// Process transactions&lt;br /&gt;
	foreach (var tran in transactions)&lt;br /&gt;
	{&lt;br /&gt;
		DateTime date = tran.Date;&lt;br /&gt;
		int type = tran.Type;&lt;br /&gt;
		string reference = tran.Reference;&lt;br /&gt;
		decimal value = tran.Value;&lt;br /&gt;
		decimal runningBalance = tran.RunningBalance;&lt;br /&gt;
		decimal subTotal = tran.SubTotal;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Combining Transaction Lists ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Combine and sort multiple transaction lists by date&lt;br /&gt;
	var combined = zinBL.DocumentFunctions.CombineTransactions(listOne, listTwo);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Date Utilities ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get last day of month&lt;br /&gt;
	DateTime monthEnd = DocumentFunctions.GetEndOfMonth(someDate);&lt;br /&gt;
// Example: 15/03/2024 returns 31/03/2024&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Building HTML Tables ==&lt;br /&gt;
&lt;br /&gt;
For inserting formatted tables into bookmarks:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	var htmlTable = new StringBuilder();&lt;br /&gt;
&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%; border-collapse: collapse;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;thead&amp;gt;&amp;quot;);&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Date&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Reference&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Amount&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;/thead&amp;gt;&amp;quot;);&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;tbody&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
	foreach (var item in dataList)&lt;br /&gt;
	{&lt;br /&gt;
		htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
		htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
		htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
		htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Amount:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
		htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;/tbody&amp;gt;&amp;quot;);&lt;br /&gt;
	htmlTable.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Insert into document&lt;br /&gt;
	document.SetBookmarkHtml(&amp;quot;TableBookmark&amp;quot;, htmlTable.ToString());&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Tips for HTML Tables:&#039;&#039;&#039;&lt;br /&gt;
* Use inline styles (external CSS won&#039;t work)&lt;br /&gt;
* Specify &amp;lt;code&amp;gt;font-family: &amp;quot;Aptos&amp;quot;, sans-serif;&amp;lt;/code&amp;gt; for consistency&lt;br /&gt;
* Use &amp;lt;code&amp;gt;text-align: right&amp;lt;/code&amp;gt; for numbers&lt;br /&gt;
* Add padding for readability: &amp;lt;code&amp;gt;padding: 5px;&amp;lt;/code&amp;gt;&lt;br /&gt;
* Format currency: &amp;lt;code&amp;gt;{value:$#,##0.00}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Email Integration ==&lt;br /&gt;
&lt;br /&gt;
=== Sending Documents via Email ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get email addresses for specific contacts&lt;br /&gt;
	var emailAddresses = otherParty.GetEmailContactsAsEmailCompatibleStringById(	document.EmailContactIds);&lt;br /&gt;
&lt;br /&gt;
	if (emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
	{&lt;br /&gt;
		// Replace placeholders in email body&lt;br /&gt;
		string emailBody = 	document.EmailBody.Replace(&amp;quot;[OtherPartyName]&amp;quot;, otherParty.Name);&lt;br /&gt;
		&lt;br /&gt;
		// Send email with attachment&lt;br /&gt;
		var result = zinBL.Emailing.SendEmailWithImage(&lt;br /&gt;
			emailAddresses,              // To addresses&lt;br /&gt;
			&amp;quot;&amp;quot;,                          // CC addresses&lt;br /&gt;
			&amp;quot;&amp;quot;,                          // BCC addresses&lt;br /&gt;
				document.EmailSubject,       // Subject&lt;br /&gt;
			emailBody,                   // HTML body&lt;br /&gt;
			zinBL,                       // Business layer&lt;br /&gt;
			attachmentFilePath,          // Path to attachment&lt;br /&gt;
			false,                       // Is HTML&lt;br /&gt;
			70L,                         // Image quality&lt;br /&gt;
			444,                         // Max image width&lt;br /&gt;
			&amp;quot;unsubscribe@yourdomain.com&amp;quot; // Unsubscribe email&lt;br /&gt;
		);&lt;br /&gt;
		&lt;br /&gt;
		if (!result.success)&lt;br /&gt;
		{&lt;br /&gt;
			MessageBox.Show(result.errorMessage);&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	try&lt;br /&gt;
	{&lt;br /&gt;
		// Your code here&lt;br /&gt;
		&lt;br /&gt;
		if (somethingWentWrong)&lt;br /&gt;
		{&lt;br /&gt;
			returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Clear error message here&amp;quot;);&lt;br /&gt;
			return false;&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
	catch (Exception ex)&lt;br /&gt;
	{&lt;br /&gt;
		MessageBox.Show($&amp;quot;An error occurred: {ex.Message}\n{ex.StackTrace}&amp;quot;, &amp;quot;Error&amp;quot;);&lt;br /&gt;
		returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, ex.Message);&lt;br /&gt;
		return false;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	return true;  // Success&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Memory Management ===&lt;br /&gt;
&lt;br /&gt;
When processing many documents, clean up:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
	{&lt;br /&gt;
		var clonedDoc = sourceDoc.Clone(true);&lt;br /&gt;
		&lt;br /&gt;
		// Process document...&lt;br /&gt;
		&lt;br /&gt;
		// If not saving this document, explicitly dispose&lt;br /&gt;
		clonedDoc = null;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Conditional Processing ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Only add to bulk document if not emailed&lt;br /&gt;
	bool wasEmailed = false;&lt;br /&gt;
&lt;br /&gt;
	if (	document.EmailIfPossible &amp;amp;&amp;amp; emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
	{&lt;br /&gt;
		// Send email&lt;br /&gt;
		wasEmailed = true;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	if (!wasEmailed)&lt;br /&gt;
	{&lt;br /&gt;
		bulkDocument.AttachDocument(processedDoc);&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Common Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Individual vs Bulk Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
	ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
	foreach (var otherParty in otherParties)&lt;br /&gt;
	{&lt;br /&gt;
		var doc = ProcessDocument(otherParty);&lt;br /&gt;
		&lt;br /&gt;
		if (individual)&lt;br /&gt;
		{&lt;br /&gt;
			// Save individual file&lt;br /&gt;
			string fileName = $&amp;quot;{folderPath}/Document_{otherParty.OtherPartyId}.pdf&amp;quot;;&lt;br /&gt;
			doc.SaveDocument(fileName);&lt;br /&gt;
		}&lt;br /&gt;
		else&lt;br /&gt;
		{&lt;br /&gt;
			// Add to bulk document&lt;br /&gt;
			bulkDoc.AttachDocument(doc);&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
// Save bulk document if not individual&lt;br /&gt;
	if (!individual &amp;amp;&amp;amp; zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
	{&lt;br /&gt;
		bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Processing with Progress ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	ProgressBarManager.Start(&amp;quot;Processing Documents&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
	{&lt;br /&gt;
		int currentCount = 1;&lt;br /&gt;
		&lt;br /&gt;
		foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
		{&lt;br /&gt;
			if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
			{&lt;br /&gt;
				break;&lt;br /&gt;
			}&lt;br /&gt;
			&lt;br /&gt;
			progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {currentItem}&amp;quot;);&lt;br /&gt;
			&lt;br /&gt;
			// Do work...&lt;br /&gt;
			&lt;br /&gt;
			currentCount++;&lt;br /&gt;
		}&lt;br /&gt;
		&lt;br /&gt;
		ProgressBarManager.Finished();&lt;br /&gt;
	});&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Empty Row Padding ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	int minLines = 13;&lt;br /&gt;
	int currentLines = dataList.Count;&lt;br /&gt;
	int emptyLines = Math.Max(0, minLines - currentLines);&lt;br /&gt;
&lt;br /&gt;
	for (int i = 0; i &amp;lt; emptyLines; i++)&lt;br /&gt;
	{&lt;br /&gt;
		htmlTable.AppendLine(&amp;quot;&amp;lt;tr style=&#039;height: 12px;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
		htmlTable.AppendLine(&amp;quot;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
		htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Add this using statement at top of your script:&lt;br /&gt;
	using ZinformAccounts.Debugger;&lt;br /&gt;
&lt;br /&gt;
// Launch debugger at any point&lt;br /&gt;
	DebuggerExtensions.LaunchNow(&amp;quot;Starting Main method&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Show debug information&lt;br /&gt;
	MessageBox.Show($&amp;quot;Debug: {variableName}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Complete Example ==&lt;br /&gt;
&lt;br /&gt;
Here&#039;s a simplified but complete statement script:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
	using System;&lt;br /&gt;
	using System.Text;&lt;br /&gt;
	using System.Windows;&lt;br /&gt;
	using System.Data;&lt;br /&gt;
	using ZinBusinessLayer;&lt;br /&gt;
	using ZinBusinessLayer.HelperClasses;&lt;br /&gt;
	using ZinBusinessLayer.Enums;&lt;br /&gt;
	using ZinBusinessLayer.Word;&lt;br /&gt;
&lt;br /&gt;
	public class DocumentScripter&lt;br /&gt;
	{&lt;br /&gt;
		public static BusinessLayerMain zinBL;&lt;br /&gt;
		public static Document document;&lt;br /&gt;
&lt;br /&gt;
		public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
						 ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
		{&lt;br /&gt;
			zinBL = bl;&lt;br /&gt;
			&lt;br /&gt;
			if (!(documentTemplate is ZinWord sourceDoc))&lt;br /&gt;
			{&lt;br /&gt;
				returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Invalid document template&amp;quot;);&lt;br /&gt;
				return false;&lt;br /&gt;
			}&lt;br /&gt;
			&lt;br /&gt;
			document = zinBL.DocumentFunctions.GetDocument(parameters.GetItemString(&amp;quot;DocumentId&amp;quot;));&lt;br /&gt;
			DateTime statementDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
			&lt;br /&gt;
			string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
					document.DocumentOutputFolder,&lt;br /&gt;
				$&amp;quot;Statement_{DateTime.Now:yyyyMMdd_hhmmss}&amp;quot;,&lt;br /&gt;
				parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),&lt;br /&gt;
				parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)&lt;br /&gt;
			);&lt;br /&gt;
			&lt;br /&gt;
			if (string.IsNullOrEmpty(saveLocation))&lt;br /&gt;
			{&lt;br /&gt;
				returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No save location chosen&amp;quot;);&lt;br /&gt;
				return false;&lt;br /&gt;
			}&lt;br /&gt;
			&lt;br /&gt;
			DataTable dataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
			ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
			&lt;br /&gt;
			ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
			{&lt;br /&gt;
				int count = 1;&lt;br /&gt;
				&lt;br /&gt;
				foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
				{&lt;br /&gt;
					if (progressBar.CancellationPending) break;&lt;br /&gt;
					&lt;br /&gt;
					string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
					var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
					&lt;br /&gt;
					progressBar.ReportProgress(count, $&amp;quot;Processing: {otherParty.Name}&amp;quot;);&lt;br /&gt;
					&lt;br /&gt;
					var doc = sourceDoc.Clone(true);&lt;br /&gt;
					&lt;br /&gt;
					// Process bookmarks&lt;br /&gt;
					zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
						doc, otherParty, parameters, zinBL, 193, 70, 5&lt;br /&gt;
					);&lt;br /&gt;
					&lt;br /&gt;
					// Get statement data&lt;br /&gt;
					var (transactions, details) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
						statementDate, otherPartyId, zinBL&lt;br /&gt;
					);&lt;br /&gt;
					&lt;br /&gt;
					// Build transaction table&lt;br /&gt;
					var html = new StringBuilder();&lt;br /&gt;
					html.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
					foreach (var tran in transactions)&lt;br /&gt;
					{&lt;br /&gt;
						html.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
						html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
						html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
						html.AppendLine($&amp;quot;&amp;lt;td style=&#039;text-align: right;&#039;&amp;gt;{tran.Value:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
						html.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
					}&lt;br /&gt;
					html.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
					&lt;br /&gt;
					doc.SetBookmarkHtml(&amp;quot;Transactions&amp;quot;, html.ToString());&lt;br /&gt;
					doc.SetBookmarkString(&amp;quot;TotalDue&amp;quot;, details.TotalDue.ToString(&amp;quot;#,##0.00&amp;quot;));&lt;br /&gt;
					&lt;br /&gt;
					bulkDoc.AttachDocument(doc);&lt;br /&gt;
					count++;&lt;br /&gt;
				}&lt;br /&gt;
				&lt;br /&gt;
				ProgressBarManager.Finished();&lt;br /&gt;
			});&lt;br /&gt;
			&lt;br /&gt;
			if (zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
			{&lt;br /&gt;
				bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
			}&lt;br /&gt;
			&lt;br /&gt;
			returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
			return true;&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Problem !! Solution&lt;br /&gt;
|-&lt;br /&gt;
| Bookmark not found error || Check spelling - bookmarks are case-sensitive&lt;br /&gt;
|-&lt;br /&gt;
| HTML not rendering properly || Use inline styles only, no external CSS&lt;br /&gt;
|-&lt;br /&gt;
| Progress bar not showing || Ensure you call &amp;lt;code&amp;gt;ProgressBarManager.Finished()&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Document won&#039;t save || Check file path and ensure extension matches format&lt;br /&gt;
|-&lt;br /&gt;
| Changes not appearing || Make sure you&#039;re modifying the correct document instance (not the template)&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=137</id>
		<title>Document Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=137"/>
		<updated>2025-11-10T03:39:42Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Document Scripting =&lt;br /&gt;
&lt;br /&gt;
Document Scripting allows you to create custom documents in ZinformAccounts by writing C# code that processes templates and data. This guide covers the core functions and patterns you&#039;ll use.&lt;br /&gt;
&lt;br /&gt;
== Getting Started ==&lt;br /&gt;
&lt;br /&gt;
Every document script follows this basic structure:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
    &lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        // Your code here&lt;br /&gt;
        return true; // or false if error&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; Your class must be named &amp;lt;code&amp;gt;DocumentScripter&amp;lt;/code&amp;gt; and have a &amp;lt;code&amp;gt;Main&amp;lt;/code&amp;gt; method with these exact parameters.&lt;br /&gt;
&lt;br /&gt;
== Working with ZinWord Documents ==&lt;br /&gt;
&lt;br /&gt;
=== Creating and Loading Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Create a new document&lt;br /&gt;
ZinWord document = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
// Load from file&lt;br /&gt;
document.LoadDocument(documentFileName);&lt;br /&gt;
&lt;br /&gt;
// Load from stream&lt;br /&gt;
document.LoadStream(memoryStream);&lt;br /&gt;
&lt;br /&gt;
// Clone an existing document (useful when processing multiple records)&lt;br /&gt;
ZinWord clonedDoc = sourceDocument.Clone(true);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Saving Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Save to file - format determined by extension&lt;br /&gt;
document.SaveDocument(saveFileName);&lt;br /&gt;
&lt;br /&gt;
// Examples:&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.pdf&amp;quot;);   // Saves as PDF&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.docx&amp;quot;);  // Saves as Word document&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Checking Document Content ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Check if document has any content (text, tables, or images)&lt;br /&gt;
bool hasContent = zinBL.DocumentFunctions.DocumentHasContent(document);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Attaching Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Combine multiple documents into one (useful for bulk processing)&lt;br /&gt;
ZinWord bulkDocument = new ZinWord(zinBL);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument1);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument2);&lt;br /&gt;
bulkDocument.SaveDocument(&amp;quot;Bulk_Output.pdf&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
Bookmarks are placeholders in your Word templates that you replace with actual data.&lt;br /&gt;
&lt;br /&gt;
=== Setting Bookmark Values ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Set text value&lt;br /&gt;
document.SetBookmarkString(&amp;quot;BookmarkName&amp;quot;, &amp;quot;Value&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Set HTML content (for formatted text, tables, etc.)&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;BookmarkName&amp;quot;, htmlString);&lt;br /&gt;
&lt;br /&gt;
// Set hyperlink&lt;br /&gt;
document.SetBookmarkHyperLink(&amp;quot;BookmarkName&amp;quot;, hyperLinkItem);&lt;br /&gt;
&lt;br /&gt;
// Set image from file&lt;br /&gt;
document.SetBookmarkImageFromFile(&amp;quot;BookmarkName&amp;quot;, pictureFileName);&lt;br /&gt;
&lt;br /&gt;
// Set image from Picture object&lt;br /&gt;
document.SetBookmarkImage(&amp;quot;BookmarkName&amp;quot;, pictureItem);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Deleting Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Remove a bookmark and its content&lt;br /&gt;
document.DeleteBookmark(&amp;quot;BookmarkName&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Getting All Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Retrieve all bookmarks in the document&lt;br /&gt;
var bookmarks = document.GetBookmarks();&lt;br /&gt;
&lt;br /&gt;
foreach (var bookmark in bookmarks)&lt;br /&gt;
{&lt;br /&gt;
    // Process each bookmark&lt;br /&gt;
    string name = bookmark.Name;&lt;br /&gt;
    // ... do something with the bookmark&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Standard Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
ZinformAccounts provides automatic processing for common bookmarks:&lt;br /&gt;
&lt;br /&gt;
=== ProcessStandardBookmarks ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
    document,           // ZinWord document to process&lt;br /&gt;
    otherParty,         // ZOtherParty object (customer/supplier)&lt;br /&gt;
    parameters,         // ZinParameters with document settings&lt;br /&gt;
    zinBL,             // Business layer instance&lt;br /&gt;
    logoWidth,         // Logo width in pixels (e.g., 193)&lt;br /&gt;
    logoHeight,        // Logo height in pixels (e.g., 70)&lt;br /&gt;
    minAddressLines,   // Minimum address lines (default: 4)&lt;br /&gt;
    addresseeFirstLineAddress,  // Use first line as addressee (default: false)&lt;br /&gt;
    stateAsCity        // Treat state as city (NZ DB) (default: false)&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Standard Bookmarks:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationLogo&amp;lt;/code&amp;gt; - Your company logo&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationName&amp;lt;/code&amp;gt; - Your company name&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationAddress&amp;lt;/code&amp;gt; - Your postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationPhone&amp;lt;/code&amp;gt; - Your phone number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationEmail&amp;lt;/code&amp;gt; - Your email address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationWebsite&amp;lt;/code&amp;gt; - Your website URL&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccount&amp;lt;/code&amp;gt; - Bank account name and number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountNumber&amp;lt;/code&amp;gt; - Bank account number only&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountName&amp;lt;/code&amp;gt; - Bank account name only&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentDate&amp;lt;/code&amp;gt; - Current date (formatted as &amp;quot;dd MMMM yyyy&amp;quot;)&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentId&amp;lt;/code&amp;gt; - Document ID from parameters&lt;br /&gt;
* &amp;lt;code&amp;gt;GSTNumber&amp;lt;/code&amp;gt; - Tax registration number&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyId&amp;lt;/code&amp;gt; - Customer/Supplier ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyName&amp;lt;/code&amp;gt; - Customer/Supplier name&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyAddress&amp;lt;/code&amp;gt; - Customer/Supplier postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartySalutation&amp;lt;/code&amp;gt; - Greeting (from Administrator/Manager contact)&lt;br /&gt;
* &amp;lt;code&amp;gt;ReferenceId&amp;lt;/code&amp;gt; - Reference ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationNameFooter&amp;lt;/code&amp;gt; - Company name for footer&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For invoice-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== ProcessInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
Handles delivery address for invoices:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
zinBL.DocumentFunctions.ProcessInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, transaction,&lt;br /&gt;
    minAddressLines, stateAsCity, &amp;quot;DELIVERY&amp;quot;&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Bookmark:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;DeliverToDetails&amp;lt;/code&amp;gt; - Delivery address (deleted if no delivery address exists)&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraStatementBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For statement-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraStatementBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with Parameters ==&lt;br /&gt;
&lt;br /&gt;
Parameters pass data between the UI and your script.&lt;br /&gt;
&lt;br /&gt;
=== Getting Parameter Values ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get string value&lt;br /&gt;
string documentId = parameters.GetItemString(&amp;quot;DocumentId&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get datetime value&lt;br /&gt;
DateTime asAtDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get boolean value&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get output extension&lt;br /&gt;
string extension = parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;);&lt;br /&gt;
string filter = parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Setting Return Parameters ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Return the save location to the caller&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
&lt;br /&gt;
// Return error message if something fails&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== File Operations ==&lt;br /&gt;
&lt;br /&gt;
=== Save File Dialog ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
    document.DocumentOutputFolder,           // Default folder&lt;br /&gt;
    String.Format(&amp;quot;Statement_{0}&amp;quot;, DateTime.Now.ToString(&amp;quot;yyyyMMdd_hhmmss&amp;quot;)), // Default filename&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),       // Default extension (e.g., &amp;quot;pdf&amp;quot;)&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)  // File type filter&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
if (saveLocation != null &amp;amp;&amp;amp; saveLocation.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // User selected a location&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    // User cancelled - handle error&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Creating Directories ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
string folderPath = Path.Combine(baseFolderPath, $&amp;quot;OtherParty_{otherPartyId}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
if (!Directory.Exists(folderPath))&lt;br /&gt;
{&lt;br /&gt;
    Directory.CreateDirectory(folderPath);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
string saveFileName = Path.Combine(folderPath, &amp;quot;Statement.pdf&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with DataTables ==&lt;br /&gt;
&lt;br /&gt;
When processing multiple records (e.g., statements for multiple customers):&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Validate the DataTable parameter&lt;br /&gt;
DataTable otherPartyDataTable;&lt;br /&gt;
if (otherPartyDataTable != null &amp;amp;&amp;amp; otherPartyDataTable is DataTable)&lt;br /&gt;
{&lt;br /&gt;
    otherPartyDataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    throw new InvalidOperationException(&amp;quot;otherPartyDataTable is not a valid DataTable&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Loop through rows&lt;br /&gt;
foreach (DataRow row in otherPartyDataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    // Get values from the row&lt;br /&gt;
    string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
    &lt;br /&gt;
    // Load the full OtherParty object&lt;br /&gt;
    var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
    &lt;br /&gt;
    // Process document for this party&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Progress Bar ==&lt;br /&gt;
&lt;br /&gt;
Show progress when processing multiple records:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, totalCount, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        // Check for cancellation&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Update progress&lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {customerName}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work here...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Mark as finished&lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Financial Functions ==&lt;br /&gt;
&lt;br /&gt;
=== Statement Data ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get statement details (aged balances)&lt;br /&gt;
ZinStatementDetails details = zinBL.DocumentFunctions.GetStatementDetails(&lt;br /&gt;
    zinBL, &lt;br /&gt;
    otherParty, &lt;br /&gt;
    statementDate, &lt;br /&gt;
    DocumentTypes.ARStatement  // or DocumentTypes.APRemittance&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// Access aged balances&lt;br /&gt;
decimal current = details.CurrentBalance;&lt;br /&gt;
decimal oneMonth = details.OneMonthOverdue;&lt;br /&gt;
decimal twoMonths = details.TwoMonthsOverdue;&lt;br /&gt;
decimal threeMonths = details.ThreeMonthsOverdue;&lt;br /&gt;
decimal total = details.TotalDue;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Transaction Data ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get monthly running transactions (preferred method)&lt;br /&gt;
var (transactions, statementDetails) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
    statementDate, &lt;br /&gt;
    otherPartyId, &lt;br /&gt;
    zinBL&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// transactions is List&amp;lt;StatementTransaction2&amp;gt;&lt;br /&gt;
// statementDetails is ZinStatementDetails with aged balances&lt;br /&gt;
&lt;br /&gt;
// Process transactions&lt;br /&gt;
foreach (var tran in transactions)&lt;br /&gt;
{&lt;br /&gt;
    DateTime date = tran.Date;&lt;br /&gt;
    int type = tran.Type;&lt;br /&gt;
    string reference = tran.Reference;&lt;br /&gt;
    decimal value = tran.Value;&lt;br /&gt;
    decimal runningBalance = tran.RunningBalance;&lt;br /&gt;
    decimal subTotal = tran.SubTotal;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Combining Transaction Lists ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Combine and sort multiple transaction lists by date&lt;br /&gt;
var combined = zinBL.DocumentFunctions.CombineTransactions(listOne, listTwo);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Date Utilities ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get last day of month&lt;br /&gt;
DateTime monthEnd = DocumentFunctions.GetEndOfMonth(someDate);&lt;br /&gt;
// Example: 15/03/2024 returns 31/03/2024&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Building HTML Tables ==&lt;br /&gt;
&lt;br /&gt;
For inserting formatted tables into bookmarks:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
var htmlTable = new StringBuilder();&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%; border-collapse: collapse;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Date&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Reference&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Amount&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tbody&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
foreach (var item in dataList)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Amount:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tbody&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Insert into document&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;TableBookmark&amp;quot;, htmlTable.ToString());&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Tips for HTML Tables:&#039;&#039;&#039;&lt;br /&gt;
* Use inline styles (external CSS won&#039;t work)&lt;br /&gt;
* Specify &amp;lt;code&amp;gt;font-family: &amp;quot;Aptos&amp;quot;, sans-serif;&amp;lt;/code&amp;gt; for consistency&lt;br /&gt;
* Use &amp;lt;code&amp;gt;text-align: right&amp;lt;/code&amp;gt; for numbers&lt;br /&gt;
* Add padding for readability: &amp;lt;code&amp;gt;padding: 5px;&amp;lt;/code&amp;gt;&lt;br /&gt;
* Format currency: &amp;lt;code&amp;gt;{value:$#,##0.00}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Email Integration ==&lt;br /&gt;
&lt;br /&gt;
=== Sending Documents via Email ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Get email addresses for specific contacts&lt;br /&gt;
var emailAddresses = otherParty.GetEmailContactsAsEmailCompatibleStringById(document.EmailContactIds);&lt;br /&gt;
&lt;br /&gt;
if (emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Replace placeholders in email body&lt;br /&gt;
    string emailBody = document.EmailBody.Replace(&amp;quot;[OtherPartyName]&amp;quot;, otherParty.Name);&lt;br /&gt;
    &lt;br /&gt;
    // Send email with attachment&lt;br /&gt;
    var result = zinBL.Emailing.SendEmailWithImage(&lt;br /&gt;
        emailAddresses,              // To addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // CC addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // BCC addresses&lt;br /&gt;
        document.EmailSubject,       // Subject&lt;br /&gt;
        emailBody,                   // HTML body&lt;br /&gt;
        zinBL,                       // Business layer&lt;br /&gt;
        attachmentFilePath,          // Path to attachment&lt;br /&gt;
        false,                       // Is HTML&lt;br /&gt;
        70L,                         // Image quality&lt;br /&gt;
        444,                         // Max image width&lt;br /&gt;
        &amp;quot;unsubscribe@yourdomain.com&amp;quot; // Unsubscribe email&lt;br /&gt;
    );&lt;br /&gt;
    &lt;br /&gt;
    if (!result.success)&lt;br /&gt;
    {&lt;br /&gt;
        MessageBox.Show(result.errorMessage);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
try&lt;br /&gt;
{&lt;br /&gt;
    // Your code here&lt;br /&gt;
    &lt;br /&gt;
    if (somethingWentWrong)&lt;br /&gt;
    {&lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Clear error message here&amp;quot;);&lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
catch (Exception ex)&lt;br /&gt;
{&lt;br /&gt;
    MessageBox.Show($&amp;quot;An error occurred: {ex.Message}\n{ex.StackTrace}&amp;quot;, &amp;quot;Error&amp;quot;);&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, ex.Message);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return true;  // Success&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Memory Management ===&lt;br /&gt;
&lt;br /&gt;
When processing many documents, clean up:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    var clonedDoc = sourceDoc.Clone(true);&lt;br /&gt;
    &lt;br /&gt;
    // Process document...&lt;br /&gt;
    &lt;br /&gt;
    // If not saving this document, explicitly dispose&lt;br /&gt;
    clonedDoc = null;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Conditional Processing ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Only add to bulk document if not emailed&lt;br /&gt;
bool wasEmailed = false;&lt;br /&gt;
&lt;br /&gt;
if (document.EmailIfPossible &amp;amp;&amp;amp; emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Send email&lt;br /&gt;
    wasEmailed = true;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
if (!wasEmailed)&lt;br /&gt;
{&lt;br /&gt;
    bulkDocument.AttachDocument(processedDoc);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Common Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Individual vs Bulk Documents ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
foreach (var otherParty in otherParties)&lt;br /&gt;
{&lt;br /&gt;
    var doc = ProcessDocument(otherParty);&lt;br /&gt;
    &lt;br /&gt;
    if (individual)&lt;br /&gt;
    {&lt;br /&gt;
        // Save individual file&lt;br /&gt;
        string fileName = $&amp;quot;{folderPath}/Document_{otherParty.OtherPartyId}.pdf&amp;quot;;&lt;br /&gt;
        doc.SaveDocument(fileName);&lt;br /&gt;
    }&lt;br /&gt;
    else&lt;br /&gt;
    {&lt;br /&gt;
        // Add to bulk document&lt;br /&gt;
        bulkDoc.AttachDocument(doc);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Save bulk document if not individual&lt;br /&gt;
if (!individual &amp;amp;&amp;amp; zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
{&lt;br /&gt;
    bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Processing with Progress ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Documents&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {currentItem}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Empty Row Padding ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
int minLines = 13;&lt;br /&gt;
int currentLines = dataList.Count;&lt;br /&gt;
int emptyLines = Math.Max(0, minLines - currentLines);&lt;br /&gt;
&lt;br /&gt;
for (int i = 0; i &amp;lt; emptyLines; i++)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr style=&#039;height: 12px;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Add this using statement at top of your script:&lt;br /&gt;
using ZinformAccounts.Debugger;&lt;br /&gt;
&lt;br /&gt;
// Launch debugger at any point&lt;br /&gt;
DebuggerExtensions.LaunchNow(&amp;quot;Starting Main method&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Show debug information&lt;br /&gt;
MessageBox.Show($&amp;quot;Debug: {variableName}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Complete Example ==&lt;br /&gt;
&lt;br /&gt;
Here&#039;s a simplified but complete statement script:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using System;&lt;br /&gt;
using System.Text;&lt;br /&gt;
using System.Windows;&lt;br /&gt;
using System.Data;&lt;br /&gt;
using ZinBusinessLayer;&lt;br /&gt;
using ZinBusinessLayer.HelperClasses;&lt;br /&gt;
using ZinBusinessLayer.Enums;&lt;br /&gt;
using ZinBusinessLayer.Word;&lt;br /&gt;
&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
&lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        &lt;br /&gt;
        if (!(documentTemplate is ZinWord sourceDoc))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Invalid document template&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        document = zinBL.DocumentFunctions.GetDocument(parameters.GetItemString(&amp;quot;DocumentId&amp;quot;));&lt;br /&gt;
        DateTime statementDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
            document.DocumentOutputFolder,&lt;br /&gt;
            $&amp;quot;Statement_{DateTime.Now:yyyyMMdd_hhmmss}&amp;quot;,&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)&lt;br /&gt;
        );&lt;br /&gt;
        &lt;br /&gt;
        if (string.IsNullOrEmpty(saveLocation))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No save location chosen&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        DataTable dataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
        ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
        &lt;br /&gt;
        ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
        {&lt;br /&gt;
            int count = 1;&lt;br /&gt;
            &lt;br /&gt;
            foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
            {&lt;br /&gt;
                if (progressBar.CancellationPending) break;&lt;br /&gt;
                &lt;br /&gt;
                string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
                var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
                &lt;br /&gt;
                progressBar.ReportProgress(count, $&amp;quot;Processing: {otherParty.Name}&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                var doc = sourceDoc.Clone(true);&lt;br /&gt;
                &lt;br /&gt;
                // Process bookmarks&lt;br /&gt;
                zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
                    doc, otherParty, parameters, zinBL, 193, 70, 5&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Get statement data&lt;br /&gt;
                var (transactions, details) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
                    statementDate, otherPartyId, zinBL&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Build transaction table&lt;br /&gt;
                var html = new StringBuilder();&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
                foreach (var tran in transactions)&lt;br /&gt;
                {&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td style=&#039;text-align: right;&#039;&amp;gt;{tran.Value:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                doc.SetBookmarkHtml(&amp;quot;Transactions&amp;quot;, html.ToString());&lt;br /&gt;
                doc.SetBookmarkString(&amp;quot;TotalDue&amp;quot;, details.TotalDue.ToString(&amp;quot;#,##0.00&amp;quot;));&lt;br /&gt;
                &lt;br /&gt;
                bulkDoc.AttachDocument(doc);&lt;br /&gt;
                count++;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            ProgressBarManager.Finished();&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        if (zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
        {&lt;br /&gt;
            bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Problem !! Solution&lt;br /&gt;
|-&lt;br /&gt;
| Bookmark not found error || Check spelling - bookmarks are case-sensitive&lt;br /&gt;
|-&lt;br /&gt;
| HTML not rendering properly || Use inline styles only, no external CSS&lt;br /&gt;
|-&lt;br /&gt;
| Progress bar not showing || Ensure you call &amp;lt;code&amp;gt;ProgressBarManager.Finished()&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Document won&#039;t save || Check file path and ensure extension matches format&lt;br /&gt;
|-&lt;br /&gt;
| Changes not appearing || Make sure you&#039;re modifying the correct document instance (not the template)&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=136</id>
		<title>Document Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=136"/>
		<updated>2025-11-10T03:39:21Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Document Scripting =&lt;br /&gt;
&lt;br /&gt;
Document Scripting allows you to create custom documents in ZinformAccounts by writing C# code that processes templates and data. This guide covers the core functions and patterns you&#039;ll use.&lt;br /&gt;
&lt;br /&gt;
== Getting Started ==&lt;br /&gt;
&lt;br /&gt;
Every document script follows this basic structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
    &lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        // Your code here&lt;br /&gt;
        return true; // or false if error&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; Your class must be named &amp;lt;code&amp;gt;DocumentScripter&amp;lt;/code&amp;gt; and have a &amp;lt;code&amp;gt;Main&amp;lt;/code&amp;gt; method with these exact parameters.&lt;br /&gt;
&lt;br /&gt;
== Working with ZinWord Documents ==&lt;br /&gt;
&lt;br /&gt;
=== Creating and Loading Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Create a new document&lt;br /&gt;
ZinWord document = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
// Load from file&lt;br /&gt;
document.LoadDocument(documentFileName);&lt;br /&gt;
&lt;br /&gt;
// Load from stream&lt;br /&gt;
document.LoadStream(memoryStream);&lt;br /&gt;
&lt;br /&gt;
// Clone an existing document (useful when processing multiple records)&lt;br /&gt;
ZinWord clonedDoc = sourceDocument.Clone(true);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Saving Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Save to file - format determined by extension&lt;br /&gt;
document.SaveDocument(saveFileName);&lt;br /&gt;
&lt;br /&gt;
// Examples:&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.pdf&amp;quot;);   // Saves as PDF&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.docx&amp;quot;);  // Saves as Word document&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Checking Document Content ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Check if document has any content (text, tables, or images)&lt;br /&gt;
bool hasContent = zinBL.DocumentFunctions.DocumentHasContent(document);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Attaching Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Combine multiple documents into one (useful for bulk processing)&lt;br /&gt;
ZinWord bulkDocument = new ZinWord(zinBL);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument1);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument2);&lt;br /&gt;
bulkDocument.SaveDocument(&amp;quot;Bulk_Output.pdf&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
Bookmarks are placeholders in your Word templates that you replace with actual data.&lt;br /&gt;
&lt;br /&gt;
=== Setting Bookmark Values ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Set text value&lt;br /&gt;
document.SetBookmarkString(&amp;quot;BookmarkName&amp;quot;, &amp;quot;Value&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Set HTML content (for formatted text, tables, etc.)&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;BookmarkName&amp;quot;, htmlString);&lt;br /&gt;
&lt;br /&gt;
// Set hyperlink&lt;br /&gt;
document.SetBookmarkHyperLink(&amp;quot;BookmarkName&amp;quot;, hyperLinkItem);&lt;br /&gt;
&lt;br /&gt;
// Set image from file&lt;br /&gt;
document.SetBookmarkImageFromFile(&amp;quot;BookmarkName&amp;quot;, pictureFileName);&lt;br /&gt;
&lt;br /&gt;
// Set image from Picture object&lt;br /&gt;
document.SetBookmarkImage(&amp;quot;BookmarkName&amp;quot;, pictureItem);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Deleting Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Remove a bookmark and its content&lt;br /&gt;
document.DeleteBookmark(&amp;quot;BookmarkName&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Getting All Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Retrieve all bookmarks in the document&lt;br /&gt;
var bookmarks = document.GetBookmarks();&lt;br /&gt;
&lt;br /&gt;
foreach (var bookmark in bookmarks)&lt;br /&gt;
{&lt;br /&gt;
    // Process each bookmark&lt;br /&gt;
    string name = bookmark.Name;&lt;br /&gt;
    // ... do something with the bookmark&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Standard Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
ZinformAccounts provides automatic processing for common bookmarks:&lt;br /&gt;
&lt;br /&gt;
=== ProcessStandardBookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
    document,           // ZinWord document to process&lt;br /&gt;
    otherParty,         // ZOtherParty object (customer/supplier)&lt;br /&gt;
    parameters,         // ZinParameters with document settings&lt;br /&gt;
    zinBL,             // Business layer instance&lt;br /&gt;
    logoWidth,         // Logo width in pixels (e.g., 193)&lt;br /&gt;
    logoHeight,        // Logo height in pixels (e.g., 70)&lt;br /&gt;
    minAddressLines,   // Minimum address lines (default: 4)&lt;br /&gt;
    addresseeFirstLineAddress,  // Use first line as addressee (default: false)&lt;br /&gt;
    stateAsCity        // Treat state as city (NZ DB) (default: false)&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Standard Bookmarks:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationLogo&amp;lt;/code&amp;gt; - Your company logo&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationName&amp;lt;/code&amp;gt; - Your company name&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationAddress&amp;lt;/code&amp;gt; - Your postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationPhone&amp;lt;/code&amp;gt; - Your phone number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationEmail&amp;lt;/code&amp;gt; - Your email address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationWebsite&amp;lt;/code&amp;gt; - Your website URL&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccount&amp;lt;/code&amp;gt; - Bank account name and number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountNumber&amp;lt;/code&amp;gt; - Bank account number only&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountName&amp;lt;/code&amp;gt; - Bank account name only&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentDate&amp;lt;/code&amp;gt; - Current date (formatted as &amp;quot;dd MMMM yyyy&amp;quot;)&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentId&amp;lt;/code&amp;gt; - Document ID from parameters&lt;br /&gt;
* &amp;lt;code&amp;gt;GSTNumber&amp;lt;/code&amp;gt; - Tax registration number&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyId&amp;lt;/code&amp;gt; - Customer/Supplier ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyName&amp;lt;/code&amp;gt; - Customer/Supplier name&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyAddress&amp;lt;/code&amp;gt; - Customer/Supplier postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartySalutation&amp;lt;/code&amp;gt; - Greeting (from Administrator/Manager contact)&lt;br /&gt;
* &amp;lt;code&amp;gt;ReferenceId&amp;lt;/code&amp;gt; - Reference ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationNameFooter&amp;lt;/code&amp;gt; - Company name for footer&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For invoice-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== ProcessInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
Handles delivery address for invoices:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, transaction,&lt;br /&gt;
    minAddressLines, stateAsCity, &amp;quot;DELIVERY&amp;quot;&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Bookmark:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;DeliverToDetails&amp;lt;/code&amp;gt; - Delivery address (deleted if no delivery address exists)&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraStatementBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For statement-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraStatementBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with Parameters ==&lt;br /&gt;
&lt;br /&gt;
Parameters pass data between the UI and your script.&lt;br /&gt;
&lt;br /&gt;
=== Getting Parameter Values ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get string value&lt;br /&gt;
string documentId = parameters.GetItemString(&amp;quot;DocumentId&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get datetime value&lt;br /&gt;
DateTime asAtDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get boolean value&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get output extension&lt;br /&gt;
string extension = parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;);&lt;br /&gt;
string filter = parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Setting Return Parameters ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Return the save location to the caller&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
&lt;br /&gt;
// Return error message if something fails&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== File Operations ==&lt;br /&gt;
&lt;br /&gt;
=== Save File Dialog ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
    document.DocumentOutputFolder,           // Default folder&lt;br /&gt;
    String.Format(&amp;quot;Statement_{0}&amp;quot;, DateTime.Now.ToString(&amp;quot;yyyyMMdd_hhmmss&amp;quot;)), // Default filename&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),       // Default extension (e.g., &amp;quot;pdf&amp;quot;)&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)  // File type filter&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
if (saveLocation != null &amp;amp;&amp;amp; saveLocation.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // User selected a location&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    // User cancelled - handle error&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Creating Directories ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
string folderPath = Path.Combine(baseFolderPath, $&amp;quot;OtherParty_{otherPartyId}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
if (!Directory.Exists(folderPath))&lt;br /&gt;
{&lt;br /&gt;
    Directory.CreateDirectory(folderPath);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
string saveFileName = Path.Combine(folderPath, &amp;quot;Statement.pdf&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Working with DataTables ==&lt;br /&gt;
&lt;br /&gt;
When processing multiple records (e.g., statements for multiple customers):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Validate the DataTable parameter&lt;br /&gt;
DataTable otherPartyDataTable;&lt;br /&gt;
if (otherPartyDataTable != null &amp;amp;&amp;amp; otherPartyDataTable is DataTable)&lt;br /&gt;
{&lt;br /&gt;
    otherPartyDataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    throw new InvalidOperationException(&amp;quot;otherPartyDataTable is not a valid DataTable&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Loop through rows&lt;br /&gt;
foreach (DataRow row in otherPartyDataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    // Get values from the row&lt;br /&gt;
    string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
    &lt;br /&gt;
    // Load the full OtherParty object&lt;br /&gt;
    var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
    &lt;br /&gt;
    // Process document for this party&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Progress Bar ==&lt;br /&gt;
&lt;br /&gt;
Show progress when processing multiple records:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, totalCount, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        // Check for cancellation&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Update progress&lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {customerName}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work here...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Mark as finished&lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Financial Functions ==&lt;br /&gt;
&lt;br /&gt;
=== Statement Data ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get statement details (aged balances)&lt;br /&gt;
ZinStatementDetails details = zinBL.DocumentFunctions.GetStatementDetails(&lt;br /&gt;
    zinBL, &lt;br /&gt;
    otherParty, &lt;br /&gt;
    statementDate, &lt;br /&gt;
    DocumentTypes.ARStatement  // or DocumentTypes.APRemittance&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// Access aged balances&lt;br /&gt;
decimal current = details.CurrentBalance;&lt;br /&gt;
decimal oneMonth = details.OneMonthOverdue;&lt;br /&gt;
decimal twoMonths = details.TwoMonthsOverdue;&lt;br /&gt;
decimal threeMonths = details.ThreeMonthsOverdue;&lt;br /&gt;
decimal total = details.TotalDue;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Transaction Data ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get monthly running transactions (preferred method)&lt;br /&gt;
var (transactions, statementDetails) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
    statementDate, &lt;br /&gt;
    otherPartyId, &lt;br /&gt;
    zinBL&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// transactions is List&amp;lt;StatementTransaction2&amp;gt;&lt;br /&gt;
// statementDetails is ZinStatementDetails with aged balances&lt;br /&gt;
&lt;br /&gt;
// Process transactions&lt;br /&gt;
foreach (var tran in transactions)&lt;br /&gt;
{&lt;br /&gt;
    DateTime date = tran.Date;&lt;br /&gt;
    int type = tran.Type;&lt;br /&gt;
    string reference = tran.Reference;&lt;br /&gt;
    decimal value = tran.Value;&lt;br /&gt;
    decimal runningBalance = tran.RunningBalance;&lt;br /&gt;
    decimal subTotal = tran.SubTotal;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Combining Transaction Lists ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Combine and sort multiple transaction lists by date&lt;br /&gt;
var combined = zinBL.DocumentFunctions.CombineTransactions(listOne, listTwo);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Date Utilities ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get last day of month&lt;br /&gt;
DateTime monthEnd = DocumentFunctions.GetEndOfMonth(someDate);&lt;br /&gt;
// Example: 15/03/2024 returns 31/03/2024&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Building HTML Tables ==&lt;br /&gt;
&lt;br /&gt;
For inserting formatted tables into bookmarks:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var htmlTable = new StringBuilder();&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%; border-collapse: collapse;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Date&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Reference&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Amount&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tbody&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
foreach (var item in dataList)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Amount:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tbody&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Insert into document&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;TableBookmark&amp;quot;, htmlTable.ToString());&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Tips for HTML Tables:&#039;&#039;&#039;&lt;br /&gt;
* Use inline styles (external CSS won&#039;t work)&lt;br /&gt;
* Specify &amp;lt;code&amp;gt;font-family: &amp;quot;Aptos&amp;quot;, sans-serif;&amp;lt;/code&amp;gt; for consistency&lt;br /&gt;
* Use &amp;lt;code&amp;gt;text-align: right&amp;lt;/code&amp;gt; for numbers&lt;br /&gt;
* Add padding for readability: &amp;lt;code&amp;gt;padding: 5px;&amp;lt;/code&amp;gt;&lt;br /&gt;
* Format currency: &amp;lt;code&amp;gt;{value:$#,##0.00}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Email Integration ==&lt;br /&gt;
&lt;br /&gt;
=== Sending Documents via Email ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get email addresses for specific contacts&lt;br /&gt;
var emailAddresses = otherParty.GetEmailContactsAsEmailCompatibleStringById(document.EmailContactIds);&lt;br /&gt;
&lt;br /&gt;
if (emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Replace placeholders in email body&lt;br /&gt;
    string emailBody = document.EmailBody.Replace(&amp;quot;[OtherPartyName]&amp;quot;, otherParty.Name);&lt;br /&gt;
    &lt;br /&gt;
    // Send email with attachment&lt;br /&gt;
    var result = zinBL.Emailing.SendEmailWithImage(&lt;br /&gt;
        emailAddresses,              // To addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // CC addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // BCC addresses&lt;br /&gt;
        document.EmailSubject,       // Subject&lt;br /&gt;
        emailBody,                   // HTML body&lt;br /&gt;
        zinBL,                       // Business layer&lt;br /&gt;
        attachmentFilePath,          // Path to attachment&lt;br /&gt;
        false,                       // Is HTML&lt;br /&gt;
        70L,                         // Image quality&lt;br /&gt;
        444,                         // Max image width&lt;br /&gt;
        &amp;quot;unsubscribe@yourdomain.com&amp;quot; // Unsubscribe email&lt;br /&gt;
    );&lt;br /&gt;
    &lt;br /&gt;
    if (!result.success)&lt;br /&gt;
    {&lt;br /&gt;
        MessageBox.Show(result.errorMessage);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
try&lt;br /&gt;
{&lt;br /&gt;
    // Your code here&lt;br /&gt;
    &lt;br /&gt;
    if (somethingWentWrong)&lt;br /&gt;
    {&lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Clear error message here&amp;quot;);&lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
catch (Exception ex)&lt;br /&gt;
{&lt;br /&gt;
    MessageBox.Show($&amp;quot;An error occurred: {ex.Message}\n{ex.StackTrace}&amp;quot;, &amp;quot;Error&amp;quot;);&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, ex.Message);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return true;  // Success&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Memory Management ===&lt;br /&gt;
&lt;br /&gt;
When processing many documents, clean up:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    var clonedDoc = sourceDoc.Clone(true);&lt;br /&gt;
    &lt;br /&gt;
    // Process document...&lt;br /&gt;
    &lt;br /&gt;
    // If not saving this document, explicitly dispose&lt;br /&gt;
    clonedDoc = null;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Conditional Processing ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Only add to bulk document if not emailed&lt;br /&gt;
bool wasEmailed = false;&lt;br /&gt;
&lt;br /&gt;
if (document.EmailIfPossible &amp;amp;&amp;amp; emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Send email&lt;br /&gt;
    wasEmailed = true;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
if (!wasEmailed)&lt;br /&gt;
{&lt;br /&gt;
    bulkDocument.AttachDocument(processedDoc);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Common Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Individual vs Bulk Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
foreach (var otherParty in otherParties)&lt;br /&gt;
{&lt;br /&gt;
    var doc = ProcessDocument(otherParty);&lt;br /&gt;
    &lt;br /&gt;
    if (individual)&lt;br /&gt;
    {&lt;br /&gt;
        // Save individual file&lt;br /&gt;
        string fileName = $&amp;quot;{folderPath}/Document_{otherParty.OtherPartyId}.pdf&amp;quot;;&lt;br /&gt;
        doc.SaveDocument(fileName);&lt;br /&gt;
    }&lt;br /&gt;
    else&lt;br /&gt;
    {&lt;br /&gt;
        // Add to bulk document&lt;br /&gt;
        bulkDoc.AttachDocument(doc);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Save bulk document if not individual&lt;br /&gt;
if (!individual &amp;amp;&amp;amp; zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
{&lt;br /&gt;
    bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Processing with Progress ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Documents&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {currentItem}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Empty Row Padding ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
int minLines = 13;&lt;br /&gt;
int currentLines = dataList.Count;&lt;br /&gt;
int emptyLines = Math.Max(0, minLines - currentLines);&lt;br /&gt;
&lt;br /&gt;
for (int i = 0; i &amp;lt; emptyLines; i++)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr style=&#039;height: 12px;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Add this using statement at top of your script:&lt;br /&gt;
using ZinformAccounts.Debugger;&lt;br /&gt;
&lt;br /&gt;
// Launch debugger at any point&lt;br /&gt;
DebuggerExtensions.LaunchNow(&amp;quot;Starting Main method&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Show debug information&lt;br /&gt;
MessageBox.Show($&amp;quot;Debug: {variableName}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Complete Example ==&lt;br /&gt;
&lt;br /&gt;
Here&#039;s a simplified but complete statement script:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using System;&lt;br /&gt;
using System.Text;&lt;br /&gt;
using System.Windows;&lt;br /&gt;
using System.Data;&lt;br /&gt;
using ZinBusinessLayer;&lt;br /&gt;
using ZinBusinessLayer.HelperClasses;&lt;br /&gt;
using ZinBusinessLayer.Enums;&lt;br /&gt;
using ZinBusinessLayer.Word;&lt;br /&gt;
&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
&lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        &lt;br /&gt;
        if (!(documentTemplate is ZinWord sourceDoc))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Invalid document template&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        document = zinBL.DocumentFunctions.GetDocument(parameters.GetItemString(&amp;quot;DocumentId&amp;quot;));&lt;br /&gt;
        DateTime statementDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
            document.DocumentOutputFolder,&lt;br /&gt;
            $&amp;quot;Statement_{DateTime.Now:yyyyMMdd_hhmmss}&amp;quot;,&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)&lt;br /&gt;
        );&lt;br /&gt;
        &lt;br /&gt;
        if (string.IsNullOrEmpty(saveLocation))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No save location chosen&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        DataTable dataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
        ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
        &lt;br /&gt;
        ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
        {&lt;br /&gt;
            int count = 1;&lt;br /&gt;
            &lt;br /&gt;
            foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
            {&lt;br /&gt;
                if (progressBar.CancellationPending) break;&lt;br /&gt;
                &lt;br /&gt;
                string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
                var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
                &lt;br /&gt;
                progressBar.ReportProgress(count, $&amp;quot;Processing: {otherParty.Name}&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                var doc = sourceDoc.Clone(true);&lt;br /&gt;
                &lt;br /&gt;
                // Process bookmarks&lt;br /&gt;
                zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
                    doc, otherParty, parameters, zinBL, 193, 70, 5&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Get statement data&lt;br /&gt;
                var (transactions, details) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
                    statementDate, otherPartyId, zinBL&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Build transaction table&lt;br /&gt;
                var html = new StringBuilder();&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
                foreach (var tran in transactions)&lt;br /&gt;
                {&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td style=&#039;text-align: right;&#039;&amp;gt;{tran.Value:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                doc.SetBookmarkHtml(&amp;quot;Transactions&amp;quot;, html.ToString());&lt;br /&gt;
                doc.SetBookmarkString(&amp;quot;TotalDue&amp;quot;, details.TotalDue.ToString(&amp;quot;#,##0.00&amp;quot;));&lt;br /&gt;
                &lt;br /&gt;
                bulkDoc.AttachDocument(doc);&lt;br /&gt;
                count++;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            ProgressBarManager.Finished();&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        if (zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
        {&lt;br /&gt;
            bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Problem !! Solution&lt;br /&gt;
|-&lt;br /&gt;
| Bookmark not found error || Check spelling - bookmarks are case-sensitive&lt;br /&gt;
|-&lt;br /&gt;
| HTML not rendering properly || Use inline styles only, no external CSS&lt;br /&gt;
|-&lt;br /&gt;
| Progress bar not showing || Ensure you call &amp;lt;code&amp;gt;ProgressBarManager.Finished()&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Document won&#039;t save || Check file path and ensure extension matches format&lt;br /&gt;
|-&lt;br /&gt;
| Changes not appearing || Make sure you&#039;re modifying the correct document instance (not the template)&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=135</id>
		<title>Document Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=135"/>
		<updated>2025-11-10T03:38:27Z</updated>

		<summary type="html">&lt;p&gt;Mike: /* Troubleshooting */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Document Scripting =&lt;br /&gt;
&lt;br /&gt;
Document Scripting allows you to create custom documents in ZinformAccounts by writing C# code that processes templates and data. This guide covers the core functions and patterns you&#039;ll use.&lt;br /&gt;
&lt;br /&gt;
== Getting Started ==&lt;br /&gt;
&lt;br /&gt;
Every document script follows this basic structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
    &lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        // Your code here&lt;br /&gt;
        return true; // or false if error&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; Your class must be named &amp;lt;code&amp;gt;DocumentScripter&amp;lt;/code&amp;gt; and have a &amp;lt;code&amp;gt;Main&amp;lt;/code&amp;gt; method with these exact parameters.&lt;br /&gt;
&lt;br /&gt;
== Working with ZinWord Documents ==&lt;br /&gt;
&lt;br /&gt;
=== Creating and Loading Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Create a new document&lt;br /&gt;
ZinWord document = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
// Load from file&lt;br /&gt;
document.LoadDocument(documentFileName);&lt;br /&gt;
&lt;br /&gt;
// Load from stream&lt;br /&gt;
document.LoadStream(memoryStream);&lt;br /&gt;
&lt;br /&gt;
// Clone an existing document (useful when processing multiple records)&lt;br /&gt;
ZinWord clonedDoc = sourceDocument.Clone(true);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Saving Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Save to file - format determined by extension&lt;br /&gt;
document.SaveDocument(saveFileName);&lt;br /&gt;
&lt;br /&gt;
// Examples:&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.pdf&amp;quot;);   // Saves as PDF&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.docx&amp;quot;);  // Saves as Word document&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Checking Document Content ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Check if document has any content (text, tables, or images)&lt;br /&gt;
bool hasContent = zinBL.DocumentFunctions.DocumentHasContent(document);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Attaching Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Combine multiple documents into one (useful for bulk processing)&lt;br /&gt;
ZinWord bulkDocument = new ZinWord(zinBL);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument1);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument2);&lt;br /&gt;
bulkDocument.SaveDocument(&amp;quot;Bulk_Output.pdf&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Working with Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
Bookmarks are placeholders in your Word templates that you replace with actual data.&lt;br /&gt;
&lt;br /&gt;
=== Setting Bookmark Values ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Set text value&lt;br /&gt;
document.SetBookmarkString(&amp;quot;BookmarkName&amp;quot;, &amp;quot;Value&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Set HTML content (for formatted text, tables, etc.)&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;BookmarkName&amp;quot;, htmlString);&lt;br /&gt;
&lt;br /&gt;
// Set hyperlink&lt;br /&gt;
document.SetBookmarkHyperLink(&amp;quot;BookmarkName&amp;quot;, hyperLinkItem);&lt;br /&gt;
&lt;br /&gt;
// Set image from file&lt;br /&gt;
document.SetBookmarkImageFromFile(&amp;quot;BookmarkName&amp;quot;, pictureFileName);&lt;br /&gt;
&lt;br /&gt;
// Set image from Picture object&lt;br /&gt;
document.SetBookmarkImage(&amp;quot;BookmarkName&amp;quot;, pictureItem);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Deleting Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Remove a bookmark and its content&lt;br /&gt;
document.DeleteBookmark(&amp;quot;BookmarkName&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Getting All Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Retrieve all bookmarks in the document&lt;br /&gt;
var bookmarks = document.GetBookmarks();&lt;br /&gt;
&lt;br /&gt;
foreach (var bookmark in bookmarks)&lt;br /&gt;
{&lt;br /&gt;
    // Process each bookmark&lt;br /&gt;
    string name = bookmark.Name;&lt;br /&gt;
    // ... do something with the bookmark&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Standard Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
ZinformAccounts provides automatic processing for common bookmarks:&lt;br /&gt;
&lt;br /&gt;
=== ProcessStandardBookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
    document,           // ZinWord document to process&lt;br /&gt;
    otherParty,         // ZOtherParty object (customer/supplier)&lt;br /&gt;
    parameters,         // ZinParameters with document settings&lt;br /&gt;
    zinBL,             // Business layer instance&lt;br /&gt;
    logoWidth,         // Logo width in pixels (e.g., 193)&lt;br /&gt;
    logoHeight,        // Logo height in pixels (e.g., 70)&lt;br /&gt;
    minAddressLines,   // Minimum address lines (default: 4)&lt;br /&gt;
    addresseeFirstLineAddress,  // Use first line as addressee (default: false)&lt;br /&gt;
    stateAsCity        // Treat state as city (NZ DB) (default: false)&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Standard Bookmarks:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationLogo&amp;lt;/code&amp;gt; - Your company logo&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationName&amp;lt;/code&amp;gt; - Your company name&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationAddress&amp;lt;/code&amp;gt; - Your postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationPhone&amp;lt;/code&amp;gt; - Your phone number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationEmail&amp;lt;/code&amp;gt; - Your email address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationWebsite&amp;lt;/code&amp;gt; - Your website URL&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccount&amp;lt;/code&amp;gt; - Bank account name and number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountNumber&amp;lt;/code&amp;gt; - Bank account number only&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountName&amp;lt;/code&amp;gt; - Bank account name only&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentDate&amp;lt;/code&amp;gt; - Current date (formatted as &amp;quot;dd MMMM yyyy&amp;quot;)&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentId&amp;lt;/code&amp;gt; - Document ID from parameters&lt;br /&gt;
* &amp;lt;code&amp;gt;GSTNumber&amp;lt;/code&amp;gt; - Tax registration number&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyId&amp;lt;/code&amp;gt; - Customer/Supplier ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyName&amp;lt;/code&amp;gt; - Customer/Supplier name&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyAddress&amp;lt;/code&amp;gt; - Customer/Supplier postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartySalutation&amp;lt;/code&amp;gt; - Greeting (from Administrator/Manager contact)&lt;br /&gt;
* &amp;lt;code&amp;gt;ReferenceId&amp;lt;/code&amp;gt; - Reference ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationNameFooter&amp;lt;/code&amp;gt; - Company name for footer&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For invoice-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ProcessInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
Handles delivery address for invoices:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, transaction,&lt;br /&gt;
    minAddressLines, stateAsCity, &amp;quot;DELIVERY&amp;quot;&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Bookmark:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;DeliverToDetails&amp;lt;/code&amp;gt; - Delivery address (deleted if no delivery address exists)&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraStatementBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For statement-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraStatementBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Working with Parameters ==&lt;br /&gt;
&lt;br /&gt;
Parameters pass data between the UI and your script.&lt;br /&gt;
&lt;br /&gt;
=== Getting Parameter Values ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get string value&lt;br /&gt;
string documentId = parameters.GetItemString(&amp;quot;DocumentId&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get datetime value&lt;br /&gt;
DateTime asAtDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get boolean value&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get output extension&lt;br /&gt;
string extension = parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;);&lt;br /&gt;
string filter = parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setting Return Parameters ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Return the save location to the caller&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
&lt;br /&gt;
// Return error message if something fails&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== File Operations ==&lt;br /&gt;
&lt;br /&gt;
=== Save File Dialog ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
    document.DocumentOutputFolder,           // Default folder&lt;br /&gt;
    String.Format(&amp;quot;Statement_{0}&amp;quot;, DateTime.Now.ToString(&amp;quot;yyyyMMdd_hhmmss&amp;quot;)), // Default filename&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),       // Default extension (e.g., &amp;quot;pdf&amp;quot;)&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)  // File type filter&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
if (saveLocation != null &amp;amp;&amp;amp; saveLocation.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // User selected a location&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    // User cancelled - handle error&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Creating Directories ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
string folderPath = Path.Combine(baseFolderPath, $&amp;quot;OtherParty_{otherPartyId}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
if (!Directory.Exists(folderPath))&lt;br /&gt;
{&lt;br /&gt;
    Directory.CreateDirectory(folderPath);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
string saveFileName = Path.Combine(folderPath, &amp;quot;Statement.pdf&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Working with DataTables ==&lt;br /&gt;
&lt;br /&gt;
When processing multiple records (e.g., statements for multiple customers):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Validate the DataTable parameter&lt;br /&gt;
DataTable otherPartyDataTable;&lt;br /&gt;
if (otherPartyDataTable != null &amp;amp;&amp;amp; otherPartyDataTable is DataTable)&lt;br /&gt;
{&lt;br /&gt;
    otherPartyDataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    throw new InvalidOperationException(&amp;quot;otherPartyDataTable is not a valid DataTable&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Loop through rows&lt;br /&gt;
foreach (DataRow row in otherPartyDataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    // Get values from the row&lt;br /&gt;
    string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
    &lt;br /&gt;
    // Load the full OtherParty object&lt;br /&gt;
    var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
    &lt;br /&gt;
    // Process document for this party&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Progress Bar ==&lt;br /&gt;
&lt;br /&gt;
Show progress when processing multiple records:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, totalCount, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        // Check for cancellation&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Update progress&lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {customerName}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work here...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Mark as finished&lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Financial Functions ==&lt;br /&gt;
&lt;br /&gt;
=== Statement Data ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get statement details (aged balances)&lt;br /&gt;
ZinStatementDetails details = zinBL.DocumentFunctions.GetStatementDetails(&lt;br /&gt;
    zinBL, &lt;br /&gt;
    otherParty, &lt;br /&gt;
    statementDate, &lt;br /&gt;
    DocumentTypes.ARStatement  // or DocumentTypes.APRemittance&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// Access aged balances&lt;br /&gt;
decimal current = details.CurrentBalance;&lt;br /&gt;
decimal oneMonth = details.OneMonthOverdue;&lt;br /&gt;
decimal twoMonths = details.TwoMonthsOverdue;&lt;br /&gt;
decimal threeMonths = details.ThreeMonthsOverdue;&lt;br /&gt;
decimal total = details.TotalDue;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Transaction Data ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get monthly running transactions (preferred method)&lt;br /&gt;
var (transactions, statementDetails) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
    statementDate, &lt;br /&gt;
    otherPartyId, &lt;br /&gt;
    zinBL&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// transactions is List&amp;lt;StatementTransaction2&amp;gt;&lt;br /&gt;
// statementDetails is ZinStatementDetails with aged balances&lt;br /&gt;
&lt;br /&gt;
// Process transactions&lt;br /&gt;
foreach (var tran in transactions)&lt;br /&gt;
{&lt;br /&gt;
    DateTime date = tran.Date;&lt;br /&gt;
    int type = tran.Type;&lt;br /&gt;
    string reference = tran.Reference;&lt;br /&gt;
    decimal value = tran.Value;&lt;br /&gt;
    decimal runningBalance = tran.RunningBalance;&lt;br /&gt;
    decimal subTotal = tran.SubTotal;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Combining Transaction Lists ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Combine and sort multiple transaction lists by date&lt;br /&gt;
var combined = zinBL.DocumentFunctions.CombineTransactions(listOne, listTwo);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Date Utilities ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get last day of month&lt;br /&gt;
DateTime monthEnd = DocumentFunctions.GetEndOfMonth(someDate);&lt;br /&gt;
// Example: 15/03/2024 returns 31/03/2024&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Building HTML Tables ==&lt;br /&gt;
&lt;br /&gt;
For inserting formatted tables into bookmarks:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var htmlTable = new StringBuilder();&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%; border-collapse: collapse;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Date&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Reference&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Amount&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tbody&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
foreach (var item in dataList)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Amount:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tbody&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Insert into document&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;TableBookmark&amp;quot;, htmlTable.ToString());&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Tips for HTML Tables:&#039;&#039;&#039;&lt;br /&gt;
* Use inline styles (external CSS won&#039;t work)&lt;br /&gt;
* Specify &amp;lt;code&amp;gt;font-family: &amp;quot;Aptos&amp;quot;, sans-serif;&amp;lt;/code&amp;gt; for consistency&lt;br /&gt;
* Use &amp;lt;code&amp;gt;text-align: right&amp;lt;/code&amp;gt; for numbers&lt;br /&gt;
* Add padding for readability: &amp;lt;code&amp;gt;padding: 5px;&amp;lt;/code&amp;gt;&lt;br /&gt;
* Format currency: &amp;lt;code&amp;gt;{value:$#,##0.00}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Email Integration ==&lt;br /&gt;
&lt;br /&gt;
=== Sending Documents via Email ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get email addresses for specific contacts&lt;br /&gt;
var emailAddresses = otherParty.GetEmailContactsAsEmailCompatibleStringById(document.EmailContactIds);&lt;br /&gt;
&lt;br /&gt;
if (emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Replace placeholders in email body&lt;br /&gt;
    string emailBody = document.EmailBody.Replace(&amp;quot;[OtherPartyName]&amp;quot;, otherParty.Name);&lt;br /&gt;
    &lt;br /&gt;
    // Send email with attachment&lt;br /&gt;
    var result = zinBL.Emailing.SendEmailWithImage(&lt;br /&gt;
        emailAddresses,              // To addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // CC addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // BCC addresses&lt;br /&gt;
        document.EmailSubject,       // Subject&lt;br /&gt;
        emailBody,                   // HTML body&lt;br /&gt;
        zinBL,                       // Business layer&lt;br /&gt;
        attachmentFilePath,          // Path to attachment&lt;br /&gt;
        false,                       // Is HTML&lt;br /&gt;
        70L,                         // Image quality&lt;br /&gt;
        444,                         // Max image width&lt;br /&gt;
        &amp;quot;unsubscribe@yourdomain.com&amp;quot; // Unsubscribe email&lt;br /&gt;
    );&lt;br /&gt;
    &lt;br /&gt;
    if (!result.success)&lt;br /&gt;
    {&lt;br /&gt;
        MessageBox.Show(result.errorMessage);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
try&lt;br /&gt;
{&lt;br /&gt;
    // Your code here&lt;br /&gt;
    &lt;br /&gt;
    if (somethingWentWrong)&lt;br /&gt;
    {&lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Clear error message here&amp;quot;);&lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
catch (Exception ex)&lt;br /&gt;
{&lt;br /&gt;
    MessageBox.Show($&amp;quot;An error occurred: {ex.Message}\n{ex.StackTrace}&amp;quot;, &amp;quot;Error&amp;quot;);&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, ex.Message);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return true;  // Success&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Memory Management ===&lt;br /&gt;
&lt;br /&gt;
When processing many documents, clean up:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    var clonedDoc = sourceDoc.Clone(true);&lt;br /&gt;
    &lt;br /&gt;
    // Process document...&lt;br /&gt;
    &lt;br /&gt;
    // If not saving this document, explicitly dispose&lt;br /&gt;
    clonedDoc = null;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Conditional Processing ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Only add to bulk document if not emailed&lt;br /&gt;
bool wasEmailed = false;&lt;br /&gt;
&lt;br /&gt;
if (document.EmailIfPossible &amp;amp;&amp;amp; emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Send email&lt;br /&gt;
    wasEmailed = true;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
if (!wasEmailed)&lt;br /&gt;
{&lt;br /&gt;
    bulkDocument.AttachDocument(processedDoc);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Common Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Individual vs Bulk Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
foreach (var otherParty in otherParties)&lt;br /&gt;
{&lt;br /&gt;
    var doc = ProcessDocument(otherParty);&lt;br /&gt;
    &lt;br /&gt;
    if (individual)&lt;br /&gt;
    {&lt;br /&gt;
        // Save individual file&lt;br /&gt;
        string fileName = $&amp;quot;{folderPath}/Document_{otherParty.OtherPartyId}.pdf&amp;quot;;&lt;br /&gt;
        doc.SaveDocument(fileName);&lt;br /&gt;
    }&lt;br /&gt;
    else&lt;br /&gt;
    {&lt;br /&gt;
        // Add to bulk document&lt;br /&gt;
        bulkDoc.AttachDocument(doc);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Save bulk document if not individual&lt;br /&gt;
if (!individual &amp;amp;&amp;amp; zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
{&lt;br /&gt;
    bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Processing with Progress ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Documents&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {currentItem}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Empty Row Padding ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
int minLines = 13;&lt;br /&gt;
int currentLines = dataList.Count;&lt;br /&gt;
int emptyLines = Math.Max(0, minLines - currentLines);&lt;br /&gt;
&lt;br /&gt;
for (int i = 0; i &amp;lt; emptyLines; i++)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr style=&#039;height: 12px;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Add this using statement at top of your script:&lt;br /&gt;
using ZinformAccounts.Debugger;&lt;br /&gt;
&lt;br /&gt;
// Launch debugger at any point&lt;br /&gt;
DebuggerExtensions.LaunchNow(&amp;quot;Starting Main method&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Show debug information&lt;br /&gt;
MessageBox.Show($&amp;quot;Debug: {variableName}&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Complete Example ==&lt;br /&gt;
&lt;br /&gt;
Here&#039;s a simplified but complete statement script:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using System;&lt;br /&gt;
using System.Text;&lt;br /&gt;
using System.Windows;&lt;br /&gt;
using System.Data;&lt;br /&gt;
using ZinBusinessLayer;&lt;br /&gt;
using ZinBusinessLayer.HelperClasses;&lt;br /&gt;
using ZinBusinessLayer.Enums;&lt;br /&gt;
using ZinBusinessLayer.Word;&lt;br /&gt;
&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
&lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        &lt;br /&gt;
        if (!(documentTemplate is ZinWord sourceDoc))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Invalid document template&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        document = zinBL.DocumentFunctions.GetDocument(parameters.GetItemString(&amp;quot;DocumentId&amp;quot;));&lt;br /&gt;
        DateTime statementDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
            document.DocumentOutputFolder,&lt;br /&gt;
            $&amp;quot;Statement_{DateTime.Now:yyyyMMdd_hhmmss}&amp;quot;,&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)&lt;br /&gt;
        );&lt;br /&gt;
        &lt;br /&gt;
        if (string.IsNullOrEmpty(saveLocation))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No save location chosen&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        DataTable dataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
        ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
        &lt;br /&gt;
        ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
        {&lt;br /&gt;
            int count = 1;&lt;br /&gt;
            &lt;br /&gt;
            foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
            {&lt;br /&gt;
                if (progressBar.CancellationPending) break;&lt;br /&gt;
                &lt;br /&gt;
                string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
                var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
                &lt;br /&gt;
                progressBar.ReportProgress(count, $&amp;quot;Processing: {otherParty.Name}&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                var doc = sourceDoc.Clone(true);&lt;br /&gt;
                &lt;br /&gt;
                // Process bookmarks&lt;br /&gt;
                zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
                    doc, otherParty, parameters, zinBL, 193, 70, 5&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Get statement data&lt;br /&gt;
                var (transactions, details) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
                    statementDate, otherPartyId, zinBL&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Build transaction table&lt;br /&gt;
                var html = new StringBuilder();&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
                foreach (var tran in transactions)&lt;br /&gt;
                {&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td style=&#039;text-align: right;&#039;&amp;gt;{tran.Value:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                doc.SetBookmarkHtml(&amp;quot;Transactions&amp;quot;, html.ToString());&lt;br /&gt;
                doc.SetBookmarkString(&amp;quot;TotalDue&amp;quot;, details.TotalDue.ToString(&amp;quot;#,##0.00&amp;quot;));&lt;br /&gt;
                &lt;br /&gt;
                bulkDoc.AttachDocument(doc);&lt;br /&gt;
                count++;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            ProgressBarManager.Finished();&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        if (zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
        {&lt;br /&gt;
            bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Problem !! Solution&lt;br /&gt;
|-&lt;br /&gt;
| Bookmark not found error || Check spelling - bookmarks are case-sensitive&lt;br /&gt;
|-&lt;br /&gt;
| HTML not rendering properly || Use inline styles only, no external CSS&lt;br /&gt;
|-&lt;br /&gt;
| Progress bar not showing || Ensure you call &amp;lt;code&amp;gt;ProgressBarManager.Finished()&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Document won&#039;t save || Check file path and ensure extension matches format&lt;br /&gt;
|-&lt;br /&gt;
| Changes not appearing || Make sure you&#039;re modifying the correct document instance (not the template)&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=134</id>
		<title>Document Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=134"/>
		<updated>2025-11-10T03:38:07Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Document Scripting =&lt;br /&gt;
&lt;br /&gt;
Document Scripting allows you to create custom documents in ZinformAccounts by writing C# code that processes templates and data. This guide covers the core functions and patterns you&#039;ll use.&lt;br /&gt;
&lt;br /&gt;
== Getting Started ==&lt;br /&gt;
&lt;br /&gt;
Every document script follows this basic structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
    &lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        // Your code here&lt;br /&gt;
        return true; // or false if error&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; Your class must be named &amp;lt;code&amp;gt;DocumentScripter&amp;lt;/code&amp;gt; and have a &amp;lt;code&amp;gt;Main&amp;lt;/code&amp;gt; method with these exact parameters.&lt;br /&gt;
&lt;br /&gt;
== Working with ZinWord Documents ==&lt;br /&gt;
&lt;br /&gt;
=== Creating and Loading Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Create a new document&lt;br /&gt;
ZinWord document = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
// Load from file&lt;br /&gt;
document.LoadDocument(documentFileName);&lt;br /&gt;
&lt;br /&gt;
// Load from stream&lt;br /&gt;
document.LoadStream(memoryStream);&lt;br /&gt;
&lt;br /&gt;
// Clone an existing document (useful when processing multiple records)&lt;br /&gt;
ZinWord clonedDoc = sourceDocument.Clone(true);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Saving Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Save to file - format determined by extension&lt;br /&gt;
document.SaveDocument(saveFileName);&lt;br /&gt;
&lt;br /&gt;
// Examples:&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.pdf&amp;quot;);   // Saves as PDF&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.docx&amp;quot;);  // Saves as Word document&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Checking Document Content ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Check if document has any content (text, tables, or images)&lt;br /&gt;
bool hasContent = zinBL.DocumentFunctions.DocumentHasContent(document);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Attaching Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Combine multiple documents into one (useful for bulk processing)&lt;br /&gt;
ZinWord bulkDocument = new ZinWord(zinBL);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument1);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument2);&lt;br /&gt;
bulkDocument.SaveDocument(&amp;quot;Bulk_Output.pdf&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Working with Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
Bookmarks are placeholders in your Word templates that you replace with actual data.&lt;br /&gt;
&lt;br /&gt;
=== Setting Bookmark Values ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Set text value&lt;br /&gt;
document.SetBookmarkString(&amp;quot;BookmarkName&amp;quot;, &amp;quot;Value&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Set HTML content (for formatted text, tables, etc.)&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;BookmarkName&amp;quot;, htmlString);&lt;br /&gt;
&lt;br /&gt;
// Set hyperlink&lt;br /&gt;
document.SetBookmarkHyperLink(&amp;quot;BookmarkName&amp;quot;, hyperLinkItem);&lt;br /&gt;
&lt;br /&gt;
// Set image from file&lt;br /&gt;
document.SetBookmarkImageFromFile(&amp;quot;BookmarkName&amp;quot;, pictureFileName);&lt;br /&gt;
&lt;br /&gt;
// Set image from Picture object&lt;br /&gt;
document.SetBookmarkImage(&amp;quot;BookmarkName&amp;quot;, pictureItem);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Deleting Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Remove a bookmark and its content&lt;br /&gt;
document.DeleteBookmark(&amp;quot;BookmarkName&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Getting All Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Retrieve all bookmarks in the document&lt;br /&gt;
var bookmarks = document.GetBookmarks();&lt;br /&gt;
&lt;br /&gt;
foreach (var bookmark in bookmarks)&lt;br /&gt;
{&lt;br /&gt;
    // Process each bookmark&lt;br /&gt;
    string name = bookmark.Name;&lt;br /&gt;
    // ... do something with the bookmark&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Standard Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
ZinformAccounts provides automatic processing for common bookmarks:&lt;br /&gt;
&lt;br /&gt;
=== ProcessStandardBookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
    document,           // ZinWord document to process&lt;br /&gt;
    otherParty,         // ZOtherParty object (customer/supplier)&lt;br /&gt;
    parameters,         // ZinParameters with document settings&lt;br /&gt;
    zinBL,             // Business layer instance&lt;br /&gt;
    logoWidth,         // Logo width in pixels (e.g., 193)&lt;br /&gt;
    logoHeight,        // Logo height in pixels (e.g., 70)&lt;br /&gt;
    minAddressLines,   // Minimum address lines (default: 4)&lt;br /&gt;
    addresseeFirstLineAddress,  // Use first line as addressee (default: false)&lt;br /&gt;
    stateAsCity        // Treat state as city (NZ DB) (default: false)&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Standard Bookmarks:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationLogo&amp;lt;/code&amp;gt; - Your company logo&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationName&amp;lt;/code&amp;gt; - Your company name&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationAddress&amp;lt;/code&amp;gt; - Your postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationPhone&amp;lt;/code&amp;gt; - Your phone number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationEmail&amp;lt;/code&amp;gt; - Your email address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationWebsite&amp;lt;/code&amp;gt; - Your website URL&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccount&amp;lt;/code&amp;gt; - Bank account name and number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountNumber&amp;lt;/code&amp;gt; - Bank account number only&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountName&amp;lt;/code&amp;gt; - Bank account name only&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentDate&amp;lt;/code&amp;gt; - Current date (formatted as &amp;quot;dd MMMM yyyy&amp;quot;)&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentId&amp;lt;/code&amp;gt; - Document ID from parameters&lt;br /&gt;
* &amp;lt;code&amp;gt;GSTNumber&amp;lt;/code&amp;gt; - Tax registration number&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyId&amp;lt;/code&amp;gt; - Customer/Supplier ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyName&amp;lt;/code&amp;gt; - Customer/Supplier name&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyAddress&amp;lt;/code&amp;gt; - Customer/Supplier postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartySalutation&amp;lt;/code&amp;gt; - Greeting (from Administrator/Manager contact)&lt;br /&gt;
* &amp;lt;code&amp;gt;ReferenceId&amp;lt;/code&amp;gt; - Reference ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationNameFooter&amp;lt;/code&amp;gt; - Company name for footer&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For invoice-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ProcessInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
Handles delivery address for invoices:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, transaction,&lt;br /&gt;
    minAddressLines, stateAsCity, &amp;quot;DELIVERY&amp;quot;&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Bookmark:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;DeliverToDetails&amp;lt;/code&amp;gt; - Delivery address (deleted if no delivery address exists)&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraStatementBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For statement-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraStatementBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Working with Parameters ==&lt;br /&gt;
&lt;br /&gt;
Parameters pass data between the UI and your script.&lt;br /&gt;
&lt;br /&gt;
=== Getting Parameter Values ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get string value&lt;br /&gt;
string documentId = parameters.GetItemString(&amp;quot;DocumentId&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get datetime value&lt;br /&gt;
DateTime asAtDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get boolean value&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get output extension&lt;br /&gt;
string extension = parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;);&lt;br /&gt;
string filter = parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setting Return Parameters ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Return the save location to the caller&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
&lt;br /&gt;
// Return error message if something fails&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== File Operations ==&lt;br /&gt;
&lt;br /&gt;
=== Save File Dialog ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
    document.DocumentOutputFolder,           // Default folder&lt;br /&gt;
    String.Format(&amp;quot;Statement_{0}&amp;quot;, DateTime.Now.ToString(&amp;quot;yyyyMMdd_hhmmss&amp;quot;)), // Default filename&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),       // Default extension (e.g., &amp;quot;pdf&amp;quot;)&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)  // File type filter&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
if (saveLocation != null &amp;amp;&amp;amp; saveLocation.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // User selected a location&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    // User cancelled - handle error&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Creating Directories ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
string folderPath = Path.Combine(baseFolderPath, $&amp;quot;OtherParty_{otherPartyId}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
if (!Directory.Exists(folderPath))&lt;br /&gt;
{&lt;br /&gt;
    Directory.CreateDirectory(folderPath);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
string saveFileName = Path.Combine(folderPath, &amp;quot;Statement.pdf&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Working with DataTables ==&lt;br /&gt;
&lt;br /&gt;
When processing multiple records (e.g., statements for multiple customers):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Validate the DataTable parameter&lt;br /&gt;
DataTable otherPartyDataTable;&lt;br /&gt;
if (otherPartyDataTable != null &amp;amp;&amp;amp; otherPartyDataTable is DataTable)&lt;br /&gt;
{&lt;br /&gt;
    otherPartyDataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    throw new InvalidOperationException(&amp;quot;otherPartyDataTable is not a valid DataTable&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Loop through rows&lt;br /&gt;
foreach (DataRow row in otherPartyDataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    // Get values from the row&lt;br /&gt;
    string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
    &lt;br /&gt;
    // Load the full OtherParty object&lt;br /&gt;
    var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
    &lt;br /&gt;
    // Process document for this party&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Progress Bar ==&lt;br /&gt;
&lt;br /&gt;
Show progress when processing multiple records:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, totalCount, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        // Check for cancellation&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Update progress&lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {customerName}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work here...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Mark as finished&lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Financial Functions ==&lt;br /&gt;
&lt;br /&gt;
=== Statement Data ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get statement details (aged balances)&lt;br /&gt;
ZinStatementDetails details = zinBL.DocumentFunctions.GetStatementDetails(&lt;br /&gt;
    zinBL, &lt;br /&gt;
    otherParty, &lt;br /&gt;
    statementDate, &lt;br /&gt;
    DocumentTypes.ARStatement  // or DocumentTypes.APRemittance&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// Access aged balances&lt;br /&gt;
decimal current = details.CurrentBalance;&lt;br /&gt;
decimal oneMonth = details.OneMonthOverdue;&lt;br /&gt;
decimal twoMonths = details.TwoMonthsOverdue;&lt;br /&gt;
decimal threeMonths = details.ThreeMonthsOverdue;&lt;br /&gt;
decimal total = details.TotalDue;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Transaction Data ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get monthly running transactions (preferred method)&lt;br /&gt;
var (transactions, statementDetails) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
    statementDate, &lt;br /&gt;
    otherPartyId, &lt;br /&gt;
    zinBL&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// transactions is List&amp;lt;StatementTransaction2&amp;gt;&lt;br /&gt;
// statementDetails is ZinStatementDetails with aged balances&lt;br /&gt;
&lt;br /&gt;
// Process transactions&lt;br /&gt;
foreach (var tran in transactions)&lt;br /&gt;
{&lt;br /&gt;
    DateTime date = tran.Date;&lt;br /&gt;
    int type = tran.Type;&lt;br /&gt;
    string reference = tran.Reference;&lt;br /&gt;
    decimal value = tran.Value;&lt;br /&gt;
    decimal runningBalance = tran.RunningBalance;&lt;br /&gt;
    decimal subTotal = tran.SubTotal;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Combining Transaction Lists ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Combine and sort multiple transaction lists by date&lt;br /&gt;
var combined = zinBL.DocumentFunctions.CombineTransactions(listOne, listTwo);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Date Utilities ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get last day of month&lt;br /&gt;
DateTime monthEnd = DocumentFunctions.GetEndOfMonth(someDate);&lt;br /&gt;
// Example: 15/03/2024 returns 31/03/2024&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Building HTML Tables ==&lt;br /&gt;
&lt;br /&gt;
For inserting formatted tables into bookmarks:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var htmlTable = new StringBuilder();&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%; border-collapse: collapse;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Date&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Reference&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Amount&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tbody&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
foreach (var item in dataList)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Amount:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tbody&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Insert into document&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;TableBookmark&amp;quot;, htmlTable.ToString());&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Tips for HTML Tables:&#039;&#039;&#039;&lt;br /&gt;
* Use inline styles (external CSS won&#039;t work)&lt;br /&gt;
* Specify &amp;lt;code&amp;gt;font-family: &amp;quot;Aptos&amp;quot;, sans-serif;&amp;lt;/code&amp;gt; for consistency&lt;br /&gt;
* Use &amp;lt;code&amp;gt;text-align: right&amp;lt;/code&amp;gt; for numbers&lt;br /&gt;
* Add padding for readability: &amp;lt;code&amp;gt;padding: 5px;&amp;lt;/code&amp;gt;&lt;br /&gt;
* Format currency: &amp;lt;code&amp;gt;{value:$#,##0.00}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Email Integration ==&lt;br /&gt;
&lt;br /&gt;
=== Sending Documents via Email ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get email addresses for specific contacts&lt;br /&gt;
var emailAddresses = otherParty.GetEmailContactsAsEmailCompatibleStringById(document.EmailContactIds);&lt;br /&gt;
&lt;br /&gt;
if (emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Replace placeholders in email body&lt;br /&gt;
    string emailBody = document.EmailBody.Replace(&amp;quot;[OtherPartyName]&amp;quot;, otherParty.Name);&lt;br /&gt;
    &lt;br /&gt;
    // Send email with attachment&lt;br /&gt;
    var result = zinBL.Emailing.SendEmailWithImage(&lt;br /&gt;
        emailAddresses,              // To addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // CC addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // BCC addresses&lt;br /&gt;
        document.EmailSubject,       // Subject&lt;br /&gt;
        emailBody,                   // HTML body&lt;br /&gt;
        zinBL,                       // Business layer&lt;br /&gt;
        attachmentFilePath,          // Path to attachment&lt;br /&gt;
        false,                       // Is HTML&lt;br /&gt;
        70L,                         // Image quality&lt;br /&gt;
        444,                         // Max image width&lt;br /&gt;
        &amp;quot;unsubscribe@yourdomain.com&amp;quot; // Unsubscribe email&lt;br /&gt;
    );&lt;br /&gt;
    &lt;br /&gt;
    if (!result.success)&lt;br /&gt;
    {&lt;br /&gt;
        MessageBox.Show(result.errorMessage);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
try&lt;br /&gt;
{&lt;br /&gt;
    // Your code here&lt;br /&gt;
    &lt;br /&gt;
    if (somethingWentWrong)&lt;br /&gt;
    {&lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Clear error message here&amp;quot;);&lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
catch (Exception ex)&lt;br /&gt;
{&lt;br /&gt;
    MessageBox.Show($&amp;quot;An error occurred: {ex.Message}\n{ex.StackTrace}&amp;quot;, &amp;quot;Error&amp;quot;);&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, ex.Message);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return true;  // Success&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Memory Management ===&lt;br /&gt;
&lt;br /&gt;
When processing many documents, clean up:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    var clonedDoc = sourceDoc.Clone(true);&lt;br /&gt;
    &lt;br /&gt;
    // Process document...&lt;br /&gt;
    &lt;br /&gt;
    // If not saving this document, explicitly dispose&lt;br /&gt;
    clonedDoc = null;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Conditional Processing ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Only add to bulk document if not emailed&lt;br /&gt;
bool wasEmailed = false;&lt;br /&gt;
&lt;br /&gt;
if (document.EmailIfPossible &amp;amp;&amp;amp; emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Send email&lt;br /&gt;
    wasEmailed = true;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
if (!wasEmailed)&lt;br /&gt;
{&lt;br /&gt;
    bulkDocument.AttachDocument(processedDoc);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Common Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Individual vs Bulk Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
foreach (var otherParty in otherParties)&lt;br /&gt;
{&lt;br /&gt;
    var doc = ProcessDocument(otherParty);&lt;br /&gt;
    &lt;br /&gt;
    if (individual)&lt;br /&gt;
    {&lt;br /&gt;
        // Save individual file&lt;br /&gt;
        string fileName = $&amp;quot;{folderPath}/Document_{otherParty.OtherPartyId}.pdf&amp;quot;;&lt;br /&gt;
        doc.SaveDocument(fileName);&lt;br /&gt;
    }&lt;br /&gt;
    else&lt;br /&gt;
    {&lt;br /&gt;
        // Add to bulk document&lt;br /&gt;
        bulkDoc.AttachDocument(doc);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Save bulk document if not individual&lt;br /&gt;
if (!individual &amp;amp;&amp;amp; zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
{&lt;br /&gt;
    bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Processing with Progress ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Documents&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {currentItem}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Empty Row Padding ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
int minLines = 13;&lt;br /&gt;
int currentLines = dataList.Count;&lt;br /&gt;
int emptyLines = Math.Max(0, minLines - currentLines);&lt;br /&gt;
&lt;br /&gt;
for (int i = 0; i &amp;lt; emptyLines; i++)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr style=&#039;height: 12px;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Add this using statement at top of your script:&lt;br /&gt;
using ZinformAccounts.Debugger;&lt;br /&gt;
&lt;br /&gt;
// Launch debugger at any point&lt;br /&gt;
DebuggerExtensions.LaunchNow(&amp;quot;Starting Main method&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Show debug information&lt;br /&gt;
MessageBox.Show($&amp;quot;Debug: {variableName}&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Complete Example ==&lt;br /&gt;
&lt;br /&gt;
Here&#039;s a simplified but complete statement script:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using System;&lt;br /&gt;
using System.Text;&lt;br /&gt;
using System.Windows;&lt;br /&gt;
using System.Data;&lt;br /&gt;
using ZinBusinessLayer;&lt;br /&gt;
using ZinBusinessLayer.HelperClasses;&lt;br /&gt;
using ZinBusinessLayer.Enums;&lt;br /&gt;
using ZinBusinessLayer.Word;&lt;br /&gt;
&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
&lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        &lt;br /&gt;
        if (!(documentTemplate is ZinWord sourceDoc))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Invalid document template&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        document = zinBL.DocumentFunctions.GetDocument(parameters.GetItemString(&amp;quot;DocumentId&amp;quot;));&lt;br /&gt;
        DateTime statementDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
            document.DocumentOutputFolder,&lt;br /&gt;
            $&amp;quot;Statement_{DateTime.Now:yyyyMMdd_hhmmss}&amp;quot;,&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)&lt;br /&gt;
        );&lt;br /&gt;
        &lt;br /&gt;
        if (string.IsNullOrEmpty(saveLocation))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No save location chosen&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        DataTable dataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
        ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
        &lt;br /&gt;
        ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
        {&lt;br /&gt;
            int count = 1;&lt;br /&gt;
            &lt;br /&gt;
            foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
            {&lt;br /&gt;
                if (progressBar.CancellationPending) break;&lt;br /&gt;
                &lt;br /&gt;
                string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
                var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
                &lt;br /&gt;
                progressBar.ReportProgress(count, $&amp;quot;Processing: {otherParty.Name}&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                var doc = sourceDoc.Clone(true);&lt;br /&gt;
                &lt;br /&gt;
                // Process bookmarks&lt;br /&gt;
                zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
                    doc, otherParty, parameters, zinBL, 193, 70, 5&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Get statement data&lt;br /&gt;
                var (transactions, details) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
                    statementDate, otherPartyId, zinBL&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Build transaction table&lt;br /&gt;
                var html = new StringBuilder();&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
                foreach (var tran in transactions)&lt;br /&gt;
                {&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td style=&#039;text-align: right;&#039;&amp;gt;{tran.Value:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                doc.SetBookmarkHtml(&amp;quot;Transactions&amp;quot;, html.ToString());&lt;br /&gt;
                doc.SetBookmarkString(&amp;quot;TotalDue&amp;quot;, details.TotalDue.ToString(&amp;quot;#,##0.00&amp;quot;));&lt;br /&gt;
                &lt;br /&gt;
                bulkDoc.AttachDocument(doc);&lt;br /&gt;
                count++;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            ProgressBarManager.Finished();&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        if (zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
        {&lt;br /&gt;
            bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Problem !! Solution&lt;br /&gt;
|-&lt;br /&gt;
| Bookmark not found error || Check spelling - bookmarks are case-sensitive&lt;br /&gt;
|-&lt;br /&gt;
| HTML not rendering properly || Use inline styles only, no external CSS&lt;br /&gt;
|-&lt;br /&gt;
| Progress bar not showing || Ensure you call &amp;lt;code&amp;gt;ProgressBarManager.Finished()&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Document won&#039;t save || Check file path and ensure extension matches format&lt;br /&gt;
|-&lt;br /&gt;
| Changes not appearing || Make sure you&#039;re modifying the correct document instance (not the template)&lt;br /&gt;
|-&lt;br /&gt;
| Out of memory with many documents || Process in batches, dispose of documents after use&lt;br /&gt;
|}&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Import_File_Scripting&amp;diff=133</id>
		<title>Import File Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Import_File_Scripting&amp;diff=133"/>
		<updated>2025-11-10T03:29:57Z</updated>

		<summary type="html">&lt;p&gt;Mike: /* See Also */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= File Import System Documentation =&lt;br /&gt;
&lt;br /&gt;
This page documents the file import infrastructure in ZinformAccounts, providing templates and patterns for implementing various file format imports including CSV, XML, Excel, JSON, and other data sources.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The import system provides a flexible framework for processing external data files into the ZinformAccounts database. All imports follow consistent patterns for error handling, validation, and transaction management.&lt;br /&gt;
&lt;br /&gt;
== Supported File Formats ==&lt;br /&gt;
&lt;br /&gt;
=== Excel Files (.xlsx, .xls) ===&lt;br /&gt;
&lt;br /&gt;
Using GemBox.Spreadsheet for Excel file processing:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using GemBox.Spreadsheet;&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportExcel&amp;lt;T&amp;gt;(string filePath) where T : new()&lt;br /&gt;
{&lt;br /&gt;
    var workbook = ExcelFile.Load(filePath);&lt;br /&gt;
    var worksheet = workbook.Worksheets[0];&lt;br /&gt;
    var results = new List&amp;lt;T&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    // Get header row to map columns&lt;br /&gt;
    int headerRowIndex = 0;&lt;br /&gt;
    var headers = new Dictionary&amp;lt;int, string&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    for (int col = 0; col &amp;lt; worksheet.CalculateMaxUsedColumns(); col++)&lt;br /&gt;
    {&lt;br /&gt;
        var headerValue = worksheet.Cells[headerRowIndex, col].Value?.ToString();&lt;br /&gt;
        if (!string.IsNullOrEmpty(headerValue))&lt;br /&gt;
            headers[col] = headerValue;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Process data rows&lt;br /&gt;
    for (int row = headerRowIndex + 1; row &amp;lt; worksheet.Rows.Count; row++)&lt;br /&gt;
    {&lt;br /&gt;
        var item = MapRowToObject&amp;lt;T&amp;gt;(worksheet.Rows[row], headers);&lt;br /&gt;
        if (item != null)&lt;br /&gt;
            results.Add(item);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== CSV Files ===&lt;br /&gt;
&lt;br /&gt;
Using CsvHelper for CSV processing with flexible date handling:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using CsvHelper;&lt;br /&gt;
using CsvHelper.Configuration;&lt;br /&gt;
using System.Globalization;&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportCsv&amp;lt;T&amp;gt;(string filePath, bool hasHeader = true)&lt;br /&gt;
{&lt;br /&gt;
    var config = new CsvConfiguration(CultureInfo.InvariantCulture)&lt;br /&gt;
    {&lt;br /&gt;
        HasHeaderRecord = hasHeader,&lt;br /&gt;
        MissingFieldFound = null,&lt;br /&gt;
        BadDataFound = null,&lt;br /&gt;
        TrimOptions = TrimOptions.Trim&lt;br /&gt;
    };&lt;br /&gt;
    &lt;br /&gt;
    using var reader = new StreamReader(filePath);&lt;br /&gt;
    using var csv = new CsvReader(reader, config);&lt;br /&gt;
    &lt;br /&gt;
    // Register custom type converters for dates&lt;br /&gt;
    csv.Context.TypeConverterCache.AddConverter&amp;lt;DateTime&amp;gt;(new FlexibleDateConverter());&lt;br /&gt;
    &lt;br /&gt;
    return csv.GetRecords&amp;lt;T&amp;gt;().ToList();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class FlexibleDateConverter : ITypeConverter&lt;br /&gt;
{&lt;br /&gt;
    private readonly string[] _dateFormats = new[]&lt;br /&gt;
    {&lt;br /&gt;
        &amp;quot;dd/MM/yyyy&amp;quot;,&lt;br /&gt;
        &amp;quot;d/M/yyyy&amp;quot;,&lt;br /&gt;
        &amp;quot;yyyy-MM-dd&amp;quot;,&lt;br /&gt;
        &amp;quot;dd-MM-yyyy&amp;quot;,&lt;br /&gt;
        &amp;quot;dd.MM.yyyy&amp;quot;&lt;br /&gt;
    };&lt;br /&gt;
    &lt;br /&gt;
    public object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)&lt;br /&gt;
    {&lt;br /&gt;
        if (DateTime.TryParseExact(text, _dateFormats, &lt;br /&gt;
            CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))&lt;br /&gt;
            return date;&lt;br /&gt;
        &lt;br /&gt;
        throw new InvalidOperationException($&amp;quot;Cannot parse date: {text}&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== XML Files ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using System.Xml.Linq;&lt;br /&gt;
using System.Xml.Serialization;&lt;br /&gt;
&lt;br /&gt;
// Method 1: Using XDocument for flexible parsing&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportXmlFlexible&amp;lt;T&amp;gt;(string filePath, Func&amp;lt;XElement, T&amp;gt; mapper)&lt;br /&gt;
{&lt;br /&gt;
    var doc = XDocument.Load(filePath);&lt;br /&gt;
    var results = new List&amp;lt;T&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    foreach (var element in doc.Descendants(&amp;quot;Record&amp;quot;))&lt;br /&gt;
    {&lt;br /&gt;
        var item = mapper(element);&lt;br /&gt;
        if (item != null)&lt;br /&gt;
            results.Add(item);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Method 2: Using XML Serialization for strongly-typed objects&lt;br /&gt;
public T ImportXmlSerialized&amp;lt;T&amp;gt;(string filePath) where T : class&lt;br /&gt;
{&lt;br /&gt;
    var serializer = new XmlSerializer(typeof(T));&lt;br /&gt;
    using var stream = File.OpenRead(filePath);&lt;br /&gt;
    return serializer.Deserialize(stream) as T;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== JSON Files ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using Newtonsoft.Json;&lt;br /&gt;
&lt;br /&gt;
public T ImportJson&amp;lt;T&amp;gt;(string filePath)&lt;br /&gt;
{&lt;br /&gt;
    var json = File.ReadAllText(filePath);&lt;br /&gt;
    return JsonConvert.DeserializeObject&amp;lt;T&amp;gt;(json);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportJsonArray&amp;lt;T&amp;gt;(string filePath)&lt;br /&gt;
{&lt;br /&gt;
    var json = File.ReadAllText(filePath);&lt;br /&gt;
    return JsonConvert.DeserializeObject&amp;lt;List&amp;lt;T&amp;gt;&amp;gt;(json) ?? new List&amp;lt;T&amp;gt;();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Tab-Delimited and Fixed-Width Files ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Tab-delimited files&lt;br /&gt;
public List&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt; ImportTabDelimited(string filePath)&lt;br /&gt;
{&lt;br /&gt;
    var lines = File.ReadAllLines(filePath);&lt;br /&gt;
    if (lines.Length == 0) return new List&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    var headers = lines[0].Split(&#039;\t&#039;);&lt;br /&gt;
    var results = new List&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    for (int i = 1; i &amp;lt; lines.Length; i++)&lt;br /&gt;
    {&lt;br /&gt;
        var values = lines[i].Split(&#039;\t&#039;);&lt;br /&gt;
        var record = new Dictionary&amp;lt;string, string&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        for (int j = 0; j &amp;lt; Math.Min(headers.Length, values.Length); j++)&lt;br /&gt;
        {&lt;br /&gt;
            record[headers[j]] = values[j];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        results.Add(record);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Fixed-width files&lt;br /&gt;
public class FixedWidthField&lt;br /&gt;
{&lt;br /&gt;
    public string Name { get; set; }&lt;br /&gt;
    public int StartPosition { get; set; }&lt;br /&gt;
    public int Length { get; set; }&lt;br /&gt;
    public Type DataType { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;Dictionary&amp;lt;string, object&amp;gt;&amp;gt; ImportFixedWidth(string filePath, List&amp;lt;FixedWidthField&amp;gt; fields)&lt;br /&gt;
{&lt;br /&gt;
    var lines = File.ReadAllLines(filePath);&lt;br /&gt;
    var results = new List&amp;lt;Dictionary&amp;lt;string, object&amp;gt;&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    foreach (var line in lines)&lt;br /&gt;
    {&lt;br /&gt;
        var record = new Dictionary&amp;lt;string, object&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        foreach (var field in fields)&lt;br /&gt;
        {&lt;br /&gt;
            if (line.Length &amp;gt;= field.StartPosition + field.Length)&lt;br /&gt;
            {&lt;br /&gt;
                var value = line.Substring(field.StartPosition, field.Length).Trim();&lt;br /&gt;
                record[field.Name] = ConvertToType(value, field.DataType);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        results.Add(record);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Generic Import Script Template ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using System;&lt;br /&gt;
using System.IO;&lt;br /&gt;
using System.Data;&lt;br /&gt;
using System.Text;&lt;br /&gt;
using System.Windows;&lt;br /&gt;
using System.Linq;&lt;br /&gt;
using System.Collections.Generic;&lt;br /&gt;
using ZinBusinessLayer;&lt;br /&gt;
using ZinDATA;&lt;br /&gt;
using ZinBusinessLayer.Models;&lt;br /&gt;
&lt;br /&gt;
public class GenericImportScripter&lt;br /&gt;
{&lt;br /&gt;
    private BusinessLayerMain zinBL;&lt;br /&gt;
    private readonly List&amp;lt;string&amp;gt; errors = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    private readonly List&amp;lt;string&amp;gt; warnings = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    public bool Main(BusinessLayerMain bl, ZinParameters parameters, ZinParameters returnParameters)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        &lt;br /&gt;
        try&lt;br /&gt;
        {&lt;br /&gt;
            // Get parameters&lt;br /&gt;
            var filename = parameters.GetItemString(&amp;quot;FileName&amp;quot;);&lt;br /&gt;
            var importType = parameters.GetItemString(&amp;quot;ImportType&amp;quot;);&lt;br /&gt;
            var validateOnly = parameters.GetItemBool(&amp;quot;ValidateOnly&amp;quot;) ?? false;&lt;br /&gt;
            &lt;br /&gt;
            // Determine file type and import&lt;br /&gt;
            var extension = Path.GetExtension(filename).ToLower();&lt;br /&gt;
            dynamic data = null;&lt;br /&gt;
            &lt;br /&gt;
            switch (extension)&lt;br /&gt;
            {&lt;br /&gt;
                case &amp;quot;.xlsx&amp;quot;:&lt;br /&gt;
                case &amp;quot;.xls&amp;quot;:&lt;br /&gt;
                    data = ImportExcel(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.csv&amp;quot;:&lt;br /&gt;
                    data = ImportCsv(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.xml&amp;quot;:&lt;br /&gt;
                    data = ImportXml(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.json&amp;quot;:&lt;br /&gt;
                    data = ImportJson(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.txt&amp;quot;:&lt;br /&gt;
                    data = DetermineAndImportText(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                default:&lt;br /&gt;
                    throw new NotSupportedException($&amp;quot;File type {extension} is not supported&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Validate data&lt;br /&gt;
            if (!ValidateData(data))&lt;br /&gt;
            {&lt;br /&gt;
                returnParameters.SetItem(&amp;quot;Errors&amp;quot;, string.Join(&amp;quot;\n&amp;quot;, errors));&lt;br /&gt;
                return false;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process data if not validation only&lt;br /&gt;
            if (!validateOnly)&lt;br /&gt;
            {&lt;br /&gt;
                return ProcessData(data, importType);&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            returnParameters.SetItem(&amp;quot;ValidationResult&amp;quot;, &amp;quot;Data validated successfully&amp;quot;);&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
        catch (Exception ex)&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItem(&amp;quot;Error&amp;quot;, $&amp;quot;Import failed: {ex.Message}&amp;quot;);&lt;br /&gt;
            LogError($&amp;quot;Import failed: {ex.ToString()}&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private bool ValidateData(dynamic data)&lt;br /&gt;
    {&lt;br /&gt;
        // Implement validation logic based on data type&lt;br /&gt;
        if (data == null || (data is ICollection coll &amp;amp;&amp;amp; coll.Count == 0))&lt;br /&gt;
        {&lt;br /&gt;
            errors.Add(&amp;quot;No data found in import file&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Add custom validation rules here&lt;br /&gt;
        return errors.Count == 0;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private bool ProcessData(dynamic data, string importType)&lt;br /&gt;
    {&lt;br /&gt;
        using (var transaction = zinBL.BeginTransaction())&lt;br /&gt;
        {&lt;br /&gt;
            try&lt;br /&gt;
            {&lt;br /&gt;
                int recordsProcessed = 0;&lt;br /&gt;
                int recordsFailed = 0;&lt;br /&gt;
                &lt;br /&gt;
                foreach (var record in data)&lt;br /&gt;
                {&lt;br /&gt;
                    try&lt;br /&gt;
                    {&lt;br /&gt;
                        ProcessRecord(record, importType);&lt;br /&gt;
                        recordsProcessed++;&lt;br /&gt;
                    }&lt;br /&gt;
                    catch (Exception ex)&lt;br /&gt;
                    {&lt;br /&gt;
                        recordsFailed++;&lt;br /&gt;
                        LogError($&amp;quot;Failed to process record: {ex.Message}&amp;quot;);&lt;br /&gt;
                        &lt;br /&gt;
                        // Optionally continue or rollback on error&lt;br /&gt;
                        if (recordsFailed &amp;gt; 10) // Threshold for failures&lt;br /&gt;
                        {&lt;br /&gt;
                            transaction.Rollback();&lt;br /&gt;
                            return false;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                transaction.Commit();&lt;br /&gt;
                LogInfo($&amp;quot;Import completed: {recordsProcessed} records processed, {recordsFailed} failed&amp;quot;);&lt;br /&gt;
                return recordsFailed == 0;&lt;br /&gt;
            }&lt;br /&gt;
            catch (Exception ex)&lt;br /&gt;
            {&lt;br /&gt;
                transaction.Rollback();&lt;br /&gt;
                LogError($&amp;quot;Transaction failed: {ex.Message}&amp;quot;);&lt;br /&gt;
                return false;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void ProcessRecord(dynamic record, string importType)&lt;br /&gt;
    {&lt;br /&gt;
        // Implement record processing based on import type&lt;br /&gt;
        switch (importType)&lt;br /&gt;
        {&lt;br /&gt;
            case &amp;quot;Invoice&amp;quot;:&lt;br /&gt;
                ProcessInvoiceRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;Payment&amp;quot;:&lt;br /&gt;
                ProcessPaymentRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;Customer&amp;quot;:&lt;br /&gt;
                ProcessCustomerRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;Item&amp;quot;:&lt;br /&gt;
                ProcessItemRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            default:&lt;br /&gt;
                throw new NotSupportedException($&amp;quot;Import type {importType} is not supported&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void LogError(string message)&lt;br /&gt;
    {&lt;br /&gt;
        errors.Add(message);&lt;br /&gt;
        zinBL.LoggingFunctions?.LogError(message);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void LogInfo(string message)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL.LoggingFunctions?.LogInfo(message);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Import Mapping Configuration ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class ImportMapping&lt;br /&gt;
{&lt;br /&gt;
    public string SourceField { get; set; }&lt;br /&gt;
    public string TargetField { get; set; }&lt;br /&gt;
    public Type DataType { get; set; }&lt;br /&gt;
    public bool Required { get; set; }&lt;br /&gt;
    public object DefaultValue { get; set; }&lt;br /&gt;
    public Func&amp;lt;object, object&amp;gt; Transform { get; set; }&lt;br /&gt;
    public Func&amp;lt;object, bool&amp;gt; Validate { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ImportConfiguration&lt;br /&gt;
{&lt;br /&gt;
    public List&amp;lt;ImportMapping&amp;gt; Mappings { get; set; } = new List&amp;lt;ImportMapping&amp;gt;();&lt;br /&gt;
    public bool SkipEmptyRows { get; set; } = true;&lt;br /&gt;
    public bool TrimValues { get; set; } = true;&lt;br /&gt;
    public int HeaderRow { get; set; } = 0;&lt;br /&gt;
    public int DataStartRow { get; set; } = 1;&lt;br /&gt;
    public string DateFormat { get; set; } = &amp;quot;dd/MM/yyyy&amp;quot;;&lt;br /&gt;
    public bool StopOnError { get; set; } = false;&lt;br /&gt;
    public int MaxErrors { get; set; } = 100;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Common Import Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Batch Processing ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public async Task&amp;lt;ImportResult&amp;gt; BatchImportAsync&amp;lt;T&amp;gt;(&lt;br /&gt;
    IEnumerable&amp;lt;T&amp;gt; data, &lt;br /&gt;
    int batchSize = 100,&lt;br /&gt;
    Func&amp;lt;List&amp;lt;T&amp;gt;, Task&amp;lt;bool&amp;gt;&amp;gt; processBatch = null)&lt;br /&gt;
{&lt;br /&gt;
    var result = new ImportResult();&lt;br /&gt;
    var batch = new List&amp;lt;T&amp;gt;(batchSize);&lt;br /&gt;
    &lt;br /&gt;
    foreach (var item in data)&lt;br /&gt;
    {&lt;br /&gt;
        batch.Add(item);&lt;br /&gt;
        &lt;br /&gt;
        if (batch.Count &amp;gt;= batchSize)&lt;br /&gt;
        {&lt;br /&gt;
            var success = await processBatch(batch);&lt;br /&gt;
            if (success)&lt;br /&gt;
                result.SuccessCount += batch.Count;&lt;br /&gt;
            else&lt;br /&gt;
                result.FailureCount += batch.Count;&lt;br /&gt;
            &lt;br /&gt;
            batch.Clear();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Process remaining items&lt;br /&gt;
    if (batch.Count &amp;gt; 0)&lt;br /&gt;
    {&lt;br /&gt;
        var success = await processBatch(batch);&lt;br /&gt;
        if (success)&lt;br /&gt;
            result.SuccessCount += batch.Count;&lt;br /&gt;
        else&lt;br /&gt;
            result.FailureCount += batch.Count;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return result;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Duplicate Detection ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class DuplicateDetector&amp;lt;T&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    private readonly Func&amp;lt;T, string&amp;gt; _keySelector;&lt;br /&gt;
    private readonly HashSet&amp;lt;string&amp;gt; _existingKeys;&lt;br /&gt;
    &lt;br /&gt;
    public DuplicateDetector(Func&amp;lt;T, string&amp;gt; keySelector)&lt;br /&gt;
    {&lt;br /&gt;
        _keySelector = keySelector;&lt;br /&gt;
        _existingKeys = new HashSet&amp;lt;string&amp;gt;();&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public bool IsDuplicate(T item)&lt;br /&gt;
    {&lt;br /&gt;
        var key = _keySelector(item);&lt;br /&gt;
        return !_existingKeys.Add(key);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public List&amp;lt;T&amp;gt; RemoveDuplicates(IEnumerable&amp;lt;T&amp;gt; items)&lt;br /&gt;
    {&lt;br /&gt;
        var unique = new List&amp;lt;T&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        foreach (var item in items)&lt;br /&gt;
        {&lt;br /&gt;
            if (!IsDuplicate(item))&lt;br /&gt;
                unique.Add(item);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return unique;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Data Transformation Pipeline ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class TransformPipeline&amp;lt;TSource, TTarget&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    private readonly List&amp;lt;Func&amp;lt;TSource, TSource&amp;gt;&amp;gt; _transformations = new();&lt;br /&gt;
    private Func&amp;lt;TSource, TTarget&amp;gt; _finalMapping;&lt;br /&gt;
    &lt;br /&gt;
    public TransformPipeline&amp;lt;TSource, TTarget&amp;gt; AddTransformation(Func&amp;lt;TSource, TSource&amp;gt; transform)&lt;br /&gt;
    {&lt;br /&gt;
        _transformations.Add(transform);&lt;br /&gt;
        return this;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public TransformPipeline&amp;lt;TSource, TTarget&amp;gt; SetMapping(Func&amp;lt;TSource, TTarget&amp;gt; mapping)&lt;br /&gt;
    {&lt;br /&gt;
        _finalMapping = mapping;&lt;br /&gt;
        return this;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public TTarget Process(TSource source)&lt;br /&gt;
    {&lt;br /&gt;
        var current = source;&lt;br /&gt;
        &lt;br /&gt;
        foreach (var transform in _transformations)&lt;br /&gt;
        {&lt;br /&gt;
            current = transform(current);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return _finalMapping(current);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public List&amp;lt;TTarget&amp;gt; ProcessBatch(IEnumerable&amp;lt;TSource&amp;gt; sources)&lt;br /&gt;
    {&lt;br /&gt;
        return sources.Select(Process).ToList();&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Error Handling and Logging ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class ImportLogger&lt;br /&gt;
{&lt;br /&gt;
    private readonly BusinessLayerMain _bl;&lt;br /&gt;
    private readonly List&amp;lt;ImportError&amp;gt; _errors = new();&lt;br /&gt;
    private readonly StringBuilder _log = new();&lt;br /&gt;
    &lt;br /&gt;
    public ImportLogger(BusinessLayerMain bl)&lt;br /&gt;
    {&lt;br /&gt;
        _bl = bl;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public void LogError(int rowNumber, string field, string message, object value = null)&lt;br /&gt;
    {&lt;br /&gt;
        var error = new ImportError&lt;br /&gt;
        {&lt;br /&gt;
            RowNumber = rowNumber,&lt;br /&gt;
            Field = field,&lt;br /&gt;
            Message = message,&lt;br /&gt;
            Value = value?.ToString(),&lt;br /&gt;
            Timestamp = DateTime.Now&lt;br /&gt;
        };&lt;br /&gt;
        &lt;br /&gt;
        _errors.Add(error);&lt;br /&gt;
        &lt;br /&gt;
        var logMessage = $&amp;quot;Row {rowNumber}, Field &#039;{field}&#039;: {message}&amp;quot;;&lt;br /&gt;
        if (value != null)&lt;br /&gt;
            logMessage += $&amp;quot; (Value: {value})&amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        _log.AppendLine(logMessage);&lt;br /&gt;
        _bl.LoggingFunctions?.LogError(logMessage);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public void ExportErrorReport(string filePath)&lt;br /&gt;
    {&lt;br /&gt;
        using var writer = new StreamWriter(filePath);&lt;br /&gt;
        writer.WriteLine(&amp;quot;Row,Field,Error,Value,Time&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        foreach (var error in _errors)&lt;br /&gt;
        {&lt;br /&gt;
            writer.WriteLine($&amp;quot;{error.RowNumber},{error.Field},\&amp;quot;{error.Message}\&amp;quot;,\&amp;quot;{error.Value}\&amp;quot;,{error.Timestamp:yyyy-MM-dd HH:mm:ss}&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ImportError&lt;br /&gt;
{&lt;br /&gt;
    public int RowNumber { get; set; }&lt;br /&gt;
    public string Field { get; set; }&lt;br /&gt;
    public string Message { get; set; }&lt;br /&gt;
    public string Value { get; set; }&lt;br /&gt;
    public DateTime Timestamp { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== File Upload and Validation ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class FileImportValidator&lt;br /&gt;
{&lt;br /&gt;
    private readonly long _maxFileSize = 50 * 1024 * 1024; // 50MB&lt;br /&gt;
    private readonly string[] _allowedExtensions = { &amp;quot;.csv&amp;quot;, &amp;quot;.xlsx&amp;quot;, &amp;quot;.xls&amp;quot;, &amp;quot;.xml&amp;quot;, &amp;quot;.json&amp;quot;, &amp;quot;.txt&amp;quot; };&lt;br /&gt;
    &lt;br /&gt;
    public ValidationResult ValidateFile(string filePath)&lt;br /&gt;
    {&lt;br /&gt;
        var result = new ValidationResult();&lt;br /&gt;
        &lt;br /&gt;
        // Check file exists&lt;br /&gt;
        if (!File.Exists(filePath))&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError(&amp;quot;File does not exist&amp;quot;);&lt;br /&gt;
            return result;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Check file extension&lt;br /&gt;
        var extension = Path.GetExtension(filePath).ToLower();&lt;br /&gt;
        if (!_allowedExtensions.Contains(extension))&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;File type {extension} is not allowed&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Check file size&lt;br /&gt;
        var fileInfo = new FileInfo(filePath);&lt;br /&gt;
        if (fileInfo.Length &amp;gt; _maxFileSize)&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;File size exceeds maximum allowed size of {_maxFileSize / (1024 * 1024)}MB&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Check file is readable&lt;br /&gt;
        try&lt;br /&gt;
        {&lt;br /&gt;
            using var stream = File.OpenRead(filePath);&lt;br /&gt;
            // Attempt to read first byte to ensure file is accessible&lt;br /&gt;
            stream.ReadByte();&lt;br /&gt;
        }&lt;br /&gt;
        catch (Exception ex)&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;Cannot read file: {ex.Message}&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Validate file content structure&lt;br /&gt;
        if (result.IsValid)&lt;br /&gt;
        {&lt;br /&gt;
            result = ValidateFileContent(filePath, extension);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private ValidationResult ValidateFileContent(string filePath, string extension)&lt;br /&gt;
    {&lt;br /&gt;
        var result = new ValidationResult();&lt;br /&gt;
        &lt;br /&gt;
        switch (extension)&lt;br /&gt;
        {&lt;br /&gt;
            case &amp;quot;.csv&amp;quot;:&lt;br /&gt;
                ValidateCsvStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;.xlsx&amp;quot;:&lt;br /&gt;
            case &amp;quot;.xls&amp;quot;:&lt;br /&gt;
                ValidateExcelStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;.xml&amp;quot;:&lt;br /&gt;
                ValidateXmlStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;.json&amp;quot;:&lt;br /&gt;
                ValidateJsonStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void ValidateCsvStructure(string filePath, ValidationResult result)&lt;br /&gt;
    {&lt;br /&gt;
        try&lt;br /&gt;
        {&lt;br /&gt;
            var lines = File.ReadAllLines(filePath).Take(10).ToList();&lt;br /&gt;
            &lt;br /&gt;
            if (lines.Count == 0)&lt;br /&gt;
            {&lt;br /&gt;
                result.AddError(&amp;quot;CSV file is empty&amp;quot;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Check for consistent column count&lt;br /&gt;
            var firstLineColumns = lines[0].Split(&#039;,&#039;).Length;&lt;br /&gt;
            for (int i = 1; i &amp;lt; lines.Count; i++)&lt;br /&gt;
            {&lt;br /&gt;
                var columnCount = lines[i].Split(&#039;,&#039;).Length;&lt;br /&gt;
                if (columnCount != firstLineColumns)&lt;br /&gt;
                {&lt;br /&gt;
                    result.AddWarning($&amp;quot;Inconsistent column count at line {i + 1}&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        catch (Exception ex)&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;Failed to validate CSV structure: {ex.Message}&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ValidationResult&lt;br /&gt;
{&lt;br /&gt;
    public List&amp;lt;string&amp;gt; Errors { get; } = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    public List&amp;lt;string&amp;gt; Warnings { get; } = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    public bool IsValid =&amp;gt; Errors.Count == 0;&lt;br /&gt;
    &lt;br /&gt;
    public void AddError(string message) =&amp;gt; Errors.Add(message);&lt;br /&gt;
    public void AddWarning(string message) =&amp;gt; Warnings.Add(message);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Performance Optimization ==&lt;br /&gt;
&lt;br /&gt;
=== Memory-Efficient Large File Processing ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class LargeFileProcessor&lt;br /&gt;
{&lt;br /&gt;
    public async Task ProcessLargeFileAsync(string filePath, Func&amp;lt;string, Task&amp;lt;bool&amp;gt;&amp;gt; processLine)&lt;br /&gt;
    {&lt;br /&gt;
        const int bufferSize = 65536; // 64KB buffer&lt;br /&gt;
        &lt;br /&gt;
        using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync: true);&lt;br /&gt;
        using var reader = new StreamReader(fileStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize);&lt;br /&gt;
        &lt;br /&gt;
        string line;&lt;br /&gt;
        int lineNumber = 0;&lt;br /&gt;
        &lt;br /&gt;
        while ((line = await reader.ReadLineAsync()) != null)&lt;br /&gt;
        {&lt;br /&gt;
            lineNumber++;&lt;br /&gt;
            &lt;br /&gt;
            if (!await processLine(line))&lt;br /&gt;
            {&lt;br /&gt;
                throw new ImportException($&amp;quot;Failed to process line {lineNumber}&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Parallel Processing ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class ParallelImporter&amp;lt;T&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    public async Task&amp;lt;ImportResult&amp;gt; ImportParallelAsync(&lt;br /&gt;
        IEnumerable&amp;lt;T&amp;gt; data,&lt;br /&gt;
        Func&amp;lt;T, Task&amp;lt;bool&amp;gt;&amp;gt; processItem,&lt;br /&gt;
        int maxDegreeOfParallelism = 4)&lt;br /&gt;
    {&lt;br /&gt;
        var result = new ImportResult();&lt;br /&gt;
        var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);&lt;br /&gt;
        var tasks = new List&amp;lt;Task&amp;lt;bool&amp;gt;&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        foreach (var item in data)&lt;br /&gt;
        {&lt;br /&gt;
            await semaphore.WaitAsync();&lt;br /&gt;
            &lt;br /&gt;
            tasks.Add(Task.Run(async () =&amp;gt;&lt;br /&gt;
            {&lt;br /&gt;
                try&lt;br /&gt;
                {&lt;br /&gt;
                    return await processItem(item);&lt;br /&gt;
                }&lt;br /&gt;
                finally&lt;br /&gt;
                {&lt;br /&gt;
                    semaphore.Release();&lt;br /&gt;
                }&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var results = await Task.WhenAll(tasks);&lt;br /&gt;
        &lt;br /&gt;
        result.SuccessCount = results.Count(r =&amp;gt; r);&lt;br /&gt;
        result.FailureCount = results.Count(r =&amp;gt; !r);&lt;br /&gt;
        &lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ImportResult&lt;br /&gt;
{&lt;br /&gt;
    public int SuccessCount { get; set; }&lt;br /&gt;
    public int FailureCount { get; set; }&lt;br /&gt;
    public int TotalCount =&amp;gt; SuccessCount + FailureCount;&lt;br /&gt;
    public double SuccessRate =&amp;gt; TotalCount &amp;gt; 0 ? (double)SuccessCount / TotalCount * 100 : 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Always validate before import&#039;&#039;&#039;&lt;br /&gt;
#* Check file format and structure&lt;br /&gt;
#* Validate required fields&lt;br /&gt;
#* Check data types and ranges&lt;br /&gt;
#* Identify duplicates&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Use transactions&#039;&#039;&#039;&lt;br /&gt;
#* Wrap imports in database transactions&lt;br /&gt;
#* Implement rollback on failure&lt;br /&gt;
#* Consider savepoints for partial commits&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Implement proper error handling&#039;&#039;&#039;&lt;br /&gt;
#* Log all errors with context&lt;br /&gt;
#* Provide detailed error reports&lt;br /&gt;
#* Allow partial success where appropriate&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Optimize for performance&#039;&#039;&#039;&lt;br /&gt;
#* Use batch processing for large datasets&lt;br /&gt;
#* Implement async/await patterns&lt;br /&gt;
#* Consider parallel processing where safe&lt;br /&gt;
#* Use streaming for large files&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Maintain data integrity&#039;&#039;&#039;&lt;br /&gt;
#* Check foreign key relationships&lt;br /&gt;
#* Validate business rules&lt;br /&gt;
#* Ensure referential integrity&lt;br /&gt;
#* Handle nullable fields properly&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Provide user feedback&#039;&#039;&#039;&lt;br /&gt;
#* Show progress indicators&lt;br /&gt;
#* Report success/failure counts&lt;br /&gt;
#* Export error logs&lt;br /&gt;
#* Allow preview before commit&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Security considerations&#039;&#039;&#039;&lt;br /&gt;
#* Validate file types and sizes&lt;br /&gt;
#* Scan for malicious content&lt;br /&gt;
#* Sanitize input data&lt;br /&gt;
#* Implement access controls&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Make imports resumable&#039;&#039;&#039;&lt;br /&gt;
#* Track processed records&lt;br /&gt;
#* Allow restart from failure point&lt;br /&gt;
#* Implement idempotent operations&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=132</id>
		<title>Import PDF File Scripting - AI Importing</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=132"/>
		<updated>2025-11-10T03:29:25Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
&lt;br /&gt;
The AI-Powered PDF Invoice Import System enables automated extraction and processing of supplier invoices from PDF documents using artificial intelligence. The system uses vision AI models to read invoice PDFs and automatically create AP (Accounts Payable) invoices in the accounting system, eliminating manual data entry.&lt;br /&gt;
&lt;br /&gt;
== System Capabilities ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Multi-file processing&#039;&#039;&#039;: Select and process multiple PDF invoices in a single operation&lt;br /&gt;
* &#039;&#039;&#039;AI vision processing&#039;&#039;&#039;: Leverages advanced AI models to read and interpret invoice documents&lt;br /&gt;
* &#039;&#039;&#039;Flexible extraction&#039;&#039;&#039;: Adapts to different invoice formats and layouts automatically&lt;br /&gt;
* &#039;&#039;&#039;OCR accuracy&#039;&#039;&#039;: Intelligent character recognition with disambiguation of similar characters&lt;br /&gt;
* &#039;&#039;&#039;Mathematical validation&#039;&#039;&#039;: Verifies totals and GST calculations for data integrity&lt;br /&gt;
* &#039;&#039;&#039;Automated invoice creation&#039;&#039;&#039;: Generates complete AP invoices with all line items&lt;br /&gt;
* &#039;&#039;&#039;Batch support&#039;&#039;&#039;: Optional batch processing for grouping related supplier invoices&lt;br /&gt;
&lt;br /&gt;
== AI Processing Methods ==&lt;br /&gt;
&lt;br /&gt;
The system supports two processing approaches:&lt;br /&gt;
&lt;br /&gt;
=== Vision-Based Processing (Recommended) ===&lt;br /&gt;
&lt;br /&gt;
Processes the PDF directly using AI vision models:&lt;br /&gt;
&lt;br /&gt;
* Analyzes the visual layout and structure of the invoice&lt;br /&gt;
* Handles complex formatting, tables, and multi-column layouts&lt;br /&gt;
* Works with scanned documents and image-based PDFs&lt;br /&gt;
* Processes multi-page invoices as a complete document&lt;br /&gt;
* More accurate for invoices with complex layouts&lt;br /&gt;
&lt;br /&gt;
=== Text Extraction Processing ===&lt;br /&gt;
&lt;br /&gt;
Extracts text from PDF then processes with AI:&lt;br /&gt;
&lt;br /&gt;
* Uses GemBox libraries to extract structured text&lt;br /&gt;
* Suitable for text-based PDFs with simple layouts&lt;br /&gt;
* Faster processing for straightforward invoices&lt;br /&gt;
* May struggle with complex table layouts or scanned documents&lt;br /&gt;
&lt;br /&gt;
== Supported AI Providers ==&lt;br /&gt;
&lt;br /&gt;
=== Anthropic (Claude) ===&lt;br /&gt;
&lt;br /&gt;
* Default and recommended provider&lt;br /&gt;
* Excellent document understanding capabilities&lt;br /&gt;
* Strong structured data extraction&lt;br /&gt;
* Handles complex invoice layouts&lt;br /&gt;
&lt;br /&gt;
=== OpenAI (GPT Vision) ===&lt;br /&gt;
&lt;br /&gt;
* Alternative vision processing option&lt;br /&gt;
* Compatible with GPT-4 Vision models&lt;br /&gt;
* Good for standard invoice formats&lt;br /&gt;
&lt;br /&gt;
== Configuration Parameters ==&lt;br /&gt;
&lt;br /&gt;
The system requires the following configuration:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;AIVisionEnabled&#039;&#039;&#039;: Enable/disable vision processing (true/false)&lt;br /&gt;
* &#039;&#039;&#039;AIModel&#039;&#039;&#039;: Model identifier string&lt;br /&gt;
* &#039;&#039;&#039;AIModelId&#039;&#039;&#039;: Specific model version (e.g., &amp;quot;claude-sonnet-4-20250514&amp;quot;)&lt;br /&gt;
* &#039;&#039;&#039;AIApiKey&#039;&#039;&#039;: Encrypted API key for AI service authentication&lt;br /&gt;
* &#039;&#039;&#039;FileName&#039;&#039;&#039;: Path to PDF file (when processing single files)&lt;br /&gt;
&lt;br /&gt;
== OCR and Character Recognition ==&lt;br /&gt;
&lt;br /&gt;
The AI system includes intelligent character disambiguation:&lt;br /&gt;
&lt;br /&gt;
=== Common Character Confusions ===&lt;br /&gt;
&lt;br /&gt;
The system is instructed to carefully distinguish between:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Z vs 2&#039;&#039;&#039;: Z has diagonal line, 2 has curves&lt;br /&gt;
* &#039;&#039;&#039;O vs 0&#039;&#039;&#039;: O is round, 0 may have slash or be more oval  &lt;br /&gt;
* &#039;&#039;&#039;I vs 1 vs l&#039;&#039;&#039;: Context-aware recognition (numbers vs letters)&lt;br /&gt;
* &#039;&#039;&#039;S vs 5&#039;&#039;&#039;: Shape and context analysis&lt;br /&gt;
* &#039;&#039;&#039;G vs 6&#039;&#039;&#039;: Character form recognition&lt;br /&gt;
&lt;br /&gt;
=== Validation Checks ===&lt;br /&gt;
&lt;br /&gt;
* Mathematical verification of line totals&lt;br /&gt;
* GST calculation validation (typically 15% in NZ)&lt;br /&gt;
* Cross-checking of subtotals and final amounts&lt;br /&gt;
* Multi-page continuity verification&lt;br /&gt;
&lt;br /&gt;
== Data Extraction Process ==&lt;br /&gt;
&lt;br /&gt;
The system can extract various fields depending on the invoice format:&lt;br /&gt;
&lt;br /&gt;
=== Standard Fields ===&lt;br /&gt;
&lt;br /&gt;
* Invoice Number&lt;br /&gt;
* Invoice Date&lt;br /&gt;
* Company/Supplier Name&lt;br /&gt;
* Client/Customer Name&lt;br /&gt;
* Account Numbers&lt;br /&gt;
* Order Numbers&lt;br /&gt;
* Reference Numbers&lt;br /&gt;
&lt;br /&gt;
=== Financial Totals ===&lt;br /&gt;
&lt;br /&gt;
* Line item amounts&lt;br /&gt;
* Subtotal (excluding GST)&lt;br /&gt;
* GST/Tax amount&lt;br /&gt;
* Total amount (including GST)&lt;br /&gt;
&lt;br /&gt;
=== Line Item Details ===&lt;br /&gt;
&lt;br /&gt;
* Product codes or descriptions&lt;br /&gt;
* Quantities&lt;br /&gt;
* Unit prices&lt;br /&gt;
* Extended amounts&lt;br /&gt;
* Date information (for time-based services)&lt;br /&gt;
* Reference codes or job numbers&lt;br /&gt;
&lt;br /&gt;
=== Custom Fields ===&lt;br /&gt;
&lt;br /&gt;
The AI prompt can be customized to extract additional fields specific to your supplier&#039;s invoice format:&lt;br /&gt;
&lt;br /&gt;
* Vehicle registration numbers&lt;br /&gt;
* Serial numbers&lt;br /&gt;
* Odometer readings&lt;br /&gt;
* Job descriptions&lt;br /&gt;
* Barcode data&lt;br /&gt;
* Custom reference fields&lt;br /&gt;
&lt;br /&gt;
== Invoice Creation Configuration ==&lt;br /&gt;
&lt;br /&gt;
=== Transaction Settings ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Transaction Type&#039;&#039;&#039;: APINV (AP Invoice)&lt;br /&gt;
* &#039;&#039;&#039;Transaction Type Code&#039;&#039;&#039;: 20 (default for AP invoices)&lt;br /&gt;
* &#039;&#039;&#039;Created Date&#039;&#039;&#039;: Defaults to current date/time&lt;br /&gt;
* &#039;&#039;&#039;Created User&#039;&#039;&#039;: Configurable (default: &amp;quot;Admin&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
=== Processing Dates ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Invoice Date&#039;&#039;&#039;: Extracted from PDF&lt;br /&gt;
* &#039;&#039;&#039;Payment Date&#039;&#039;&#039;: Calculated (typically 20th of following month)&lt;br /&gt;
* &#039;&#039;&#039;Process Date&#039;&#039;&#039;: Defaults to current date&lt;br /&gt;
&lt;br /&gt;
=== Account Assignments ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Supplier ID (OtherPartyId)&#039;&#039;&#039;: Must be configured in script&lt;br /&gt;
* &#039;&#039;&#039;Location ID&#039;&#039;&#039;: Configurable (default: &amp;quot;Misc&amp;quot;)&lt;br /&gt;
* &#039;&#039;&#039;Order Number&#039;&#039;&#039;: Extracted from invoice or use reference&lt;br /&gt;
* &#039;&#039;&#039;Reference&#039;&#039;&#039;: Invoice number from PDF&lt;br /&gt;
&lt;br /&gt;
=== Line Item Configuration ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Item ID&#039;&#039;&#039;: Default item code for imported lines (must be configured)&lt;br /&gt;
* &#039;&#039;&#039;Description&#039;&#039;&#039;: Extracted from PDF line items&lt;br /&gt;
* &#039;&#039;&#039;Department&#039;&#039;&#039;: Optional, can be mapped from invoice fields&lt;br /&gt;
* &#039;&#039;&#039;Quantity&#039;&#039;&#039;: Extracted from line items&lt;br /&gt;
* &#039;&#039;&#039;Unit Price&#039;&#039;&#039;: Calculated or extracted&lt;br /&gt;
* &#039;&#039;&#039;GST Treatment&#039;&#039;&#039;: Configurable (GST inclusive or exclusive)&lt;br /&gt;
&lt;br /&gt;
== GST Calculation Methods ==&lt;br /&gt;
&lt;br /&gt;
The system supports different GST calculation approaches:&lt;br /&gt;
&lt;br /&gt;
=== GST Inclusive Amounts ===&lt;br /&gt;
&lt;br /&gt;
When invoice amounts include GST:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Total With GST = Line Amount (as shown on invoice)&lt;br /&gt;
GST Exc Total = Total With GST ÷ 1.15&lt;br /&gt;
GST Amount = Total With GST - GST Exc Total&lt;br /&gt;
Unit Price = GST Exc Total ÷ Quantity&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== GST Exclusive Amounts ===&lt;br /&gt;
&lt;br /&gt;
When invoice amounts exclude GST:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
GST Exc Total = Line Amount (as shown on invoice)&lt;br /&gt;
Total With GST = GST Exc Total × 1.15&lt;br /&gt;
GST Amount = Total With GST - GST Exc Total&lt;br /&gt;
Unit Price = GST Exc Total ÷ Quantity&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Multi-Page Invoice Handling ==&lt;br /&gt;
&lt;br /&gt;
The system is designed to handle multi-page invoices:&lt;br /&gt;
&lt;br /&gt;
* Processes all pages of the PDF document&lt;br /&gt;
* Extracts line items spanning multiple pages&lt;br /&gt;
* Maintains line item sequence&lt;br /&gt;
* Validates totals across entire document&lt;br /&gt;
* Provides page count in extraction results&lt;br /&gt;
&lt;br /&gt;
== Batch Processing ==&lt;br /&gt;
&lt;br /&gt;
=== Batch Creation ===&lt;br /&gt;
&lt;br /&gt;
Optional batch grouping for related invoices:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch ID&#039;&#039;&#039;: Defaults to supplier ID or can be specified&lt;br /&gt;
* &#039;&#039;&#039;Batch Comment&#039;&#039;&#039;: Descriptive text for the batch&lt;br /&gt;
* &#039;&#039;&#039;Automatic Batching&#039;&#039;&#039;: Groups invoices by supplier&lt;br /&gt;
&lt;br /&gt;
=== Batch Benefits ===&lt;br /&gt;
&lt;br /&gt;
* Groups related invoices for review&lt;br /&gt;
* Simplifies posting process&lt;br /&gt;
* Maintains audit trail&lt;br /&gt;
* Enables bulk approval workflows&lt;br /&gt;
&lt;br /&gt;
== Error Handling ==&lt;br /&gt;
&lt;br /&gt;
The system provides comprehensive error handling:&lt;br /&gt;
&lt;br /&gt;
=== File Selection Errors ===&lt;br /&gt;
&lt;br /&gt;
* No file selected&lt;br /&gt;
* Invalid file type (non-PDF)&lt;br /&gt;
* File not found or inaccessible&lt;br /&gt;
* Corrupted PDF files&lt;br /&gt;
&lt;br /&gt;
=== AI Processing Errors ===&lt;br /&gt;
&lt;br /&gt;
* AI service connection failures&lt;br /&gt;
* Invalid or empty AI responses&lt;br /&gt;
* JSON parsing errors&lt;br /&gt;
* Incomplete data extraction&lt;br /&gt;
&lt;br /&gt;
=== Data Validation Errors ===&lt;br /&gt;
&lt;br /&gt;
* Missing required fields&lt;br /&gt;
* Invalid date formats&lt;br /&gt;
* Mathematical inconsistencies&lt;br /&gt;
* Zero or negative amounts&lt;br /&gt;
&lt;br /&gt;
=== Invoice Creation Errors ===&lt;br /&gt;
&lt;br /&gt;
* Invalid supplier ID&lt;br /&gt;
* Missing item master data&lt;br /&gt;
* Missing department codes&lt;br /&gt;
* Transaction validation failures&lt;br /&gt;
* Database constraint violations&lt;br /&gt;
&lt;br /&gt;
== Customizing the AI Prompt ==&lt;br /&gt;
&lt;br /&gt;
The extraction prompt can be customized for specific invoice formats:&lt;br /&gt;
&lt;br /&gt;
=== Prompt Structure ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
1. Role definition (extraction agent)&lt;br /&gt;
2. Task description (extract invoice data)&lt;br /&gt;
3. OCR guidance (character disambiguation)&lt;br /&gt;
4. Field list (specific fields to extract)&lt;br /&gt;
5. JSON schema (output format)&lt;br /&gt;
6. Validation rules (totals, dates, etc.)&lt;br /&gt;
7. Output constraints (no extra text, no markdown)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Customization Examples ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Adding Custom Fields:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Add fields to the JSON schema in the prompt to extract additional data specific to your invoices.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Changing Date Formats:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Specify date format requirements in the prompt (e.g., &amp;quot;yyyy-MM-dd&amp;quot;, &amp;quot;dd/MM/yyyy&amp;quot;).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Field Name Variations:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Provide alternative field names the AI should recognize (e.g., &amp;quot;Cust. Order No&amp;quot;, &amp;quot;Customer Order&amp;quot;, &amp;quot;Order Ref&amp;quot;).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Calculation Rules:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Specify how amounts should be calculated or validated.&lt;br /&gt;
&lt;br /&gt;
== Implementation Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Basic Implementation Steps ===&lt;br /&gt;
&lt;br /&gt;
# Configure AI provider and API credentials&lt;br /&gt;
# Set default supplier ID and item codes&lt;br /&gt;
# Customize extraction prompt for invoice format&lt;br /&gt;
# Configure GST calculation method&lt;br /&gt;
# Set location and account defaults&lt;br /&gt;
# Test with sample invoices&lt;br /&gt;
# Review and validate created invoices&lt;br /&gt;
# Adjust prompt and settings as needed&lt;br /&gt;
&lt;br /&gt;
=== Testing Recommendations ===&lt;br /&gt;
&lt;br /&gt;
* Start with clear, simple invoices&lt;br /&gt;
* Verify mathematical accuracy of extractions&lt;br /&gt;
* Check department and item code assignments&lt;br /&gt;
* Validate date parsing and calculations&lt;br /&gt;
* Test multi-page invoice handling&lt;br /&gt;
* Review batch creation behavior&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Invoice Preparation ===&lt;br /&gt;
&lt;br /&gt;
* Use clear, readable PDF scans&lt;br /&gt;
* Ensure full pages are captured&lt;br /&gt;
* Avoid skewed or rotated scans&lt;br /&gt;
* Check PDF file integrity before processing&lt;br /&gt;
* Process similar invoice types together&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
&lt;br /&gt;
* Set appropriate default values for all parameters&lt;br /&gt;
* Use descriptive batch comments&lt;br /&gt;
* Configure supplier-specific item codes&lt;br /&gt;
* Validate master data prerequisites&lt;br /&gt;
* Document custom prompt modifications&lt;br /&gt;
&lt;br /&gt;
=== Data Quality ===&lt;br /&gt;
&lt;br /&gt;
* Review AI-extracted data before posting&lt;br /&gt;
* Verify mathematical calculations&lt;br /&gt;
* Check supplier ID assignments&lt;br /&gt;
* Validate department code mapping&lt;br /&gt;
* Confirm date calculations&lt;br /&gt;
&lt;br /&gt;
=== Performance ===&lt;br /&gt;
&lt;br /&gt;
* Process invoices in reasonable batch sizes&lt;br /&gt;
* Monitor AI service response times&lt;br /&gt;
* Handle errors gracefully with clear messages&lt;br /&gt;
* Log processing results for audit trails&lt;br /&gt;
&lt;br /&gt;
== Advanced Features ==&lt;br /&gt;
&lt;br /&gt;
=== JSON Response Extraction ===&lt;br /&gt;
&lt;br /&gt;
The system includes a helper method to extract clean JSON from AI responses:&lt;br /&gt;
&lt;br /&gt;
* Navigates AI response structure&lt;br /&gt;
* Extracts text content from nested JSON&lt;br /&gt;
* Handles various response formats&lt;br /&gt;
* Provides error handling for malformed responses&lt;br /&gt;
&lt;br /&gt;
=== Dynamic Field Mapping ===&lt;br /&gt;
&lt;br /&gt;
The system can map extracted fields to invoice line items:&lt;br /&gt;
&lt;br /&gt;
* Product codes to item IDs&lt;br /&gt;
* Reference numbers to departments&lt;br /&gt;
* Custom fields to standard accounting fields&lt;br /&gt;
* Date parsing and conversion&lt;br /&gt;
&lt;br /&gt;
=== Calculation Flexibility ===&lt;br /&gt;
&lt;br /&gt;
Supports various calculation scenarios:&lt;br /&gt;
&lt;br /&gt;
* Zero-quantity items (single services)&lt;br /&gt;
* Division by zero protection&lt;br /&gt;
* Rounding rules for currency&lt;br /&gt;
* Tax-inclusive vs tax-exclusive amounts&lt;br /&gt;
&lt;br /&gt;
== Integration Points ==&lt;br /&gt;
&lt;br /&gt;
The PDF Import System integrates with:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Item Management&#039;&#039;&#039;: Item code lookup and validation&lt;br /&gt;
* &#039;&#039;&#039;Department Management&#039;&#039;&#039;: Department code resolution&lt;br /&gt;
* &#039;&#039;&#039;Supplier Management&#039;&#039;&#039;: Supplier/vendor record validation&lt;br /&gt;
* &#039;&#039;&#039;Transaction Processing&#039;&#039;&#039;: Invoice creation and persistence&lt;br /&gt;
* &#039;&#039;&#039;Batch Management&#039;&#039;&#039;: Batch header creation and tracking&lt;br /&gt;
* &#039;&#039;&#039;AI Vision Services&#039;&#039;&#039;: External API for document analysis&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
* API keys are encrypted using 128-bit encryption&lt;br /&gt;
* File access restricted to allowed paths&lt;br /&gt;
* User authentication tracked for created invoices&lt;br /&gt;
* Audit trail maintained for all imports&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Common Issues ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: AI extracts incorrect invoice numbers&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Add specific field location hints in prompt, emphasize OCR character disambiguation&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: Missing line items from multi-page invoices&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Ensure prompt explicitly mentions checking all pages, verify PDF page count&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: GST calculations don&#039;t match&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Verify GST inclusive/exclusive setting matches invoice format&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: Department codes not assigned&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Check department master data exists, verify field mapping in script&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: JSON deserialization errors&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Check AI response format, verify date format compatibility, review JSON schema&lt;br /&gt;
&lt;br /&gt;
== Future Enhancements ==&lt;br /&gt;
&lt;br /&gt;
Potential improvements for consideration:&lt;br /&gt;
&lt;br /&gt;
* Automatic supplier detection and matching&lt;br /&gt;
* Learning from correction patterns&lt;br /&gt;
* Support for additional currencies&lt;br /&gt;
* Purchase order matching (three-way matching)&lt;br /&gt;
* Email-based invoice submission&lt;br /&gt;
* Duplicate invoice detection&lt;br /&gt;
* Confidence scoring for extracted data&lt;br /&gt;
* Interactive review and correction interface&lt;br /&gt;
* Export of extraction results for verification&lt;br /&gt;
* Batch progress tracking and reporting&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=131</id>
		<title>Import PDF File Scripting - AI Importing</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=131"/>
		<updated>2025-11-10T03:28:46Z</updated>

		<summary type="html">&lt;p&gt;Mike: /* See Also */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{ScriptingNav}}&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The AI-Powered PDF Invoice Import System enables automated extraction and processing of supplier invoices from PDF documents using artificial intelligence. The system uses vision AI models to read invoice PDFs and automatically create AP (Accounts Payable) invoices in the accounting system, eliminating manual data entry.&lt;br /&gt;
&lt;br /&gt;
== System Capabilities ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Multi-file processing&#039;&#039;&#039;: Select and process multiple PDF invoices in a single operation&lt;br /&gt;
* &#039;&#039;&#039;AI vision processing&#039;&#039;&#039;: Leverages advanced AI models to read and interpret invoice documents&lt;br /&gt;
* &#039;&#039;&#039;Flexible extraction&#039;&#039;&#039;: Adapts to different invoice formats and layouts automatically&lt;br /&gt;
* &#039;&#039;&#039;OCR accuracy&#039;&#039;&#039;: Intelligent character recognition with disambiguation of similar characters&lt;br /&gt;
* &#039;&#039;&#039;Mathematical validation&#039;&#039;&#039;: Verifies totals and GST calculations for data integrity&lt;br /&gt;
* &#039;&#039;&#039;Automated invoice creation&#039;&#039;&#039;: Generates complete AP invoices with all line items&lt;br /&gt;
* &#039;&#039;&#039;Batch support&#039;&#039;&#039;: Optional batch processing for grouping related supplier invoices&lt;br /&gt;
&lt;br /&gt;
== AI Processing Methods ==&lt;br /&gt;
&lt;br /&gt;
The system supports two processing approaches:&lt;br /&gt;
&lt;br /&gt;
=== Vision-Based Processing (Recommended) ===&lt;br /&gt;
&lt;br /&gt;
Processes the PDF directly using AI vision models:&lt;br /&gt;
&lt;br /&gt;
* Analyzes the visual layout and structure of the invoice&lt;br /&gt;
* Handles complex formatting, tables, and multi-column layouts&lt;br /&gt;
* Works with scanned documents and image-based PDFs&lt;br /&gt;
* Processes multi-page invoices as a complete document&lt;br /&gt;
* More accurate for invoices with complex layouts&lt;br /&gt;
&lt;br /&gt;
=== Text Extraction Processing ===&lt;br /&gt;
&lt;br /&gt;
Extracts text from PDF then processes with AI:&lt;br /&gt;
&lt;br /&gt;
* Uses GemBox libraries to extract structured text&lt;br /&gt;
* Suitable for text-based PDFs with simple layouts&lt;br /&gt;
* Faster processing for straightforward invoices&lt;br /&gt;
* May struggle with complex table layouts or scanned documents&lt;br /&gt;
&lt;br /&gt;
== Supported AI Providers ==&lt;br /&gt;
&lt;br /&gt;
=== Anthropic (Claude) ===&lt;br /&gt;
&lt;br /&gt;
* Default and recommended provider&lt;br /&gt;
* Excellent document understanding capabilities&lt;br /&gt;
* Strong structured data extraction&lt;br /&gt;
* Handles complex invoice layouts&lt;br /&gt;
&lt;br /&gt;
=== OpenAI (GPT Vision) ===&lt;br /&gt;
&lt;br /&gt;
* Alternative vision processing option&lt;br /&gt;
* Compatible with GPT-4 Vision models&lt;br /&gt;
* Good for standard invoice formats&lt;br /&gt;
&lt;br /&gt;
== Configuration Parameters ==&lt;br /&gt;
&lt;br /&gt;
The system requires the following configuration:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;AIVisionEnabled&#039;&#039;&#039;: Enable/disable vision processing (true/false)&lt;br /&gt;
* &#039;&#039;&#039;AIModel&#039;&#039;&#039;: Model identifier string&lt;br /&gt;
* &#039;&#039;&#039;AIModelId&#039;&#039;&#039;: Specific model version (e.g., &amp;quot;claude-sonnet-4-20250514&amp;quot;)&lt;br /&gt;
* &#039;&#039;&#039;AIApiKey&#039;&#039;&#039;: Encrypted API key for AI service authentication&lt;br /&gt;
* &#039;&#039;&#039;FileName&#039;&#039;&#039;: Path to PDF file (when processing single files)&lt;br /&gt;
&lt;br /&gt;
== OCR and Character Recognition ==&lt;br /&gt;
&lt;br /&gt;
The AI system includes intelligent character disambiguation:&lt;br /&gt;
&lt;br /&gt;
=== Common Character Confusions ===&lt;br /&gt;
&lt;br /&gt;
The system is instructed to carefully distinguish between:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Z vs 2&#039;&#039;&#039;: Z has diagonal line, 2 has curves&lt;br /&gt;
* &#039;&#039;&#039;O vs 0&#039;&#039;&#039;: O is round, 0 may have slash or be more oval  &lt;br /&gt;
* &#039;&#039;&#039;I vs 1 vs l&#039;&#039;&#039;: Context-aware recognition (numbers vs letters)&lt;br /&gt;
* &#039;&#039;&#039;S vs 5&#039;&#039;&#039;: Shape and context analysis&lt;br /&gt;
* &#039;&#039;&#039;G vs 6&#039;&#039;&#039;: Character form recognition&lt;br /&gt;
&lt;br /&gt;
=== Validation Checks ===&lt;br /&gt;
&lt;br /&gt;
* Mathematical verification of line totals&lt;br /&gt;
* GST calculation validation (typically 15% in NZ)&lt;br /&gt;
* Cross-checking of subtotals and final amounts&lt;br /&gt;
* Multi-page continuity verification&lt;br /&gt;
&lt;br /&gt;
== Data Extraction Process ==&lt;br /&gt;
&lt;br /&gt;
The system can extract various fields depending on the invoice format:&lt;br /&gt;
&lt;br /&gt;
=== Standard Fields ===&lt;br /&gt;
&lt;br /&gt;
* Invoice Number&lt;br /&gt;
* Invoice Date&lt;br /&gt;
* Company/Supplier Name&lt;br /&gt;
* Client/Customer Name&lt;br /&gt;
* Account Numbers&lt;br /&gt;
* Order Numbers&lt;br /&gt;
* Reference Numbers&lt;br /&gt;
&lt;br /&gt;
=== Financial Totals ===&lt;br /&gt;
&lt;br /&gt;
* Line item amounts&lt;br /&gt;
* Subtotal (excluding GST)&lt;br /&gt;
* GST/Tax amount&lt;br /&gt;
* Total amount (including GST)&lt;br /&gt;
&lt;br /&gt;
=== Line Item Details ===&lt;br /&gt;
&lt;br /&gt;
* Product codes or descriptions&lt;br /&gt;
* Quantities&lt;br /&gt;
* Unit prices&lt;br /&gt;
* Extended amounts&lt;br /&gt;
* Date information (for time-based services)&lt;br /&gt;
* Reference codes or job numbers&lt;br /&gt;
&lt;br /&gt;
=== Custom Fields ===&lt;br /&gt;
&lt;br /&gt;
The AI prompt can be customized to extract additional fields specific to your supplier&#039;s invoice format:&lt;br /&gt;
&lt;br /&gt;
* Vehicle registration numbers&lt;br /&gt;
* Serial numbers&lt;br /&gt;
* Odometer readings&lt;br /&gt;
* Job descriptions&lt;br /&gt;
* Barcode data&lt;br /&gt;
* Custom reference fields&lt;br /&gt;
&lt;br /&gt;
== Invoice Creation Configuration ==&lt;br /&gt;
&lt;br /&gt;
=== Transaction Settings ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Transaction Type&#039;&#039;&#039;: APINV (AP Invoice)&lt;br /&gt;
* &#039;&#039;&#039;Transaction Type Code&#039;&#039;&#039;: 20 (default for AP invoices)&lt;br /&gt;
* &#039;&#039;&#039;Created Date&#039;&#039;&#039;: Defaults to current date/time&lt;br /&gt;
* &#039;&#039;&#039;Created User&#039;&#039;&#039;: Configurable (default: &amp;quot;Admin&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
=== Processing Dates ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Invoice Date&#039;&#039;&#039;: Extracted from PDF&lt;br /&gt;
* &#039;&#039;&#039;Payment Date&#039;&#039;&#039;: Calculated (typically 20th of following month)&lt;br /&gt;
* &#039;&#039;&#039;Process Date&#039;&#039;&#039;: Defaults to current date&lt;br /&gt;
&lt;br /&gt;
=== Account Assignments ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Supplier ID (OtherPartyId)&#039;&#039;&#039;: Must be configured in script&lt;br /&gt;
* &#039;&#039;&#039;Location ID&#039;&#039;&#039;: Configurable (default: &amp;quot;Misc&amp;quot;)&lt;br /&gt;
* &#039;&#039;&#039;Order Number&#039;&#039;&#039;: Extracted from invoice or use reference&lt;br /&gt;
* &#039;&#039;&#039;Reference&#039;&#039;&#039;: Invoice number from PDF&lt;br /&gt;
&lt;br /&gt;
=== Line Item Configuration ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Item ID&#039;&#039;&#039;: Default item code for imported lines (must be configured)&lt;br /&gt;
* &#039;&#039;&#039;Description&#039;&#039;&#039;: Extracted from PDF line items&lt;br /&gt;
* &#039;&#039;&#039;Department&#039;&#039;&#039;: Optional, can be mapped from invoice fields&lt;br /&gt;
* &#039;&#039;&#039;Quantity&#039;&#039;&#039;: Extracted from line items&lt;br /&gt;
* &#039;&#039;&#039;Unit Price&#039;&#039;&#039;: Calculated or extracted&lt;br /&gt;
* &#039;&#039;&#039;GST Treatment&#039;&#039;&#039;: Configurable (GST inclusive or exclusive)&lt;br /&gt;
&lt;br /&gt;
== GST Calculation Methods ==&lt;br /&gt;
&lt;br /&gt;
The system supports different GST calculation approaches:&lt;br /&gt;
&lt;br /&gt;
=== GST Inclusive Amounts ===&lt;br /&gt;
&lt;br /&gt;
When invoice amounts include GST:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Total With GST = Line Amount (as shown on invoice)&lt;br /&gt;
GST Exc Total = Total With GST ÷ 1.15&lt;br /&gt;
GST Amount = Total With GST - GST Exc Total&lt;br /&gt;
Unit Price = GST Exc Total ÷ Quantity&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== GST Exclusive Amounts ===&lt;br /&gt;
&lt;br /&gt;
When invoice amounts exclude GST:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
GST Exc Total = Line Amount (as shown on invoice)&lt;br /&gt;
Total With GST = GST Exc Total × 1.15&lt;br /&gt;
GST Amount = Total With GST - GST Exc Total&lt;br /&gt;
Unit Price = GST Exc Total ÷ Quantity&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Multi-Page Invoice Handling ==&lt;br /&gt;
&lt;br /&gt;
The system is designed to handle multi-page invoices:&lt;br /&gt;
&lt;br /&gt;
* Processes all pages of the PDF document&lt;br /&gt;
* Extracts line items spanning multiple pages&lt;br /&gt;
* Maintains line item sequence&lt;br /&gt;
* Validates totals across entire document&lt;br /&gt;
* Provides page count in extraction results&lt;br /&gt;
&lt;br /&gt;
== Batch Processing ==&lt;br /&gt;
&lt;br /&gt;
=== Batch Creation ===&lt;br /&gt;
&lt;br /&gt;
Optional batch grouping for related invoices:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch ID&#039;&#039;&#039;: Defaults to supplier ID or can be specified&lt;br /&gt;
* &#039;&#039;&#039;Batch Comment&#039;&#039;&#039;: Descriptive text for the batch&lt;br /&gt;
* &#039;&#039;&#039;Automatic Batching&#039;&#039;&#039;: Groups invoices by supplier&lt;br /&gt;
&lt;br /&gt;
=== Batch Benefits ===&lt;br /&gt;
&lt;br /&gt;
* Groups related invoices for review&lt;br /&gt;
* Simplifies posting process&lt;br /&gt;
* Maintains audit trail&lt;br /&gt;
* Enables bulk approval workflows&lt;br /&gt;
&lt;br /&gt;
== Error Handling ==&lt;br /&gt;
&lt;br /&gt;
The system provides comprehensive error handling:&lt;br /&gt;
&lt;br /&gt;
=== File Selection Errors ===&lt;br /&gt;
&lt;br /&gt;
* No file selected&lt;br /&gt;
* Invalid file type (non-PDF)&lt;br /&gt;
* File not found or inaccessible&lt;br /&gt;
* Corrupted PDF files&lt;br /&gt;
&lt;br /&gt;
=== AI Processing Errors ===&lt;br /&gt;
&lt;br /&gt;
* AI service connection failures&lt;br /&gt;
* Invalid or empty AI responses&lt;br /&gt;
* JSON parsing errors&lt;br /&gt;
* Incomplete data extraction&lt;br /&gt;
&lt;br /&gt;
=== Data Validation Errors ===&lt;br /&gt;
&lt;br /&gt;
* Missing required fields&lt;br /&gt;
* Invalid date formats&lt;br /&gt;
* Mathematical inconsistencies&lt;br /&gt;
* Zero or negative amounts&lt;br /&gt;
&lt;br /&gt;
=== Invoice Creation Errors ===&lt;br /&gt;
&lt;br /&gt;
* Invalid supplier ID&lt;br /&gt;
* Missing item master data&lt;br /&gt;
* Missing department codes&lt;br /&gt;
* Transaction validation failures&lt;br /&gt;
* Database constraint violations&lt;br /&gt;
&lt;br /&gt;
== Customizing the AI Prompt ==&lt;br /&gt;
&lt;br /&gt;
The extraction prompt can be customized for specific invoice formats:&lt;br /&gt;
&lt;br /&gt;
=== Prompt Structure ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
1. Role definition (extraction agent)&lt;br /&gt;
2. Task description (extract invoice data)&lt;br /&gt;
3. OCR guidance (character disambiguation)&lt;br /&gt;
4. Field list (specific fields to extract)&lt;br /&gt;
5. JSON schema (output format)&lt;br /&gt;
6. Validation rules (totals, dates, etc.)&lt;br /&gt;
7. Output constraints (no extra text, no markdown)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Customization Examples ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Adding Custom Fields:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Add fields to the JSON schema in the prompt to extract additional data specific to your invoices.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Changing Date Formats:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Specify date format requirements in the prompt (e.g., &amp;quot;yyyy-MM-dd&amp;quot;, &amp;quot;dd/MM/yyyy&amp;quot;).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Field Name Variations:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Provide alternative field names the AI should recognize (e.g., &amp;quot;Cust. Order No&amp;quot;, &amp;quot;Customer Order&amp;quot;, &amp;quot;Order Ref&amp;quot;).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Calculation Rules:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Specify how amounts should be calculated or validated.&lt;br /&gt;
&lt;br /&gt;
== Implementation Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Basic Implementation Steps ===&lt;br /&gt;
&lt;br /&gt;
# Configure AI provider and API credentials&lt;br /&gt;
# Set default supplier ID and item codes&lt;br /&gt;
# Customize extraction prompt for invoice format&lt;br /&gt;
# Configure GST calculation method&lt;br /&gt;
# Set location and account defaults&lt;br /&gt;
# Test with sample invoices&lt;br /&gt;
# Review and validate created invoices&lt;br /&gt;
# Adjust prompt and settings as needed&lt;br /&gt;
&lt;br /&gt;
=== Testing Recommendations ===&lt;br /&gt;
&lt;br /&gt;
* Start with clear, simple invoices&lt;br /&gt;
* Verify mathematical accuracy of extractions&lt;br /&gt;
* Check department and item code assignments&lt;br /&gt;
* Validate date parsing and calculations&lt;br /&gt;
* Test multi-page invoice handling&lt;br /&gt;
* Review batch creation behavior&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Invoice Preparation ===&lt;br /&gt;
&lt;br /&gt;
* Use clear, readable PDF scans&lt;br /&gt;
* Ensure full pages are captured&lt;br /&gt;
* Avoid skewed or rotated scans&lt;br /&gt;
* Check PDF file integrity before processing&lt;br /&gt;
* Process similar invoice types together&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
&lt;br /&gt;
* Set appropriate default values for all parameters&lt;br /&gt;
* Use descriptive batch comments&lt;br /&gt;
* Configure supplier-specific item codes&lt;br /&gt;
* Validate master data prerequisites&lt;br /&gt;
* Document custom prompt modifications&lt;br /&gt;
&lt;br /&gt;
=== Data Quality ===&lt;br /&gt;
&lt;br /&gt;
* Review AI-extracted data before posting&lt;br /&gt;
* Verify mathematical calculations&lt;br /&gt;
* Check supplier ID assignments&lt;br /&gt;
* Validate department code mapping&lt;br /&gt;
* Confirm date calculations&lt;br /&gt;
&lt;br /&gt;
=== Performance ===&lt;br /&gt;
&lt;br /&gt;
* Process invoices in reasonable batch sizes&lt;br /&gt;
* Monitor AI service response times&lt;br /&gt;
* Handle errors gracefully with clear messages&lt;br /&gt;
* Log processing results for audit trails&lt;br /&gt;
&lt;br /&gt;
== Advanced Features ==&lt;br /&gt;
&lt;br /&gt;
=== JSON Response Extraction ===&lt;br /&gt;
&lt;br /&gt;
The system includes a helper method to extract clean JSON from AI responses:&lt;br /&gt;
&lt;br /&gt;
* Navigates AI response structure&lt;br /&gt;
* Extracts text content from nested JSON&lt;br /&gt;
* Handles various response formats&lt;br /&gt;
* Provides error handling for malformed responses&lt;br /&gt;
&lt;br /&gt;
=== Dynamic Field Mapping ===&lt;br /&gt;
&lt;br /&gt;
The system can map extracted fields to invoice line items:&lt;br /&gt;
&lt;br /&gt;
* Product codes to item IDs&lt;br /&gt;
* Reference numbers to departments&lt;br /&gt;
* Custom fields to standard accounting fields&lt;br /&gt;
* Date parsing and conversion&lt;br /&gt;
&lt;br /&gt;
=== Calculation Flexibility ===&lt;br /&gt;
&lt;br /&gt;
Supports various calculation scenarios:&lt;br /&gt;
&lt;br /&gt;
* Zero-quantity items (single services)&lt;br /&gt;
* Division by zero protection&lt;br /&gt;
* Rounding rules for currency&lt;br /&gt;
* Tax-inclusive vs tax-exclusive amounts&lt;br /&gt;
&lt;br /&gt;
== Integration Points ==&lt;br /&gt;
&lt;br /&gt;
The PDF Import System integrates with:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Item Management&#039;&#039;&#039;: Item code lookup and validation&lt;br /&gt;
* &#039;&#039;&#039;Department Management&#039;&#039;&#039;: Department code resolution&lt;br /&gt;
* &#039;&#039;&#039;Supplier Management&#039;&#039;&#039;: Supplier/vendor record validation&lt;br /&gt;
* &#039;&#039;&#039;Transaction Processing&#039;&#039;&#039;: Invoice creation and persistence&lt;br /&gt;
* &#039;&#039;&#039;Batch Management&#039;&#039;&#039;: Batch header creation and tracking&lt;br /&gt;
* &#039;&#039;&#039;AI Vision Services&#039;&#039;&#039;: External API for document analysis&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
* API keys are encrypted using 128-bit encryption&lt;br /&gt;
* File access restricted to allowed paths&lt;br /&gt;
* User authentication tracked for created invoices&lt;br /&gt;
* Audit trail maintained for all imports&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Common Issues ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: AI extracts incorrect invoice numbers&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Add specific field location hints in prompt, emphasize OCR character disambiguation&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: Missing line items from multi-page invoices&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Ensure prompt explicitly mentions checking all pages, verify PDF page count&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: GST calculations don&#039;t match&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Verify GST inclusive/exclusive setting matches invoice format&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: Department codes not assigned&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Check department master data exists, verify field mapping in script&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: JSON deserialization errors&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Check AI response format, verify date format compatibility, review JSON schema&lt;br /&gt;
&lt;br /&gt;
== Future Enhancements ==&lt;br /&gt;
&lt;br /&gt;
Potential improvements for consideration:&lt;br /&gt;
&lt;br /&gt;
* Automatic supplier detection and matching&lt;br /&gt;
* Learning from correction patterns&lt;br /&gt;
* Support for additional currencies&lt;br /&gt;
* Purchase order matching (three-way matching)&lt;br /&gt;
* Email-based invoice submission&lt;br /&gt;
* Duplicate invoice detection&lt;br /&gt;
* Confidence scoring for extracted data&lt;br /&gt;
* Interactive review and correction interface&lt;br /&gt;
* Export of extraction results for verification&lt;br /&gt;
* Batch progress tracking and reporting&lt;br /&gt;
&lt;br /&gt;
[[index.php?title=Category:Scripting]]&lt;br /&gt;
[[index.php?title=Category:Import Functions]]&lt;br /&gt;
[[index.php?title=Category:AI Features]]&lt;br /&gt;
[[index.php?title=Category:Accounts Payable]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=130</id>
		<title>Import PDF File Scripting - AI Importing</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=130"/>
		<updated>2025-11-10T03:26:24Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{ScriptingNav}}&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The AI-Powered PDF Invoice Import System enables automated extraction and processing of supplier invoices from PDF documents using artificial intelligence. The system uses vision AI models to read invoice PDFs and automatically create AP (Accounts Payable) invoices in the accounting system, eliminating manual data entry.&lt;br /&gt;
&lt;br /&gt;
== System Capabilities ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Multi-file processing&#039;&#039;&#039;: Select and process multiple PDF invoices in a single operation&lt;br /&gt;
* &#039;&#039;&#039;AI vision processing&#039;&#039;&#039;: Leverages advanced AI models to read and interpret invoice documents&lt;br /&gt;
* &#039;&#039;&#039;Flexible extraction&#039;&#039;&#039;: Adapts to different invoice formats and layouts automatically&lt;br /&gt;
* &#039;&#039;&#039;OCR accuracy&#039;&#039;&#039;: Intelligent character recognition with disambiguation of similar characters&lt;br /&gt;
* &#039;&#039;&#039;Mathematical validation&#039;&#039;&#039;: Verifies totals and GST calculations for data integrity&lt;br /&gt;
* &#039;&#039;&#039;Automated invoice creation&#039;&#039;&#039;: Generates complete AP invoices with all line items&lt;br /&gt;
* &#039;&#039;&#039;Batch support&#039;&#039;&#039;: Optional batch processing for grouping related supplier invoices&lt;br /&gt;
&lt;br /&gt;
== AI Processing Methods ==&lt;br /&gt;
&lt;br /&gt;
The system supports two processing approaches:&lt;br /&gt;
&lt;br /&gt;
=== Vision-Based Processing (Recommended) ===&lt;br /&gt;
&lt;br /&gt;
Processes the PDF directly using AI vision models:&lt;br /&gt;
&lt;br /&gt;
* Analyzes the visual layout and structure of the invoice&lt;br /&gt;
* Handles complex formatting, tables, and multi-column layouts&lt;br /&gt;
* Works with scanned documents and image-based PDFs&lt;br /&gt;
* Processes multi-page invoices as a complete document&lt;br /&gt;
* More accurate for invoices with complex layouts&lt;br /&gt;
&lt;br /&gt;
=== Text Extraction Processing ===&lt;br /&gt;
&lt;br /&gt;
Extracts text from PDF then processes with AI:&lt;br /&gt;
&lt;br /&gt;
* Uses GemBox libraries to extract structured text&lt;br /&gt;
* Suitable for text-based PDFs with simple layouts&lt;br /&gt;
* Faster processing for straightforward invoices&lt;br /&gt;
* May struggle with complex table layouts or scanned documents&lt;br /&gt;
&lt;br /&gt;
== Supported AI Providers ==&lt;br /&gt;
&lt;br /&gt;
=== Anthropic (Claude) ===&lt;br /&gt;
&lt;br /&gt;
* Default and recommended provider&lt;br /&gt;
* Excellent document understanding capabilities&lt;br /&gt;
* Strong structured data extraction&lt;br /&gt;
* Handles complex invoice layouts&lt;br /&gt;
&lt;br /&gt;
=== OpenAI (GPT Vision) ===&lt;br /&gt;
&lt;br /&gt;
* Alternative vision processing option&lt;br /&gt;
* Compatible with GPT-4 Vision models&lt;br /&gt;
* Good for standard invoice formats&lt;br /&gt;
&lt;br /&gt;
== Configuration Parameters ==&lt;br /&gt;
&lt;br /&gt;
The system requires the following configuration:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;AIVisionEnabled&#039;&#039;&#039;: Enable/disable vision processing (true/false)&lt;br /&gt;
* &#039;&#039;&#039;AIModel&#039;&#039;&#039;: Model identifier string&lt;br /&gt;
* &#039;&#039;&#039;AIModelId&#039;&#039;&#039;: Specific model version (e.g., &amp;quot;claude-sonnet-4-20250514&amp;quot;)&lt;br /&gt;
* &#039;&#039;&#039;AIApiKey&#039;&#039;&#039;: Encrypted API key for AI service authentication&lt;br /&gt;
* &#039;&#039;&#039;FileName&#039;&#039;&#039;: Path to PDF file (when processing single files)&lt;br /&gt;
&lt;br /&gt;
== OCR and Character Recognition ==&lt;br /&gt;
&lt;br /&gt;
The AI system includes intelligent character disambiguation:&lt;br /&gt;
&lt;br /&gt;
=== Common Character Confusions ===&lt;br /&gt;
&lt;br /&gt;
The system is instructed to carefully distinguish between:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Z vs 2&#039;&#039;&#039;: Z has diagonal line, 2 has curves&lt;br /&gt;
* &#039;&#039;&#039;O vs 0&#039;&#039;&#039;: O is round, 0 may have slash or be more oval  &lt;br /&gt;
* &#039;&#039;&#039;I vs 1 vs l&#039;&#039;&#039;: Context-aware recognition (numbers vs letters)&lt;br /&gt;
* &#039;&#039;&#039;S vs 5&#039;&#039;&#039;: Shape and context analysis&lt;br /&gt;
* &#039;&#039;&#039;G vs 6&#039;&#039;&#039;: Character form recognition&lt;br /&gt;
&lt;br /&gt;
=== Validation Checks ===&lt;br /&gt;
&lt;br /&gt;
* Mathematical verification of line totals&lt;br /&gt;
* GST calculation validation (typically 15% in NZ)&lt;br /&gt;
* Cross-checking of subtotals and final amounts&lt;br /&gt;
* Multi-page continuity verification&lt;br /&gt;
&lt;br /&gt;
== Data Extraction Process ==&lt;br /&gt;
&lt;br /&gt;
The system can extract various fields depending on the invoice format:&lt;br /&gt;
&lt;br /&gt;
=== Standard Fields ===&lt;br /&gt;
&lt;br /&gt;
* Invoice Number&lt;br /&gt;
* Invoice Date&lt;br /&gt;
* Company/Supplier Name&lt;br /&gt;
* Client/Customer Name&lt;br /&gt;
* Account Numbers&lt;br /&gt;
* Order Numbers&lt;br /&gt;
* Reference Numbers&lt;br /&gt;
&lt;br /&gt;
=== Financial Totals ===&lt;br /&gt;
&lt;br /&gt;
* Line item amounts&lt;br /&gt;
* Subtotal (excluding GST)&lt;br /&gt;
* GST/Tax amount&lt;br /&gt;
* Total amount (including GST)&lt;br /&gt;
&lt;br /&gt;
=== Line Item Details ===&lt;br /&gt;
&lt;br /&gt;
* Product codes or descriptions&lt;br /&gt;
* Quantities&lt;br /&gt;
* Unit prices&lt;br /&gt;
* Extended amounts&lt;br /&gt;
* Date information (for time-based services)&lt;br /&gt;
* Reference codes or job numbers&lt;br /&gt;
&lt;br /&gt;
=== Custom Fields ===&lt;br /&gt;
&lt;br /&gt;
The AI prompt can be customized to extract additional fields specific to your supplier&#039;s invoice format:&lt;br /&gt;
&lt;br /&gt;
* Vehicle registration numbers&lt;br /&gt;
* Serial numbers&lt;br /&gt;
* Odometer readings&lt;br /&gt;
* Job descriptions&lt;br /&gt;
* Barcode data&lt;br /&gt;
* Custom reference fields&lt;br /&gt;
&lt;br /&gt;
== Invoice Creation Configuration ==&lt;br /&gt;
&lt;br /&gt;
=== Transaction Settings ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Transaction Type&#039;&#039;&#039;: APINV (AP Invoice)&lt;br /&gt;
* &#039;&#039;&#039;Transaction Type Code&#039;&#039;&#039;: 20 (default for AP invoices)&lt;br /&gt;
* &#039;&#039;&#039;Created Date&#039;&#039;&#039;: Defaults to current date/time&lt;br /&gt;
* &#039;&#039;&#039;Created User&#039;&#039;&#039;: Configurable (default: &amp;quot;Admin&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
=== Processing Dates ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Invoice Date&#039;&#039;&#039;: Extracted from PDF&lt;br /&gt;
* &#039;&#039;&#039;Payment Date&#039;&#039;&#039;: Calculated (typically 20th of following month)&lt;br /&gt;
* &#039;&#039;&#039;Process Date&#039;&#039;&#039;: Defaults to current date&lt;br /&gt;
&lt;br /&gt;
=== Account Assignments ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Supplier ID (OtherPartyId)&#039;&#039;&#039;: Must be configured in script&lt;br /&gt;
* &#039;&#039;&#039;Location ID&#039;&#039;&#039;: Configurable (default: &amp;quot;Misc&amp;quot;)&lt;br /&gt;
* &#039;&#039;&#039;Order Number&#039;&#039;&#039;: Extracted from invoice or use reference&lt;br /&gt;
* &#039;&#039;&#039;Reference&#039;&#039;&#039;: Invoice number from PDF&lt;br /&gt;
&lt;br /&gt;
=== Line Item Configuration ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Item ID&#039;&#039;&#039;: Default item code for imported lines (must be configured)&lt;br /&gt;
* &#039;&#039;&#039;Description&#039;&#039;&#039;: Extracted from PDF line items&lt;br /&gt;
* &#039;&#039;&#039;Department&#039;&#039;&#039;: Optional, can be mapped from invoice fields&lt;br /&gt;
* &#039;&#039;&#039;Quantity&#039;&#039;&#039;: Extracted from line items&lt;br /&gt;
* &#039;&#039;&#039;Unit Price&#039;&#039;&#039;: Calculated or extracted&lt;br /&gt;
* &#039;&#039;&#039;GST Treatment&#039;&#039;&#039;: Configurable (GST inclusive or exclusive)&lt;br /&gt;
&lt;br /&gt;
== GST Calculation Methods ==&lt;br /&gt;
&lt;br /&gt;
The system supports different GST calculation approaches:&lt;br /&gt;
&lt;br /&gt;
=== GST Inclusive Amounts ===&lt;br /&gt;
&lt;br /&gt;
When invoice amounts include GST:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Total With GST = Line Amount (as shown on invoice)&lt;br /&gt;
GST Exc Total = Total With GST ÷ 1.15&lt;br /&gt;
GST Amount = Total With GST - GST Exc Total&lt;br /&gt;
Unit Price = GST Exc Total ÷ Quantity&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== GST Exclusive Amounts ===&lt;br /&gt;
&lt;br /&gt;
When invoice amounts exclude GST:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
GST Exc Total = Line Amount (as shown on invoice)&lt;br /&gt;
Total With GST = GST Exc Total × 1.15&lt;br /&gt;
GST Amount = Total With GST - GST Exc Total&lt;br /&gt;
Unit Price = GST Exc Total ÷ Quantity&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Multi-Page Invoice Handling ==&lt;br /&gt;
&lt;br /&gt;
The system is designed to handle multi-page invoices:&lt;br /&gt;
&lt;br /&gt;
* Processes all pages of the PDF document&lt;br /&gt;
* Extracts line items spanning multiple pages&lt;br /&gt;
* Maintains line item sequence&lt;br /&gt;
* Validates totals across entire document&lt;br /&gt;
* Provides page count in extraction results&lt;br /&gt;
&lt;br /&gt;
== Batch Processing ==&lt;br /&gt;
&lt;br /&gt;
=== Batch Creation ===&lt;br /&gt;
&lt;br /&gt;
Optional batch grouping for related invoices:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch ID&#039;&#039;&#039;: Defaults to supplier ID or can be specified&lt;br /&gt;
* &#039;&#039;&#039;Batch Comment&#039;&#039;&#039;: Descriptive text for the batch&lt;br /&gt;
* &#039;&#039;&#039;Automatic Batching&#039;&#039;&#039;: Groups invoices by supplier&lt;br /&gt;
&lt;br /&gt;
=== Batch Benefits ===&lt;br /&gt;
&lt;br /&gt;
* Groups related invoices for review&lt;br /&gt;
* Simplifies posting process&lt;br /&gt;
* Maintains audit trail&lt;br /&gt;
* Enables bulk approval workflows&lt;br /&gt;
&lt;br /&gt;
== Error Handling ==&lt;br /&gt;
&lt;br /&gt;
The system provides comprehensive error handling:&lt;br /&gt;
&lt;br /&gt;
=== File Selection Errors ===&lt;br /&gt;
&lt;br /&gt;
* No file selected&lt;br /&gt;
* Invalid file type (non-PDF)&lt;br /&gt;
* File not found or inaccessible&lt;br /&gt;
* Corrupted PDF files&lt;br /&gt;
&lt;br /&gt;
=== AI Processing Errors ===&lt;br /&gt;
&lt;br /&gt;
* AI service connection failures&lt;br /&gt;
* Invalid or empty AI responses&lt;br /&gt;
* JSON parsing errors&lt;br /&gt;
* Incomplete data extraction&lt;br /&gt;
&lt;br /&gt;
=== Data Validation Errors ===&lt;br /&gt;
&lt;br /&gt;
* Missing required fields&lt;br /&gt;
* Invalid date formats&lt;br /&gt;
* Mathematical inconsistencies&lt;br /&gt;
* Zero or negative amounts&lt;br /&gt;
&lt;br /&gt;
=== Invoice Creation Errors ===&lt;br /&gt;
&lt;br /&gt;
* Invalid supplier ID&lt;br /&gt;
* Missing item master data&lt;br /&gt;
* Missing department codes&lt;br /&gt;
* Transaction validation failures&lt;br /&gt;
* Database constraint violations&lt;br /&gt;
&lt;br /&gt;
== Customizing the AI Prompt ==&lt;br /&gt;
&lt;br /&gt;
The extraction prompt can be customized for specific invoice formats:&lt;br /&gt;
&lt;br /&gt;
=== Prompt Structure ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
1. Role definition (extraction agent)&lt;br /&gt;
2. Task description (extract invoice data)&lt;br /&gt;
3. OCR guidance (character disambiguation)&lt;br /&gt;
4. Field list (specific fields to extract)&lt;br /&gt;
5. JSON schema (output format)&lt;br /&gt;
6. Validation rules (totals, dates, etc.)&lt;br /&gt;
7. Output constraints (no extra text, no markdown)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Customization Examples ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Adding Custom Fields:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Add fields to the JSON schema in the prompt to extract additional data specific to your invoices.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Changing Date Formats:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Specify date format requirements in the prompt (e.g., &amp;quot;yyyy-MM-dd&amp;quot;, &amp;quot;dd/MM/yyyy&amp;quot;).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Field Name Variations:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Provide alternative field names the AI should recognize (e.g., &amp;quot;Cust. Order No&amp;quot;, &amp;quot;Customer Order&amp;quot;, &amp;quot;Order Ref&amp;quot;).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Calculation Rules:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Specify how amounts should be calculated or validated.&lt;br /&gt;
&lt;br /&gt;
== Implementation Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Basic Implementation Steps ===&lt;br /&gt;
&lt;br /&gt;
# Configure AI provider and API credentials&lt;br /&gt;
# Set default supplier ID and item codes&lt;br /&gt;
# Customize extraction prompt for invoice format&lt;br /&gt;
# Configure GST calculation method&lt;br /&gt;
# Set location and account defaults&lt;br /&gt;
# Test with sample invoices&lt;br /&gt;
# Review and validate created invoices&lt;br /&gt;
# Adjust prompt and settings as needed&lt;br /&gt;
&lt;br /&gt;
=== Testing Recommendations ===&lt;br /&gt;
&lt;br /&gt;
* Start with clear, simple invoices&lt;br /&gt;
* Verify mathematical accuracy of extractions&lt;br /&gt;
* Check department and item code assignments&lt;br /&gt;
* Validate date parsing and calculations&lt;br /&gt;
* Test multi-page invoice handling&lt;br /&gt;
* Review batch creation behavior&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Invoice Preparation ===&lt;br /&gt;
&lt;br /&gt;
* Use clear, readable PDF scans&lt;br /&gt;
* Ensure full pages are captured&lt;br /&gt;
* Avoid skewed or rotated scans&lt;br /&gt;
* Check PDF file integrity before processing&lt;br /&gt;
* Process similar invoice types together&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
&lt;br /&gt;
* Set appropriate default values for all parameters&lt;br /&gt;
* Use descriptive batch comments&lt;br /&gt;
* Configure supplier-specific item codes&lt;br /&gt;
* Validate master data prerequisites&lt;br /&gt;
* Document custom prompt modifications&lt;br /&gt;
&lt;br /&gt;
=== Data Quality ===&lt;br /&gt;
&lt;br /&gt;
* Review AI-extracted data before posting&lt;br /&gt;
* Verify mathematical calculations&lt;br /&gt;
* Check supplier ID assignments&lt;br /&gt;
* Validate department code mapping&lt;br /&gt;
* Confirm date calculations&lt;br /&gt;
&lt;br /&gt;
=== Performance ===&lt;br /&gt;
&lt;br /&gt;
* Process invoices in reasonable batch sizes&lt;br /&gt;
* Monitor AI service response times&lt;br /&gt;
* Handle errors gracefully with clear messages&lt;br /&gt;
* Log processing results for audit trails&lt;br /&gt;
&lt;br /&gt;
== Advanced Features ==&lt;br /&gt;
&lt;br /&gt;
=== JSON Response Extraction ===&lt;br /&gt;
&lt;br /&gt;
The system includes a helper method to extract clean JSON from AI responses:&lt;br /&gt;
&lt;br /&gt;
* Navigates AI response structure&lt;br /&gt;
* Extracts text content from nested JSON&lt;br /&gt;
* Handles various response formats&lt;br /&gt;
* Provides error handling for malformed responses&lt;br /&gt;
&lt;br /&gt;
=== Dynamic Field Mapping ===&lt;br /&gt;
&lt;br /&gt;
The system can map extracted fields to invoice line items:&lt;br /&gt;
&lt;br /&gt;
* Product codes to item IDs&lt;br /&gt;
* Reference numbers to departments&lt;br /&gt;
* Custom fields to standard accounting fields&lt;br /&gt;
* Date parsing and conversion&lt;br /&gt;
&lt;br /&gt;
=== Calculation Flexibility ===&lt;br /&gt;
&lt;br /&gt;
Supports various calculation scenarios:&lt;br /&gt;
&lt;br /&gt;
* Zero-quantity items (single services)&lt;br /&gt;
* Division by zero protection&lt;br /&gt;
* Rounding rules for currency&lt;br /&gt;
* Tax-inclusive vs tax-exclusive amounts&lt;br /&gt;
&lt;br /&gt;
== Integration Points ==&lt;br /&gt;
&lt;br /&gt;
The PDF Import System integrates with:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Item Management&#039;&#039;&#039;: Item code lookup and validation&lt;br /&gt;
* &#039;&#039;&#039;Department Management&#039;&#039;&#039;: Department code resolution&lt;br /&gt;
* &#039;&#039;&#039;Supplier Management&#039;&#039;&#039;: Supplier/vendor record validation&lt;br /&gt;
* &#039;&#039;&#039;Transaction Processing&#039;&#039;&#039;: Invoice creation and persistence&lt;br /&gt;
* &#039;&#039;&#039;Batch Management&#039;&#039;&#039;: Batch header creation and tracking&lt;br /&gt;
* &#039;&#039;&#039;AI Vision Services&#039;&#039;&#039;: External API for document analysis&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
* API keys are encrypted using 128-bit encryption&lt;br /&gt;
* File access restricted to allowed paths&lt;br /&gt;
* User authentication tracked for created invoices&lt;br /&gt;
* Audit trail maintained for all imports&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Common Issues ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: AI extracts incorrect invoice numbers&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Add specific field location hints in prompt, emphasize OCR character disambiguation&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: Missing line items from multi-page invoices&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Ensure prompt explicitly mentions checking all pages, verify PDF page count&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: GST calculations don&#039;t match&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Verify GST inclusive/exclusive setting matches invoice format&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: Department codes not assigned&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Check department master data exists, verify field mapping in script&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Problem&#039;&#039;&#039;: JSON deserialization errors&amp;lt;br/&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Check AI response format, verify date format compatibility, review JSON schema&lt;br /&gt;
&lt;br /&gt;
== Future Enhancements ==&lt;br /&gt;
&lt;br /&gt;
Potential improvements for consideration:&lt;br /&gt;
&lt;br /&gt;
* Automatic supplier detection and matching&lt;br /&gt;
* Learning from correction patterns&lt;br /&gt;
* Support for additional currencies&lt;br /&gt;
* Purchase order matching (three-way matching)&lt;br /&gt;
* Email-based invoice submission&lt;br /&gt;
* Duplicate invoice detection&lt;br /&gt;
* Confidence scoring for extracted data&lt;br /&gt;
* Interactive review and correction interface&lt;br /&gt;
* Export of extraction results for verification&lt;br /&gt;
* Batch progress tracking and reporting&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* [[Import Scripting]]&lt;br /&gt;
* [[Transaction Processing]]&lt;br /&gt;
* [[AI Vision Services]]&lt;br /&gt;
* [[Batch Processing]]&lt;br /&gt;
* [[Supplier Management]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Scripting]]&lt;br /&gt;
[[Category:Import Functions]]&lt;br /&gt;
[[Category:AI Features]]&lt;br /&gt;
[[Category:Accounts Payable]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=129</id>
		<title>Import PDF File Scripting - AI Importing</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=129"/>
		<updated>2025-11-10T03:23:10Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{ScriptingNav}}&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The Invoice PDF Import System provides automated processing of supplier invoices using AI-powered document analysis. The system extracts invoice data from PDF files and creates AP (Accounts Payable) invoices in the accounting system with minimal manual intervention.&lt;br /&gt;
&lt;br /&gt;
== Key Features ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Multi-file processing&#039;&#039;&#039;: Process multiple PDF invoices in a single operation&lt;br /&gt;
* &#039;&#039;&#039;AI-powered extraction&#039;&#039;&#039;: Uses vision AI models to read and interpret invoice documents&lt;br /&gt;
* &#039;&#039;&#039;Invoice type detection&#039;&#039;&#039;: Automatically distinguishes between Service and Parts invoices&lt;br /&gt;
* &#039;&#039;&#039;Data validation&#039;&#039;&#039;: Performs mathematical verification of totals and line items&lt;br /&gt;
* &#039;&#039;&#039;Automated invoice creation&#039;&#039;&#039;: Generates complete AP invoices with line items&lt;br /&gt;
* &#039;&#039;&#039;Batch processing&#039;&#039;&#039;: Optional batch creation for grouping related invoices&lt;br /&gt;
&lt;br /&gt;
== Invoice Types ==&lt;br /&gt;
&lt;br /&gt;
=== Service Invoices ===&lt;br /&gt;
&lt;br /&gt;
Service invoices contain work performed on specific vehicles or equipment. Key characteristics:&lt;br /&gt;
&lt;br /&gt;
* Include vehicle/equipment identifiers (Serial Number, Make, Model, Meter Reading)&lt;br /&gt;
* Contain detailed service description text&lt;br /&gt;
* Service description may span multiple pages&lt;br /&gt;
* Serial Number is used as the Department identifier&lt;br /&gt;
&lt;br /&gt;
=== Parts Invoices ===&lt;br /&gt;
&lt;br /&gt;
Parts invoices list individual components or products purchased. Key characteristics:&lt;br /&gt;
&lt;br /&gt;
* Focus on part numbers and quantities&lt;br /&gt;
* No vehicle-specific information&lt;br /&gt;
* Line items include Part Number + Description format&lt;br /&gt;
&lt;br /&gt;
== Data Extraction ==&lt;br /&gt;
&lt;br /&gt;
The system extracts the following information from invoice PDFs:&lt;br /&gt;
&lt;br /&gt;
=== Header Information ===&lt;br /&gt;
&lt;br /&gt;
* Invoice Type (Service/Parts)&lt;br /&gt;
* Invoice Number&lt;br /&gt;
* Invoice Date&lt;br /&gt;
* Order Number (Sales Order for Parts, Purchase Order for Service)&lt;br /&gt;
* Serial Number (Service invoices only)&lt;br /&gt;
* Customer Reference Number&lt;br /&gt;
* Service Description (Service invoices only)&lt;br /&gt;
&lt;br /&gt;
=== Line Items ===&lt;br /&gt;
&lt;br /&gt;
For each line on the invoice:&lt;br /&gt;
&lt;br /&gt;
* Description (Part Number + Description for parts)&lt;br /&gt;
* Quantity&lt;br /&gt;
* Unit Price&lt;br /&gt;
* Extended Total (line total)&lt;br /&gt;
&lt;br /&gt;
=== Financial Totals ===&lt;br /&gt;
&lt;br /&gt;
* Net Subtotal (excluding tax)&lt;br /&gt;
* Total GST/Tax&lt;br /&gt;
* Total Amount (including tax)&lt;br /&gt;
&lt;br /&gt;
== AI Model Configuration ==&lt;br /&gt;
&lt;br /&gt;
The system supports multiple AI vision providers:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Anthropic&#039;&#039;&#039; (default): Claude vision models&lt;br /&gt;
* &#039;&#039;&#039;OpenAI&#039;&#039;&#039;: GPT vision models&lt;br /&gt;
&lt;br /&gt;
Configuration parameters:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;AIModel&amp;lt;/code&amp;gt;: Model identifier string&lt;br /&gt;
* &amp;lt;code&amp;gt;AIModelId&amp;lt;/code&amp;gt;: Specific model version&lt;br /&gt;
* &amp;lt;code&amp;gt;AIVisionEnabled&amp;lt;/code&amp;gt;: Enable/disable AI processing&lt;br /&gt;
* &amp;lt;code&amp;gt;AIApiKey&amp;lt;/code&amp;gt;: Encrypted API key for authentication&lt;br /&gt;
&lt;br /&gt;
== Data Validation ==&lt;br /&gt;
&lt;br /&gt;
The extraction process includes built-in validation:&lt;br /&gt;
&lt;br /&gt;
=== OCR Accuracy ===&lt;br /&gt;
&lt;br /&gt;
* Disambiguates common character confusions (Z/2, O/0, I/1/l, S/5, G/6)&lt;br /&gt;
* Reads all pages of multi-page invoices&lt;br /&gt;
&lt;br /&gt;
=== Mathematical Verification ===&lt;br /&gt;
&lt;br /&gt;
* Verifies sum of line items matches subtotal (within 0.02 tolerance)&lt;br /&gt;
* Confirms GST calculation (≈ 15% of subtotal)&lt;br /&gt;
* Validates total = subtotal + GST&lt;br /&gt;
&lt;br /&gt;
== Invoice Creation Process ==&lt;br /&gt;
&lt;br /&gt;
=== Line Item Processing ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;For Service Invoices:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* Creates a nominal line item with service description&lt;br /&gt;
* Adds offsetting line item for calculation purposes&lt;br /&gt;
* Uses extracted line items for parts/materials used&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;For Parts Invoices:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* Converts each extracted line item to invoice row&lt;br /&gt;
* Calculates GST inclusive totals&lt;br /&gt;
* Computes unit prices from extended totals&lt;br /&gt;
&lt;br /&gt;
=== Department Assignment ===&lt;br /&gt;
&lt;br /&gt;
* Service invoices: Uses Serial Number as Department&lt;br /&gt;
* Parts invoices: Department left blank or as specified&lt;br /&gt;
&lt;br /&gt;
=== Financial Calculations ===&lt;br /&gt;
&lt;br /&gt;
All amounts are stored GST exclusive with separate tax tracking:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
GST Exc Total = Line Extended Total&lt;br /&gt;
Total With GST = GST Exc Total × 1.15&lt;br /&gt;
GST Amount = Total With GST - GST Exc Total&lt;br /&gt;
Unit Price = GST Exc Total ÷ Quantity&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Invoice Record Creation ===&lt;br /&gt;
&lt;br /&gt;
Creates AP invoice with:&lt;br /&gt;
&lt;br /&gt;
* Transaction Type: APINV&lt;br /&gt;
* Vendor/Supplier: OtherPartyId (default: &amp;quot;5521&amp;quot;)&lt;br /&gt;
* Location: Configurable (default: &amp;quot;Misc&amp;quot;)&lt;br /&gt;
* Payment Date: 20th of month following invoice date&lt;br /&gt;
* Reference: Invoice Number from PDF&lt;br /&gt;
* Line items with all pricing and tax details&lt;br /&gt;
&lt;br /&gt;
=== Batch Processing ===&lt;br /&gt;
&lt;br /&gt;
Optional batch creation groups related invoices:&lt;br /&gt;
&lt;br /&gt;
* Batch ID defaults to supplier/vendor ID&lt;br /&gt;
* Includes batch comments for tracking&lt;br /&gt;
* Supports recurring batch patterns&lt;br /&gt;
&lt;br /&gt;
== Error Handling ==&lt;br /&gt;
&lt;br /&gt;
The system includes comprehensive error handling:&lt;br /&gt;
&lt;br /&gt;
=== PDF Processing Errors ===&lt;br /&gt;
&lt;br /&gt;
* Invalid or corrupted PDF files&lt;br /&gt;
* Missing or unreadable pages&lt;br /&gt;
* Unsupported PDF formats&lt;br /&gt;
&lt;br /&gt;
=== Data Extraction Errors ===&lt;br /&gt;
&lt;br /&gt;
* No invoice detected in PDF&lt;br /&gt;
* Invalid JSON response from AI model&lt;br /&gt;
* Missing required fields&lt;br /&gt;
* Multiple invoices in single PDF (selects largest by subtotal)&lt;br /&gt;
&lt;br /&gt;
=== Invoice Creation Errors ===&lt;br /&gt;
&lt;br /&gt;
* Invalid supplier/vendor ID&lt;br /&gt;
* Missing required master data (items, departments)&lt;br /&gt;
* Transaction validation failures&lt;br /&gt;
* Batch creation conflicts&lt;br /&gt;
&lt;br /&gt;
== Usage Guidelines ==&lt;br /&gt;
&lt;br /&gt;
=== File Selection ===&lt;br /&gt;
&lt;br /&gt;
# Use the file picker dialog to select one or more PDF files&lt;br /&gt;
# Only PDF files are supported&lt;br /&gt;
# Multiple files can be processed in sequence&lt;br /&gt;
&lt;br /&gt;
=== Best Practices ===&lt;br /&gt;
&lt;br /&gt;
* Ensure invoice PDFs are clear and readable&lt;br /&gt;
* Verify supplier master data exists before import&lt;br /&gt;
* Check that department codes (serial numbers) are valid&lt;br /&gt;
* Review created invoices for accuracy&lt;br /&gt;
* Use batch processing for related invoices from same supplier&lt;br /&gt;
&lt;br /&gt;
=== Post-Import Actions ===&lt;br /&gt;
&lt;br /&gt;
* Review generated invoices in the system&lt;br /&gt;
* Verify line item details and totals&lt;br /&gt;
* Confirm department assignments&lt;br /&gt;
* Check payment dates&lt;br /&gt;
* Approve batches when ready for posting&lt;br /&gt;
&lt;br /&gt;
== Technical Architecture ==&lt;br /&gt;
&lt;br /&gt;
=== Components ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Import Scripter&#039;&#039;&#039;: Main orchestration class&lt;br /&gt;
* &#039;&#039;&#039;AI Vision Service&#039;&#039;&#039;: PDF analysis and data extraction&lt;br /&gt;
* &#039;&#039;&#039;Invoice Functions&#039;&#039;&#039;: Business logic for invoice creation&lt;br /&gt;
* &#039;&#039;&#039;Transaction Functions&#039;&#039;&#039;: Database persistence layer&lt;br /&gt;
&lt;br /&gt;
=== Data Flow ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PDF File → AI Vision Analysis → JSON Extraction → &lt;br /&gt;
Data Validation → Invoice Row Creation → &lt;br /&gt;
AP Invoice Generation → Database Persistence&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Performance Considerations ===&lt;br /&gt;
&lt;br /&gt;
* Batch loading of items and departments&lt;br /&gt;
* Dictionary-based lookups for master data&lt;br /&gt;
* Minimal database queries during processing&lt;br /&gt;
* Asynchronous AI processing support&lt;br /&gt;
&lt;br /&gt;
== Configuration Requirements ==&lt;br /&gt;
&lt;br /&gt;
=== System Parameters ===&lt;br /&gt;
&lt;br /&gt;
* AI API credentials (encrypted)&lt;br /&gt;
* Default supplier/vendor ID&lt;br /&gt;
* Default location ID&lt;br /&gt;
* Invoice numbering scheme&lt;br /&gt;
* Tax calculation rules (GST rate)&lt;br /&gt;
* Payment terms (default 20 days after month-end)&lt;br /&gt;
&lt;br /&gt;
=== Master Data Prerequisites ===&lt;br /&gt;
&lt;br /&gt;
* Supplier/vendor records&lt;br /&gt;
* Item master data (for parts)&lt;br /&gt;
* Department codes (for service invoices)&lt;br /&gt;
* Location codes&lt;br /&gt;
* Transaction type definitions&lt;br /&gt;
&lt;br /&gt;
== Integration Points ==&lt;br /&gt;
&lt;br /&gt;
The Invoice PDF Import integrates with:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Item Management&#039;&#039;&#039;: Item lookup and validation&lt;br /&gt;
* &#039;&#039;&#039;Department Management&#039;&#039;&#039;: Department code resolution&lt;br /&gt;
* &#039;&#039;&#039;Transaction Processing&#039;&#039;&#039;: Invoice creation and posting&lt;br /&gt;
* &#039;&#039;&#039;Batch Management&#039;&#039;&#039;: Batch header creation and tracking&lt;br /&gt;
* &#039;&#039;&#039;AI Vision Services&#039;&#039;&#039;: External API for document analysis&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* [[Import Scripting]]&lt;br /&gt;
* [[Transaction Processing]]&lt;br /&gt;
* [[AI Vision Services]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Scripting]]&lt;br /&gt;
[[Category:Import Functions]]&lt;br /&gt;
[[Category:AI Features]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=128</id>
		<title>Zinform Wiki Home</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=128"/>
		<updated>2025-11-10T03:21:53Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== &amp;lt;strong&amp;gt;Zinform Main Help Page&amp;lt;/strong&amp;gt; ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Getting started ==&lt;br /&gt;
[[Minimum System Requirements]]&lt;br /&gt;
&lt;br /&gt;
[[Guide to installing Zinform Accounts V6|Installation of Zinform Accounts]]&lt;br /&gt;
&lt;br /&gt;
[[Invoice Printing Direct via New Zinform]]&lt;br /&gt;
&lt;br /&gt;
[[Installing SQL and Zinform]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Features ==&lt;br /&gt;
[[Current Release Features|List of features in current release]]&lt;br /&gt;
&lt;br /&gt;
[[Floating Views]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Scripting ==&lt;br /&gt;
[[Document Scripting|Document scripting]]&lt;br /&gt;
&lt;br /&gt;
[[Import File Scripting|File Import Scripting]]&lt;br /&gt;
&lt;br /&gt;
[[Import PDF File Scripting - AI Importing|PDF Import Scripting - Ai Importing]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Permissions and Roles ==&lt;br /&gt;
[[Permissions and Roles]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=127</id>
		<title>Zinform Wiki Home</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=127"/>
		<updated>2025-11-10T03:21:28Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== &amp;lt;strong&amp;gt;Zinform Main Help Page&amp;lt;/strong&amp;gt; ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Getting started ==&lt;br /&gt;
[[Minimum System Requirements]]&lt;br /&gt;
&lt;br /&gt;
[[Guide to installing Zinform Accounts V6|Installation of Zinform Accounts]]&lt;br /&gt;
&lt;br /&gt;
[[Invoice Printing Direct via New Zinform]]&lt;br /&gt;
&lt;br /&gt;
[[Installing SQL and Zinform]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Features ==&lt;br /&gt;
[[Current Release Features|List of features in current release]]&lt;br /&gt;
&lt;br /&gt;
[[Floating Views]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Scripting ==&lt;br /&gt;
[[Document Scripting|Document scripting]]&lt;br /&gt;
&lt;br /&gt;
[[Import File Scripting|File Import Scripting]]&lt;br /&gt;
&lt;br /&gt;
[[PDF Import Scripting - Ai Importing]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Permissions and Roles ==&lt;br /&gt;
[[Permissions and Roles]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=126</id>
		<title>Zinform Wiki Home</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=126"/>
		<updated>2025-11-10T03:20:52Z</updated>

		<summary type="html">&lt;p&gt;Mike: /* Zinform Accounts 6 Scripting */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== &amp;lt;strong&amp;gt;Zinform Main Help Page&amp;lt;/strong&amp;gt; ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Getting started ==&lt;br /&gt;
[[Minimum System Requirements]]&lt;br /&gt;
&lt;br /&gt;
[[Guide to installing Zinform Accounts V6|Installation of Zinform Accounts]]&lt;br /&gt;
&lt;br /&gt;
[[Invoice Printing Direct via New Zinform]]&lt;br /&gt;
&lt;br /&gt;
[[Installing SQL and Zinform]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Features ==&lt;br /&gt;
[[Current Release Features|List of features in current release]]&lt;br /&gt;
&lt;br /&gt;
[[Floating Views]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Scripting ==&lt;br /&gt;
[[Document Scripting|Document scripting]]&lt;br /&gt;
&lt;br /&gt;
[[File Import Scripting]]&lt;br /&gt;
&lt;br /&gt;
[[PDF Import Scripting - Ai Importing]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Permissions and Roles ==&lt;br /&gt;
[[Permissions and Roles]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Import_File_Scripting&amp;diff=125</id>
		<title>Import File Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Import_File_Scripting&amp;diff=125"/>
		<updated>2025-11-10T03:06:50Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= File Import System Documentation =&lt;br /&gt;
&lt;br /&gt;
This page documents the file import infrastructure in ZinformAccounts, providing templates and patterns for implementing various file format imports including CSV, XML, Excel, JSON, and other data sources.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The import system provides a flexible framework for processing external data files into the ZinformAccounts database. All imports follow consistent patterns for error handling, validation, and transaction management.&lt;br /&gt;
&lt;br /&gt;
== Supported File Formats ==&lt;br /&gt;
&lt;br /&gt;
=== Excel Files (.xlsx, .xls) ===&lt;br /&gt;
&lt;br /&gt;
Using GemBox.Spreadsheet for Excel file processing:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using GemBox.Spreadsheet;&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportExcel&amp;lt;T&amp;gt;(string filePath) where T : new()&lt;br /&gt;
{&lt;br /&gt;
    var workbook = ExcelFile.Load(filePath);&lt;br /&gt;
    var worksheet = workbook.Worksheets[0];&lt;br /&gt;
    var results = new List&amp;lt;T&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    // Get header row to map columns&lt;br /&gt;
    int headerRowIndex = 0;&lt;br /&gt;
    var headers = new Dictionary&amp;lt;int, string&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    for (int col = 0; col &amp;lt; worksheet.CalculateMaxUsedColumns(); col++)&lt;br /&gt;
    {&lt;br /&gt;
        var headerValue = worksheet.Cells[headerRowIndex, col].Value?.ToString();&lt;br /&gt;
        if (!string.IsNullOrEmpty(headerValue))&lt;br /&gt;
            headers[col] = headerValue;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Process data rows&lt;br /&gt;
    for (int row = headerRowIndex + 1; row &amp;lt; worksheet.Rows.Count; row++)&lt;br /&gt;
    {&lt;br /&gt;
        var item = MapRowToObject&amp;lt;T&amp;gt;(worksheet.Rows[row], headers);&lt;br /&gt;
        if (item != null)&lt;br /&gt;
            results.Add(item);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== CSV Files ===&lt;br /&gt;
&lt;br /&gt;
Using CsvHelper for CSV processing with flexible date handling:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using CsvHelper;&lt;br /&gt;
using CsvHelper.Configuration;&lt;br /&gt;
using System.Globalization;&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportCsv&amp;lt;T&amp;gt;(string filePath, bool hasHeader = true)&lt;br /&gt;
{&lt;br /&gt;
    var config = new CsvConfiguration(CultureInfo.InvariantCulture)&lt;br /&gt;
    {&lt;br /&gt;
        HasHeaderRecord = hasHeader,&lt;br /&gt;
        MissingFieldFound = null,&lt;br /&gt;
        BadDataFound = null,&lt;br /&gt;
        TrimOptions = TrimOptions.Trim&lt;br /&gt;
    };&lt;br /&gt;
    &lt;br /&gt;
    using var reader = new StreamReader(filePath);&lt;br /&gt;
    using var csv = new CsvReader(reader, config);&lt;br /&gt;
    &lt;br /&gt;
    // Register custom type converters for dates&lt;br /&gt;
    csv.Context.TypeConverterCache.AddConverter&amp;lt;DateTime&amp;gt;(new FlexibleDateConverter());&lt;br /&gt;
    &lt;br /&gt;
    return csv.GetRecords&amp;lt;T&amp;gt;().ToList();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class FlexibleDateConverter : ITypeConverter&lt;br /&gt;
{&lt;br /&gt;
    private readonly string[] _dateFormats = new[]&lt;br /&gt;
    {&lt;br /&gt;
        &amp;quot;dd/MM/yyyy&amp;quot;,&lt;br /&gt;
        &amp;quot;d/M/yyyy&amp;quot;,&lt;br /&gt;
        &amp;quot;yyyy-MM-dd&amp;quot;,&lt;br /&gt;
        &amp;quot;dd-MM-yyyy&amp;quot;,&lt;br /&gt;
        &amp;quot;dd.MM.yyyy&amp;quot;&lt;br /&gt;
    };&lt;br /&gt;
    &lt;br /&gt;
    public object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)&lt;br /&gt;
    {&lt;br /&gt;
        if (DateTime.TryParseExact(text, _dateFormats, &lt;br /&gt;
            CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))&lt;br /&gt;
            return date;&lt;br /&gt;
        &lt;br /&gt;
        throw new InvalidOperationException($&amp;quot;Cannot parse date: {text}&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== XML Files ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using System.Xml.Linq;&lt;br /&gt;
using System.Xml.Serialization;&lt;br /&gt;
&lt;br /&gt;
// Method 1: Using XDocument for flexible parsing&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportXmlFlexible&amp;lt;T&amp;gt;(string filePath, Func&amp;lt;XElement, T&amp;gt; mapper)&lt;br /&gt;
{&lt;br /&gt;
    var doc = XDocument.Load(filePath);&lt;br /&gt;
    var results = new List&amp;lt;T&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    foreach (var element in doc.Descendants(&amp;quot;Record&amp;quot;))&lt;br /&gt;
    {&lt;br /&gt;
        var item = mapper(element);&lt;br /&gt;
        if (item != null)&lt;br /&gt;
            results.Add(item);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Method 2: Using XML Serialization for strongly-typed objects&lt;br /&gt;
public T ImportXmlSerialized&amp;lt;T&amp;gt;(string filePath) where T : class&lt;br /&gt;
{&lt;br /&gt;
    var serializer = new XmlSerializer(typeof(T));&lt;br /&gt;
    using var stream = File.OpenRead(filePath);&lt;br /&gt;
    return serializer.Deserialize(stream) as T;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== JSON Files ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using Newtonsoft.Json;&lt;br /&gt;
&lt;br /&gt;
public T ImportJson&amp;lt;T&amp;gt;(string filePath)&lt;br /&gt;
{&lt;br /&gt;
    var json = File.ReadAllText(filePath);&lt;br /&gt;
    return JsonConvert.DeserializeObject&amp;lt;T&amp;gt;(json);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportJsonArray&amp;lt;T&amp;gt;(string filePath)&lt;br /&gt;
{&lt;br /&gt;
    var json = File.ReadAllText(filePath);&lt;br /&gt;
    return JsonConvert.DeserializeObject&amp;lt;List&amp;lt;T&amp;gt;&amp;gt;(json) ?? new List&amp;lt;T&amp;gt;();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Tab-Delimited and Fixed-Width Files ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
// Tab-delimited files&lt;br /&gt;
public List&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt; ImportTabDelimited(string filePath)&lt;br /&gt;
{&lt;br /&gt;
    var lines = File.ReadAllLines(filePath);&lt;br /&gt;
    if (lines.Length == 0) return new List&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    var headers = lines[0].Split(&#039;\t&#039;);&lt;br /&gt;
    var results = new List&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    for (int i = 1; i &amp;lt; lines.Length; i++)&lt;br /&gt;
    {&lt;br /&gt;
        var values = lines[i].Split(&#039;\t&#039;);&lt;br /&gt;
        var record = new Dictionary&amp;lt;string, string&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        for (int j = 0; j &amp;lt; Math.Min(headers.Length, values.Length); j++)&lt;br /&gt;
        {&lt;br /&gt;
            record[headers[j]] = values[j];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        results.Add(record);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Fixed-width files&lt;br /&gt;
public class FixedWidthField&lt;br /&gt;
{&lt;br /&gt;
    public string Name { get; set; }&lt;br /&gt;
    public int StartPosition { get; set; }&lt;br /&gt;
    public int Length { get; set; }&lt;br /&gt;
    public Type DataType { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;Dictionary&amp;lt;string, object&amp;gt;&amp;gt; ImportFixedWidth(string filePath, List&amp;lt;FixedWidthField&amp;gt; fields)&lt;br /&gt;
{&lt;br /&gt;
    var lines = File.ReadAllLines(filePath);&lt;br /&gt;
    var results = new List&amp;lt;Dictionary&amp;lt;string, object&amp;gt;&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    foreach (var line in lines)&lt;br /&gt;
    {&lt;br /&gt;
        var record = new Dictionary&amp;lt;string, object&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        foreach (var field in fields)&lt;br /&gt;
        {&lt;br /&gt;
            if (line.Length &amp;gt;= field.StartPosition + field.Length)&lt;br /&gt;
            {&lt;br /&gt;
                var value = line.Substring(field.StartPosition, field.Length).Trim();&lt;br /&gt;
                record[field.Name] = ConvertToType(value, field.DataType);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        results.Add(record);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Generic Import Script Template ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
using System;&lt;br /&gt;
using System.IO;&lt;br /&gt;
using System.Data;&lt;br /&gt;
using System.Text;&lt;br /&gt;
using System.Windows;&lt;br /&gt;
using System.Linq;&lt;br /&gt;
using System.Collections.Generic;&lt;br /&gt;
using ZinBusinessLayer;&lt;br /&gt;
using ZinDATA;&lt;br /&gt;
using ZinBusinessLayer.Models;&lt;br /&gt;
&lt;br /&gt;
public class GenericImportScripter&lt;br /&gt;
{&lt;br /&gt;
    private BusinessLayerMain zinBL;&lt;br /&gt;
    private readonly List&amp;lt;string&amp;gt; errors = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    private readonly List&amp;lt;string&amp;gt; warnings = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    public bool Main(BusinessLayerMain bl, ZinParameters parameters, ZinParameters returnParameters)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        &lt;br /&gt;
        try&lt;br /&gt;
        {&lt;br /&gt;
            // Get parameters&lt;br /&gt;
            var filename = parameters.GetItemString(&amp;quot;FileName&amp;quot;);&lt;br /&gt;
            var importType = parameters.GetItemString(&amp;quot;ImportType&amp;quot;);&lt;br /&gt;
            var validateOnly = parameters.GetItemBool(&amp;quot;ValidateOnly&amp;quot;) ?? false;&lt;br /&gt;
            &lt;br /&gt;
            // Determine file type and import&lt;br /&gt;
            var extension = Path.GetExtension(filename).ToLower();&lt;br /&gt;
            dynamic data = null;&lt;br /&gt;
            &lt;br /&gt;
            switch (extension)&lt;br /&gt;
            {&lt;br /&gt;
                case &amp;quot;.xlsx&amp;quot;:&lt;br /&gt;
                case &amp;quot;.xls&amp;quot;:&lt;br /&gt;
                    data = ImportExcel(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.csv&amp;quot;:&lt;br /&gt;
                    data = ImportCsv(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.xml&amp;quot;:&lt;br /&gt;
                    data = ImportXml(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.json&amp;quot;:&lt;br /&gt;
                    data = ImportJson(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.txt&amp;quot;:&lt;br /&gt;
                    data = DetermineAndImportText(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                default:&lt;br /&gt;
                    throw new NotSupportedException($&amp;quot;File type {extension} is not supported&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Validate data&lt;br /&gt;
            if (!ValidateData(data))&lt;br /&gt;
            {&lt;br /&gt;
                returnParameters.SetItem(&amp;quot;Errors&amp;quot;, string.Join(&amp;quot;\n&amp;quot;, errors));&lt;br /&gt;
                return false;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process data if not validation only&lt;br /&gt;
            if (!validateOnly)&lt;br /&gt;
            {&lt;br /&gt;
                return ProcessData(data, importType);&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            returnParameters.SetItem(&amp;quot;ValidationResult&amp;quot;, &amp;quot;Data validated successfully&amp;quot;);&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
        catch (Exception ex)&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItem(&amp;quot;Error&amp;quot;, $&amp;quot;Import failed: {ex.Message}&amp;quot;);&lt;br /&gt;
            LogError($&amp;quot;Import failed: {ex.ToString()}&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private bool ValidateData(dynamic data)&lt;br /&gt;
    {&lt;br /&gt;
        // Implement validation logic based on data type&lt;br /&gt;
        if (data == null || (data is ICollection coll &amp;amp;&amp;amp; coll.Count == 0))&lt;br /&gt;
        {&lt;br /&gt;
            errors.Add(&amp;quot;No data found in import file&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Add custom validation rules here&lt;br /&gt;
        return errors.Count == 0;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private bool ProcessData(dynamic data, string importType)&lt;br /&gt;
    {&lt;br /&gt;
        using (var transaction = zinBL.BeginTransaction())&lt;br /&gt;
        {&lt;br /&gt;
            try&lt;br /&gt;
            {&lt;br /&gt;
                int recordsProcessed = 0;&lt;br /&gt;
                int recordsFailed = 0;&lt;br /&gt;
                &lt;br /&gt;
                foreach (var record in data)&lt;br /&gt;
                {&lt;br /&gt;
                    try&lt;br /&gt;
                    {&lt;br /&gt;
                        ProcessRecord(record, importType);&lt;br /&gt;
                        recordsProcessed++;&lt;br /&gt;
                    }&lt;br /&gt;
                    catch (Exception ex)&lt;br /&gt;
                    {&lt;br /&gt;
                        recordsFailed++;&lt;br /&gt;
                        LogError($&amp;quot;Failed to process record: {ex.Message}&amp;quot;);&lt;br /&gt;
                        &lt;br /&gt;
                        // Optionally continue or rollback on error&lt;br /&gt;
                        if (recordsFailed &amp;gt; 10) // Threshold for failures&lt;br /&gt;
                        {&lt;br /&gt;
                            transaction.Rollback();&lt;br /&gt;
                            return false;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                transaction.Commit();&lt;br /&gt;
                LogInfo($&amp;quot;Import completed: {recordsProcessed} records processed, {recordsFailed} failed&amp;quot;);&lt;br /&gt;
                return recordsFailed == 0;&lt;br /&gt;
            }&lt;br /&gt;
            catch (Exception ex)&lt;br /&gt;
            {&lt;br /&gt;
                transaction.Rollback();&lt;br /&gt;
                LogError($&amp;quot;Transaction failed: {ex.Message}&amp;quot;);&lt;br /&gt;
                return false;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void ProcessRecord(dynamic record, string importType)&lt;br /&gt;
    {&lt;br /&gt;
        // Implement record processing based on import type&lt;br /&gt;
        switch (importType)&lt;br /&gt;
        {&lt;br /&gt;
            case &amp;quot;Invoice&amp;quot;:&lt;br /&gt;
                ProcessInvoiceRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;Payment&amp;quot;:&lt;br /&gt;
                ProcessPaymentRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;Customer&amp;quot;:&lt;br /&gt;
                ProcessCustomerRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;Item&amp;quot;:&lt;br /&gt;
                ProcessItemRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            default:&lt;br /&gt;
                throw new NotSupportedException($&amp;quot;Import type {importType} is not supported&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void LogError(string message)&lt;br /&gt;
    {&lt;br /&gt;
        errors.Add(message);&lt;br /&gt;
        zinBL.LoggingFunctions?.LogError(message);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void LogInfo(string message)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL.LoggingFunctions?.LogInfo(message);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Import Mapping Configuration ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class ImportMapping&lt;br /&gt;
{&lt;br /&gt;
    public string SourceField { get; set; }&lt;br /&gt;
    public string TargetField { get; set; }&lt;br /&gt;
    public Type DataType { get; set; }&lt;br /&gt;
    public bool Required { get; set; }&lt;br /&gt;
    public object DefaultValue { get; set; }&lt;br /&gt;
    public Func&amp;lt;object, object&amp;gt; Transform { get; set; }&lt;br /&gt;
    public Func&amp;lt;object, bool&amp;gt; Validate { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ImportConfiguration&lt;br /&gt;
{&lt;br /&gt;
    public List&amp;lt;ImportMapping&amp;gt; Mappings { get; set; } = new List&amp;lt;ImportMapping&amp;gt;();&lt;br /&gt;
    public bool SkipEmptyRows { get; set; } = true;&lt;br /&gt;
    public bool TrimValues { get; set; } = true;&lt;br /&gt;
    public int HeaderRow { get; set; } = 0;&lt;br /&gt;
    public int DataStartRow { get; set; } = 1;&lt;br /&gt;
    public string DateFormat { get; set; } = &amp;quot;dd/MM/yyyy&amp;quot;;&lt;br /&gt;
    public bool StopOnError { get; set; } = false;&lt;br /&gt;
    public int MaxErrors { get; set; } = 100;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Common Import Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Batch Processing ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public async Task&amp;lt;ImportResult&amp;gt; BatchImportAsync&amp;lt;T&amp;gt;(&lt;br /&gt;
    IEnumerable&amp;lt;T&amp;gt; data, &lt;br /&gt;
    int batchSize = 100,&lt;br /&gt;
    Func&amp;lt;List&amp;lt;T&amp;gt;, Task&amp;lt;bool&amp;gt;&amp;gt; processBatch = null)&lt;br /&gt;
{&lt;br /&gt;
    var result = new ImportResult();&lt;br /&gt;
    var batch = new List&amp;lt;T&amp;gt;(batchSize);&lt;br /&gt;
    &lt;br /&gt;
    foreach (var item in data)&lt;br /&gt;
    {&lt;br /&gt;
        batch.Add(item);&lt;br /&gt;
        &lt;br /&gt;
        if (batch.Count &amp;gt;= batchSize)&lt;br /&gt;
        {&lt;br /&gt;
            var success = await processBatch(batch);&lt;br /&gt;
            if (success)&lt;br /&gt;
                result.SuccessCount += batch.Count;&lt;br /&gt;
            else&lt;br /&gt;
                result.FailureCount += batch.Count;&lt;br /&gt;
            &lt;br /&gt;
            batch.Clear();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Process remaining items&lt;br /&gt;
    if (batch.Count &amp;gt; 0)&lt;br /&gt;
    {&lt;br /&gt;
        var success = await processBatch(batch);&lt;br /&gt;
        if (success)&lt;br /&gt;
            result.SuccessCount += batch.Count;&lt;br /&gt;
        else&lt;br /&gt;
            result.FailureCount += batch.Count;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return result;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Duplicate Detection ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class DuplicateDetector&amp;lt;T&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    private readonly Func&amp;lt;T, string&amp;gt; _keySelector;&lt;br /&gt;
    private readonly HashSet&amp;lt;string&amp;gt; _existingKeys;&lt;br /&gt;
    &lt;br /&gt;
    public DuplicateDetector(Func&amp;lt;T, string&amp;gt; keySelector)&lt;br /&gt;
    {&lt;br /&gt;
        _keySelector = keySelector;&lt;br /&gt;
        _existingKeys = new HashSet&amp;lt;string&amp;gt;();&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public bool IsDuplicate(T item)&lt;br /&gt;
    {&lt;br /&gt;
        var key = _keySelector(item);&lt;br /&gt;
        return !_existingKeys.Add(key);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public List&amp;lt;T&amp;gt; RemoveDuplicates(IEnumerable&amp;lt;T&amp;gt; items)&lt;br /&gt;
    {&lt;br /&gt;
        var unique = new List&amp;lt;T&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        foreach (var item in items)&lt;br /&gt;
        {&lt;br /&gt;
            if (!IsDuplicate(item))&lt;br /&gt;
                unique.Add(item);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return unique;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Data Transformation Pipeline ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class TransformPipeline&amp;lt;TSource, TTarget&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    private readonly List&amp;lt;Func&amp;lt;TSource, TSource&amp;gt;&amp;gt; _transformations = new();&lt;br /&gt;
    private Func&amp;lt;TSource, TTarget&amp;gt; _finalMapping;&lt;br /&gt;
    &lt;br /&gt;
    public TransformPipeline&amp;lt;TSource, TTarget&amp;gt; AddTransformation(Func&amp;lt;TSource, TSource&amp;gt; transform)&lt;br /&gt;
    {&lt;br /&gt;
        _transformations.Add(transform);&lt;br /&gt;
        return this;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public TransformPipeline&amp;lt;TSource, TTarget&amp;gt; SetMapping(Func&amp;lt;TSource, TTarget&amp;gt; mapping)&lt;br /&gt;
    {&lt;br /&gt;
        _finalMapping = mapping;&lt;br /&gt;
        return this;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public TTarget Process(TSource source)&lt;br /&gt;
    {&lt;br /&gt;
        var current = source;&lt;br /&gt;
        &lt;br /&gt;
        foreach (var transform in _transformations)&lt;br /&gt;
        {&lt;br /&gt;
            current = transform(current);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return _finalMapping(current);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public List&amp;lt;TTarget&amp;gt; ProcessBatch(IEnumerable&amp;lt;TSource&amp;gt; sources)&lt;br /&gt;
    {&lt;br /&gt;
        return sources.Select(Process).ToList();&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Error Handling and Logging ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class ImportLogger&lt;br /&gt;
{&lt;br /&gt;
    private readonly BusinessLayerMain _bl;&lt;br /&gt;
    private readonly List&amp;lt;ImportError&amp;gt; _errors = new();&lt;br /&gt;
    private readonly StringBuilder _log = new();&lt;br /&gt;
    &lt;br /&gt;
    public ImportLogger(BusinessLayerMain bl)&lt;br /&gt;
    {&lt;br /&gt;
        _bl = bl;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public void LogError(int rowNumber, string field, string message, object value = null)&lt;br /&gt;
    {&lt;br /&gt;
        var error = new ImportError&lt;br /&gt;
        {&lt;br /&gt;
            RowNumber = rowNumber,&lt;br /&gt;
            Field = field,&lt;br /&gt;
            Message = message,&lt;br /&gt;
            Value = value?.ToString(),&lt;br /&gt;
            Timestamp = DateTime.Now&lt;br /&gt;
        };&lt;br /&gt;
        &lt;br /&gt;
        _errors.Add(error);&lt;br /&gt;
        &lt;br /&gt;
        var logMessage = $&amp;quot;Row {rowNumber}, Field &#039;{field}&#039;: {message}&amp;quot;;&lt;br /&gt;
        if (value != null)&lt;br /&gt;
            logMessage += $&amp;quot; (Value: {value})&amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        _log.AppendLine(logMessage);&lt;br /&gt;
        _bl.LoggingFunctions?.LogError(logMessage);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public void ExportErrorReport(string filePath)&lt;br /&gt;
    {&lt;br /&gt;
        using var writer = new StreamWriter(filePath);&lt;br /&gt;
        writer.WriteLine(&amp;quot;Row,Field,Error,Value,Time&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        foreach (var error in _errors)&lt;br /&gt;
        {&lt;br /&gt;
            writer.WriteLine($&amp;quot;{error.RowNumber},{error.Field},\&amp;quot;{error.Message}\&amp;quot;,\&amp;quot;{error.Value}\&amp;quot;,{error.Timestamp:yyyy-MM-dd HH:mm:ss}&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ImportError&lt;br /&gt;
{&lt;br /&gt;
    public int RowNumber { get; set; }&lt;br /&gt;
    public string Field { get; set; }&lt;br /&gt;
    public string Message { get; set; }&lt;br /&gt;
    public string Value { get; set; }&lt;br /&gt;
    public DateTime Timestamp { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== File Upload and Validation ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class FileImportValidator&lt;br /&gt;
{&lt;br /&gt;
    private readonly long _maxFileSize = 50 * 1024 * 1024; // 50MB&lt;br /&gt;
    private readonly string[] _allowedExtensions = { &amp;quot;.csv&amp;quot;, &amp;quot;.xlsx&amp;quot;, &amp;quot;.xls&amp;quot;, &amp;quot;.xml&amp;quot;, &amp;quot;.json&amp;quot;, &amp;quot;.txt&amp;quot; };&lt;br /&gt;
    &lt;br /&gt;
    public ValidationResult ValidateFile(string filePath)&lt;br /&gt;
    {&lt;br /&gt;
        var result = new ValidationResult();&lt;br /&gt;
        &lt;br /&gt;
        // Check file exists&lt;br /&gt;
        if (!File.Exists(filePath))&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError(&amp;quot;File does not exist&amp;quot;);&lt;br /&gt;
            return result;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Check file extension&lt;br /&gt;
        var extension = Path.GetExtension(filePath).ToLower();&lt;br /&gt;
        if (!_allowedExtensions.Contains(extension))&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;File type {extension} is not allowed&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Check file size&lt;br /&gt;
        var fileInfo = new FileInfo(filePath);&lt;br /&gt;
        if (fileInfo.Length &amp;gt; _maxFileSize)&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;File size exceeds maximum allowed size of {_maxFileSize / (1024 * 1024)}MB&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Check file is readable&lt;br /&gt;
        try&lt;br /&gt;
        {&lt;br /&gt;
            using var stream = File.OpenRead(filePath);&lt;br /&gt;
            // Attempt to read first byte to ensure file is accessible&lt;br /&gt;
            stream.ReadByte();&lt;br /&gt;
        }&lt;br /&gt;
        catch (Exception ex)&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;Cannot read file: {ex.Message}&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Validate file content structure&lt;br /&gt;
        if (result.IsValid)&lt;br /&gt;
        {&lt;br /&gt;
            result = ValidateFileContent(filePath, extension);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private ValidationResult ValidateFileContent(string filePath, string extension)&lt;br /&gt;
    {&lt;br /&gt;
        var result = new ValidationResult();&lt;br /&gt;
        &lt;br /&gt;
        switch (extension)&lt;br /&gt;
        {&lt;br /&gt;
            case &amp;quot;.csv&amp;quot;:&lt;br /&gt;
                ValidateCsvStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;.xlsx&amp;quot;:&lt;br /&gt;
            case &amp;quot;.xls&amp;quot;:&lt;br /&gt;
                ValidateExcelStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;.xml&amp;quot;:&lt;br /&gt;
                ValidateXmlStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;.json&amp;quot;:&lt;br /&gt;
                ValidateJsonStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void ValidateCsvStructure(string filePath, ValidationResult result)&lt;br /&gt;
    {&lt;br /&gt;
        try&lt;br /&gt;
        {&lt;br /&gt;
            var lines = File.ReadAllLines(filePath).Take(10).ToList();&lt;br /&gt;
            &lt;br /&gt;
            if (lines.Count == 0)&lt;br /&gt;
            {&lt;br /&gt;
                result.AddError(&amp;quot;CSV file is empty&amp;quot;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Check for consistent column count&lt;br /&gt;
            var firstLineColumns = lines[0].Split(&#039;,&#039;).Length;&lt;br /&gt;
            for (int i = 1; i &amp;lt; lines.Count; i++)&lt;br /&gt;
            {&lt;br /&gt;
                var columnCount = lines[i].Split(&#039;,&#039;).Length;&lt;br /&gt;
                if (columnCount != firstLineColumns)&lt;br /&gt;
                {&lt;br /&gt;
                    result.AddWarning($&amp;quot;Inconsistent column count at line {i + 1}&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        catch (Exception ex)&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;Failed to validate CSV structure: {ex.Message}&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ValidationResult&lt;br /&gt;
{&lt;br /&gt;
    public List&amp;lt;string&amp;gt; Errors { get; } = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    public List&amp;lt;string&amp;gt; Warnings { get; } = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    public bool IsValid =&amp;gt; Errors.Count == 0;&lt;br /&gt;
    &lt;br /&gt;
    public void AddError(string message) =&amp;gt; Errors.Add(message);&lt;br /&gt;
    public void AddWarning(string message) =&amp;gt; Warnings.Add(message);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Performance Optimization ==&lt;br /&gt;
&lt;br /&gt;
=== Memory-Efficient Large File Processing ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class LargeFileProcessor&lt;br /&gt;
{&lt;br /&gt;
    public async Task ProcessLargeFileAsync(string filePath, Func&amp;lt;string, Task&amp;lt;bool&amp;gt;&amp;gt; processLine)&lt;br /&gt;
    {&lt;br /&gt;
        const int bufferSize = 65536; // 64KB buffer&lt;br /&gt;
        &lt;br /&gt;
        using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync: true);&lt;br /&gt;
        using var reader = new StreamReader(fileStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize);&lt;br /&gt;
        &lt;br /&gt;
        string line;&lt;br /&gt;
        int lineNumber = 0;&lt;br /&gt;
        &lt;br /&gt;
        while ((line = await reader.ReadLineAsync()) != null)&lt;br /&gt;
        {&lt;br /&gt;
            lineNumber++;&lt;br /&gt;
            &lt;br /&gt;
            if (!await processLine(line))&lt;br /&gt;
            {&lt;br /&gt;
                throw new ImportException($&amp;quot;Failed to process line {lineNumber}&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Parallel Processing ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
public class ParallelImporter&amp;lt;T&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    public async Task&amp;lt;ImportResult&amp;gt; ImportParallelAsync(&lt;br /&gt;
        IEnumerable&amp;lt;T&amp;gt; data,&lt;br /&gt;
        Func&amp;lt;T, Task&amp;lt;bool&amp;gt;&amp;gt; processItem,&lt;br /&gt;
        int maxDegreeOfParallelism = 4)&lt;br /&gt;
    {&lt;br /&gt;
        var result = new ImportResult();&lt;br /&gt;
        var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);&lt;br /&gt;
        var tasks = new List&amp;lt;Task&amp;lt;bool&amp;gt;&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        foreach (var item in data)&lt;br /&gt;
        {&lt;br /&gt;
            await semaphore.WaitAsync();&lt;br /&gt;
            &lt;br /&gt;
            tasks.Add(Task.Run(async () =&amp;gt;&lt;br /&gt;
            {&lt;br /&gt;
                try&lt;br /&gt;
                {&lt;br /&gt;
                    return await processItem(item);&lt;br /&gt;
                }&lt;br /&gt;
                finally&lt;br /&gt;
                {&lt;br /&gt;
                    semaphore.Release();&lt;br /&gt;
                }&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var results = await Task.WhenAll(tasks);&lt;br /&gt;
        &lt;br /&gt;
        result.SuccessCount = results.Count(r =&amp;gt; r);&lt;br /&gt;
        result.FailureCount = results.Count(r =&amp;gt; !r);&lt;br /&gt;
        &lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ImportResult&lt;br /&gt;
{&lt;br /&gt;
    public int SuccessCount { get; set; }&lt;br /&gt;
    public int FailureCount { get; set; }&lt;br /&gt;
    public int TotalCount =&amp;gt; SuccessCount + FailureCount;&lt;br /&gt;
    public double SuccessRate =&amp;gt; TotalCount &amp;gt; 0 ? (double)SuccessCount / TotalCount * 100 : 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Always validate before import&#039;&#039;&#039;&lt;br /&gt;
#* Check file format and structure&lt;br /&gt;
#* Validate required fields&lt;br /&gt;
#* Check data types and ranges&lt;br /&gt;
#* Identify duplicates&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Use transactions&#039;&#039;&#039;&lt;br /&gt;
#* Wrap imports in database transactions&lt;br /&gt;
#* Implement rollback on failure&lt;br /&gt;
#* Consider savepoints for partial commits&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Implement proper error handling&#039;&#039;&#039;&lt;br /&gt;
#* Log all errors with context&lt;br /&gt;
#* Provide detailed error reports&lt;br /&gt;
#* Allow partial success where appropriate&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Optimize for performance&#039;&#039;&#039;&lt;br /&gt;
#* Use batch processing for large datasets&lt;br /&gt;
#* Implement async/await patterns&lt;br /&gt;
#* Consider parallel processing where safe&lt;br /&gt;
#* Use streaming for large files&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Maintain data integrity&#039;&#039;&#039;&lt;br /&gt;
#* Check foreign key relationships&lt;br /&gt;
#* Validate business rules&lt;br /&gt;
#* Ensure referential integrity&lt;br /&gt;
#* Handle nullable fields properly&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Provide user feedback&#039;&#039;&#039;&lt;br /&gt;
#* Show progress indicators&lt;br /&gt;
#* Report success/failure counts&lt;br /&gt;
#* Export error logs&lt;br /&gt;
#* Allow preview before commit&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Security considerations&#039;&#039;&#039;&lt;br /&gt;
#* Validate file types and sizes&lt;br /&gt;
#* Scan for malicious content&lt;br /&gt;
#* Sanitize input data&lt;br /&gt;
#* Implement access controls&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Make imports resumable&#039;&#039;&#039;&lt;br /&gt;
#* Track processed records&lt;br /&gt;
#* Allow restart from failure point&lt;br /&gt;
#* Implement idempotent operations&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* [[Data Validation]]&lt;br /&gt;
* [[Transaction Management]]&lt;br /&gt;
* [[Error Handling]]&lt;br /&gt;
* [[Performance Optimization]]&lt;br /&gt;
* [[Database Operations]]&lt;br /&gt;
* [[File Management]]&lt;br /&gt;
* [[Logging System]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Import/Export]]&lt;br /&gt;
[[Category:Data Processing]]&lt;br /&gt;
[[Category:File Operations]]&lt;br /&gt;
[[Category:Technical Documentation]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Import_File_Scripting&amp;diff=124</id>
		<title>Import File Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Import_File_Scripting&amp;diff=124"/>
		<updated>2025-11-10T03:04:29Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= File Import System Documentation =&lt;br /&gt;
&lt;br /&gt;
This page documents the file import infrastructure in ZinformAccounts, providing templates and patterns for implementing various file format imports including CSV, XML, Excel, JSON, and other data sources.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The import system provides a flexible framework for processing external data files into the ZinformAccounts database. All imports follow consistent patterns for error handling, validation, and transaction management.&lt;br /&gt;
&lt;br /&gt;
== Supported File Formats ==&lt;br /&gt;
&lt;br /&gt;
=== Excel Files (.xlsx, .xls) ===&lt;br /&gt;
&lt;br /&gt;
Using GemBox.Spreadsheet for Excel file processing:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using GemBox.Spreadsheet;&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportExcel&amp;lt;T&amp;gt;(string filePath) where T : new()&lt;br /&gt;
{&lt;br /&gt;
    var workbook = ExcelFile.Load(filePath);&lt;br /&gt;
    var worksheet = workbook.Worksheets[0];&lt;br /&gt;
    var results = new List&amp;lt;T&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    // Get header row to map columns&lt;br /&gt;
    int headerRowIndex = 0;&lt;br /&gt;
    var headers = new Dictionary&amp;lt;int, string&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    for (int col = 0; col &amp;lt; worksheet.CalculateMaxUsedColumns(); col++)&lt;br /&gt;
    {&lt;br /&gt;
        var headerValue = worksheet.Cells[headerRowIndex, col].Value?.ToString();&lt;br /&gt;
        if (!string.IsNullOrEmpty(headerValue))&lt;br /&gt;
            headers[col] = headerValue;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Process data rows&lt;br /&gt;
    for (int row = headerRowIndex + 1; row &amp;lt; worksheet.Rows.Count; row++)&lt;br /&gt;
    {&lt;br /&gt;
        var item = MapRowToObject&amp;lt;T&amp;gt;(worksheet.Rows[row], headers);&lt;br /&gt;
        if (item != null)&lt;br /&gt;
            results.Add(item);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== CSV Files ===&lt;br /&gt;
&lt;br /&gt;
Using CsvHelper for CSV processing with flexible date handling:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using CsvHelper;&lt;br /&gt;
using CsvHelper.Configuration;&lt;br /&gt;
using System.Globalization;&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportCsv&amp;lt;T&amp;gt;(string filePath, bool hasHeader = true)&lt;br /&gt;
{&lt;br /&gt;
    var config = new CsvConfiguration(CultureInfo.InvariantCulture)&lt;br /&gt;
    {&lt;br /&gt;
        HasHeaderRecord = hasHeader,&lt;br /&gt;
        MissingFieldFound = null,&lt;br /&gt;
        BadDataFound = null,&lt;br /&gt;
        TrimOptions = TrimOptions.Trim&lt;br /&gt;
    };&lt;br /&gt;
    &lt;br /&gt;
    using var reader = new StreamReader(filePath);&lt;br /&gt;
    using var csv = new CsvReader(reader, config);&lt;br /&gt;
    &lt;br /&gt;
    // Register custom type converters for dates&lt;br /&gt;
    csv.Context.TypeConverterCache.AddConverter&amp;lt;DateTime&amp;gt;(new FlexibleDateConverter());&lt;br /&gt;
    &lt;br /&gt;
    return csv.GetRecords&amp;lt;T&amp;gt;().ToList();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class FlexibleDateConverter : ITypeConverter&lt;br /&gt;
{&lt;br /&gt;
    private readonly string[] _dateFormats = new[]&lt;br /&gt;
    {&lt;br /&gt;
        &amp;quot;dd/MM/yyyy&amp;quot;,&lt;br /&gt;
        &amp;quot;d/M/yyyy&amp;quot;,&lt;br /&gt;
        &amp;quot;yyyy-MM-dd&amp;quot;,&lt;br /&gt;
        &amp;quot;dd-MM-yyyy&amp;quot;,&lt;br /&gt;
        &amp;quot;dd.MM.yyyy&amp;quot;&lt;br /&gt;
    };&lt;br /&gt;
    &lt;br /&gt;
    public object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)&lt;br /&gt;
    {&lt;br /&gt;
        if (DateTime.TryParseExact(text, _dateFormats, &lt;br /&gt;
            CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))&lt;br /&gt;
            return date;&lt;br /&gt;
        &lt;br /&gt;
        throw new InvalidOperationException($&amp;quot;Cannot parse date: {text}&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== XML Files ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using System.Xml.Linq;&lt;br /&gt;
using System.Xml.Serialization;&lt;br /&gt;
&lt;br /&gt;
// Method 1: Using XDocument for flexible parsing&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportXmlFlexible&amp;lt;T&amp;gt;(string filePath, Func&amp;lt;XElement, T&amp;gt; mapper)&lt;br /&gt;
{&lt;br /&gt;
    var doc = XDocument.Load(filePath);&lt;br /&gt;
    var results = new List&amp;lt;T&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    foreach (var element in doc.Descendants(&amp;quot;Record&amp;quot;))&lt;br /&gt;
    {&lt;br /&gt;
        var item = mapper(element);&lt;br /&gt;
        if (item != null)&lt;br /&gt;
            results.Add(item);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Method 2: Using XML Serialization for strongly-typed objects&lt;br /&gt;
public T ImportXmlSerialized&amp;lt;T&amp;gt;(string filePath) where T : class&lt;br /&gt;
{&lt;br /&gt;
    var serializer = new XmlSerializer(typeof(T));&lt;br /&gt;
    using var stream = File.OpenRead(filePath);&lt;br /&gt;
    return serializer.Deserialize(stream) as T;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== JSON Files ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using Newtonsoft.Json;&lt;br /&gt;
&lt;br /&gt;
public T ImportJson&amp;lt;T&amp;gt;(string filePath)&lt;br /&gt;
{&lt;br /&gt;
    var json = File.ReadAllText(filePath);&lt;br /&gt;
    return JsonConvert.DeserializeObject&amp;lt;T&amp;gt;(json);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;T&amp;gt; ImportJsonArray&amp;lt;T&amp;gt;(string filePath)&lt;br /&gt;
{&lt;br /&gt;
    var json = File.ReadAllText(filePath);&lt;br /&gt;
    return JsonConvert.DeserializeObject&amp;lt;List&amp;lt;T&amp;gt;&amp;gt;(json) ?? new List&amp;lt;T&amp;gt;();&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Tab-Delimited and Fixed-Width Files ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Tab-delimited files&lt;br /&gt;
public List&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt; ImportTabDelimited(string filePath)&lt;br /&gt;
{&lt;br /&gt;
    var lines = File.ReadAllLines(filePath);&lt;br /&gt;
    if (lines.Length == 0) return new List&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    var headers = lines[0].Split(&#039;\t&#039;);&lt;br /&gt;
    var results = new List&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    for (int i = 1; i &amp;lt; lines.Length; i++)&lt;br /&gt;
    {&lt;br /&gt;
        var values = lines[i].Split(&#039;\t&#039;);&lt;br /&gt;
        var record = new Dictionary&amp;lt;string, string&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        for (int j = 0; j &amp;lt; Math.Min(headers.Length, values.Length); j++)&lt;br /&gt;
        {&lt;br /&gt;
            record[headers[j]] = values[j];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        results.Add(record);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Fixed-width files&lt;br /&gt;
public class FixedWidthField&lt;br /&gt;
{&lt;br /&gt;
    public string Name { get; set; }&lt;br /&gt;
    public int StartPosition { get; set; }&lt;br /&gt;
    public int Length { get; set; }&lt;br /&gt;
    public Type DataType { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public List&amp;lt;Dictionary&amp;lt;string, object&amp;gt;&amp;gt; ImportFixedWidth(string filePath, List&amp;lt;FixedWidthField&amp;gt; fields)&lt;br /&gt;
{&lt;br /&gt;
    var lines = File.ReadAllLines(filePath);&lt;br /&gt;
    var results = new List&amp;lt;Dictionary&amp;lt;string, object&amp;gt;&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    foreach (var line in lines)&lt;br /&gt;
    {&lt;br /&gt;
        var record = new Dictionary&amp;lt;string, object&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        foreach (var field in fields)&lt;br /&gt;
        {&lt;br /&gt;
            if (line.Length &amp;gt;= field.StartPosition + field.Length)&lt;br /&gt;
            {&lt;br /&gt;
                var value = line.Substring(field.StartPosition, field.Length).Trim();&lt;br /&gt;
                record[field.Name] = ConvertToType(value, field.DataType);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        results.Add(record);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return results;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Generic Import Script Template ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using System;&lt;br /&gt;
using System.IO;&lt;br /&gt;
using System.Data;&lt;br /&gt;
using System.Text;&lt;br /&gt;
using System.Windows;&lt;br /&gt;
using System.Linq;&lt;br /&gt;
using System.Collections.Generic;&lt;br /&gt;
using ZinBusinessLayer;&lt;br /&gt;
using ZinDATA;&lt;br /&gt;
using ZinBusinessLayer.Models;&lt;br /&gt;
&lt;br /&gt;
public class GenericImportScripter&lt;br /&gt;
{&lt;br /&gt;
    private BusinessLayerMain zinBL;&lt;br /&gt;
    private readonly List&amp;lt;string&amp;gt; errors = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    private readonly List&amp;lt;string&amp;gt; warnings = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    public bool Main(BusinessLayerMain bl, ZinParameters parameters, ZinParameters returnParameters)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        &lt;br /&gt;
        try&lt;br /&gt;
        {&lt;br /&gt;
            // Get parameters&lt;br /&gt;
            var filename = parameters.GetItemString(&amp;quot;FileName&amp;quot;);&lt;br /&gt;
            var importType = parameters.GetItemString(&amp;quot;ImportType&amp;quot;);&lt;br /&gt;
            var validateOnly = parameters.GetItemBool(&amp;quot;ValidateOnly&amp;quot;) ?? false;&lt;br /&gt;
            &lt;br /&gt;
            // Determine file type and import&lt;br /&gt;
            var extension = Path.GetExtension(filename).ToLower();&lt;br /&gt;
            dynamic data = null;&lt;br /&gt;
            &lt;br /&gt;
            switch (extension)&lt;br /&gt;
            {&lt;br /&gt;
                case &amp;quot;.xlsx&amp;quot;:&lt;br /&gt;
                case &amp;quot;.xls&amp;quot;:&lt;br /&gt;
                    data = ImportExcel(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.csv&amp;quot;:&lt;br /&gt;
                    data = ImportCsv(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.xml&amp;quot;:&lt;br /&gt;
                    data = ImportXml(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.json&amp;quot;:&lt;br /&gt;
                    data = ImportJson(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                case &amp;quot;.txt&amp;quot;:&lt;br /&gt;
                    data = DetermineAndImportText(filename);&lt;br /&gt;
                    break;&lt;br /&gt;
                default:&lt;br /&gt;
                    throw new NotSupportedException($&amp;quot;File type {extension} is not supported&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Validate data&lt;br /&gt;
            if (!ValidateData(data))&lt;br /&gt;
            {&lt;br /&gt;
                returnParameters.SetItem(&amp;quot;Errors&amp;quot;, string.Join(&amp;quot;\n&amp;quot;, errors));&lt;br /&gt;
                return false;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process data if not validation only&lt;br /&gt;
            if (!validateOnly)&lt;br /&gt;
            {&lt;br /&gt;
                return ProcessData(data, importType);&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            returnParameters.SetItem(&amp;quot;ValidationResult&amp;quot;, &amp;quot;Data validated successfully&amp;quot;);&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
        catch (Exception ex)&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItem(&amp;quot;Error&amp;quot;, $&amp;quot;Import failed: {ex.Message}&amp;quot;);&lt;br /&gt;
            LogError($&amp;quot;Import failed: {ex.ToString()}&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private bool ValidateData(dynamic data)&lt;br /&gt;
    {&lt;br /&gt;
        // Implement validation logic based on data type&lt;br /&gt;
        if (data == null || (data is ICollection coll &amp;amp;&amp;amp; coll.Count == 0))&lt;br /&gt;
        {&lt;br /&gt;
            errors.Add(&amp;quot;No data found in import file&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Add custom validation rules here&lt;br /&gt;
        return errors.Count == 0;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private bool ProcessData(dynamic data, string importType)&lt;br /&gt;
    {&lt;br /&gt;
        using (var transaction = zinBL.BeginTransaction())&lt;br /&gt;
        {&lt;br /&gt;
            try&lt;br /&gt;
            {&lt;br /&gt;
                int recordsProcessed = 0;&lt;br /&gt;
                int recordsFailed = 0;&lt;br /&gt;
                &lt;br /&gt;
                foreach (var record in data)&lt;br /&gt;
                {&lt;br /&gt;
                    try&lt;br /&gt;
                    {&lt;br /&gt;
                        ProcessRecord(record, importType);&lt;br /&gt;
                        recordsProcessed++;&lt;br /&gt;
                    }&lt;br /&gt;
                    catch (Exception ex)&lt;br /&gt;
                    {&lt;br /&gt;
                        recordsFailed++;&lt;br /&gt;
                        LogError($&amp;quot;Failed to process record: {ex.Message}&amp;quot;);&lt;br /&gt;
                        &lt;br /&gt;
                        // Optionally continue or rollback on error&lt;br /&gt;
                        if (recordsFailed &amp;gt; 10) // Threshold for failures&lt;br /&gt;
                        {&lt;br /&gt;
                            transaction.Rollback();&lt;br /&gt;
                            return false;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                transaction.Commit();&lt;br /&gt;
                LogInfo($&amp;quot;Import completed: {recordsProcessed} records processed, {recordsFailed} failed&amp;quot;);&lt;br /&gt;
                return recordsFailed == 0;&lt;br /&gt;
            }&lt;br /&gt;
            catch (Exception ex)&lt;br /&gt;
            {&lt;br /&gt;
                transaction.Rollback();&lt;br /&gt;
                LogError($&amp;quot;Transaction failed: {ex.Message}&amp;quot;);&lt;br /&gt;
                return false;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void ProcessRecord(dynamic record, string importType)&lt;br /&gt;
    {&lt;br /&gt;
        // Implement record processing based on import type&lt;br /&gt;
        switch (importType)&lt;br /&gt;
        {&lt;br /&gt;
            case &amp;quot;Invoice&amp;quot;:&lt;br /&gt;
                ProcessInvoiceRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;Payment&amp;quot;:&lt;br /&gt;
                ProcessPaymentRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;Customer&amp;quot;:&lt;br /&gt;
                ProcessCustomerRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;Item&amp;quot;:&lt;br /&gt;
                ProcessItemRecord(record);&lt;br /&gt;
                break;&lt;br /&gt;
            default:&lt;br /&gt;
                throw new NotSupportedException($&amp;quot;Import type {importType} is not supported&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void LogError(string message)&lt;br /&gt;
    {&lt;br /&gt;
        errors.Add(message);&lt;br /&gt;
        zinBL.LoggingFunctions?.LogError(message);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void LogInfo(string message)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL.LoggingFunctions?.LogInfo(message);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Import Mapping Configuration ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class ImportMapping&lt;br /&gt;
{&lt;br /&gt;
    public string SourceField { get; set; }&lt;br /&gt;
    public string TargetField { get; set; }&lt;br /&gt;
    public Type DataType { get; set; }&lt;br /&gt;
    public bool Required { get; set; }&lt;br /&gt;
    public object DefaultValue { get; set; }&lt;br /&gt;
    public Func&amp;lt;object, object&amp;gt; Transform { get; set; }&lt;br /&gt;
    public Func&amp;lt;object, bool&amp;gt; Validate { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ImportConfiguration&lt;br /&gt;
{&lt;br /&gt;
    public List&amp;lt;ImportMapping&amp;gt; Mappings { get; set; } = new List&amp;lt;ImportMapping&amp;gt;();&lt;br /&gt;
    public bool SkipEmptyRows { get; set; } = true;&lt;br /&gt;
    public bool TrimValues { get; set; } = true;&lt;br /&gt;
    public int HeaderRow { get; set; } = 0;&lt;br /&gt;
    public int DataStartRow { get; set; } = 1;&lt;br /&gt;
    public string DateFormat { get; set; } = &amp;quot;dd/MM/yyyy&amp;quot;;&lt;br /&gt;
    public bool StopOnError { get; set; } = false;&lt;br /&gt;
    public int MaxErrors { get; set; } = 100;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Common Import Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Batch Processing ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public async Task&amp;lt;ImportResult&amp;gt; BatchImportAsync&amp;lt;T&amp;gt;(&lt;br /&gt;
    IEnumerable&amp;lt;T&amp;gt; data, &lt;br /&gt;
    int batchSize = 100,&lt;br /&gt;
    Func&amp;lt;List&amp;lt;T&amp;gt;, Task&amp;lt;bool&amp;gt;&amp;gt; processBatch = null)&lt;br /&gt;
{&lt;br /&gt;
    var result = new ImportResult();&lt;br /&gt;
    var batch = new List&amp;lt;T&amp;gt;(batchSize);&lt;br /&gt;
    &lt;br /&gt;
    foreach (var item in data)&lt;br /&gt;
    {&lt;br /&gt;
        batch.Add(item);&lt;br /&gt;
        &lt;br /&gt;
        if (batch.Count &amp;gt;= batchSize)&lt;br /&gt;
        {&lt;br /&gt;
            var success = await processBatch(batch);&lt;br /&gt;
            if (success)&lt;br /&gt;
                result.SuccessCount += batch.Count;&lt;br /&gt;
            else&lt;br /&gt;
                result.FailureCount += batch.Count;&lt;br /&gt;
            &lt;br /&gt;
            batch.Clear();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Process remaining items&lt;br /&gt;
    if (batch.Count &amp;gt; 0)&lt;br /&gt;
    {&lt;br /&gt;
        var success = await processBatch(batch);&lt;br /&gt;
        if (success)&lt;br /&gt;
            result.SuccessCount += batch.Count;&lt;br /&gt;
        else&lt;br /&gt;
            result.FailureCount += batch.Count;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    return result;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Duplicate Detection ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class DuplicateDetector&amp;lt;T&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    private readonly Func&amp;lt;T, string&amp;gt; _keySelector;&lt;br /&gt;
    private readonly HashSet&amp;lt;string&amp;gt; _existingKeys;&lt;br /&gt;
    &lt;br /&gt;
    public DuplicateDetector(Func&amp;lt;T, string&amp;gt; keySelector)&lt;br /&gt;
    {&lt;br /&gt;
        _keySelector = keySelector;&lt;br /&gt;
        _existingKeys = new HashSet&amp;lt;string&amp;gt;();&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public bool IsDuplicate(T item)&lt;br /&gt;
    {&lt;br /&gt;
        var key = _keySelector(item);&lt;br /&gt;
        return !_existingKeys.Add(key);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public List&amp;lt;T&amp;gt; RemoveDuplicates(IEnumerable&amp;lt;T&amp;gt; items)&lt;br /&gt;
    {&lt;br /&gt;
        var unique = new List&amp;lt;T&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        foreach (var item in items)&lt;br /&gt;
        {&lt;br /&gt;
            if (!IsDuplicate(item))&lt;br /&gt;
                unique.Add(item);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return unique;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Data Transformation Pipeline ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class TransformPipeline&amp;lt;TSource, TTarget&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    private readonly List&amp;lt;Func&amp;lt;TSource, TSource&amp;gt;&amp;gt; _transformations = new();&lt;br /&gt;
    private Func&amp;lt;TSource, TTarget&amp;gt; _finalMapping;&lt;br /&gt;
    &lt;br /&gt;
    public TransformPipeline&amp;lt;TSource, TTarget&amp;gt; AddTransformation(Func&amp;lt;TSource, TSource&amp;gt; transform)&lt;br /&gt;
    {&lt;br /&gt;
        _transformations.Add(transform);&lt;br /&gt;
        return this;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public TransformPipeline&amp;lt;TSource, TTarget&amp;gt; SetMapping(Func&amp;lt;TSource, TTarget&amp;gt; mapping)&lt;br /&gt;
    {&lt;br /&gt;
        _finalMapping = mapping;&lt;br /&gt;
        return this;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public TTarget Process(TSource source)&lt;br /&gt;
    {&lt;br /&gt;
        var current = source;&lt;br /&gt;
        &lt;br /&gt;
        foreach (var transform in _transformations)&lt;br /&gt;
        {&lt;br /&gt;
            current = transform(current);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return _finalMapping(current);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public List&amp;lt;TTarget&amp;gt; ProcessBatch(IEnumerable&amp;lt;TSource&amp;gt; sources)&lt;br /&gt;
    {&lt;br /&gt;
        return sources.Select(Process).ToList();&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Error Handling and Logging ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class ImportLogger&lt;br /&gt;
{&lt;br /&gt;
    private readonly BusinessLayerMain _bl;&lt;br /&gt;
    private readonly List&amp;lt;ImportError&amp;gt; _errors = new();&lt;br /&gt;
    private readonly StringBuilder _log = new();&lt;br /&gt;
    &lt;br /&gt;
    public ImportLogger(BusinessLayerMain bl)&lt;br /&gt;
    {&lt;br /&gt;
        _bl = bl;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public void LogError(int rowNumber, string field, string message, object value = null)&lt;br /&gt;
    {&lt;br /&gt;
        var error = new ImportError&lt;br /&gt;
        {&lt;br /&gt;
            RowNumber = rowNumber,&lt;br /&gt;
            Field = field,&lt;br /&gt;
            Message = message,&lt;br /&gt;
            Value = value?.ToString(),&lt;br /&gt;
            Timestamp = DateTime.Now&lt;br /&gt;
        };&lt;br /&gt;
        &lt;br /&gt;
        _errors.Add(error);&lt;br /&gt;
        &lt;br /&gt;
        var logMessage = $&amp;quot;Row {rowNumber}, Field &#039;{field}&#039;: {message}&amp;quot;;&lt;br /&gt;
        if (value != null)&lt;br /&gt;
            logMessage += $&amp;quot; (Value: {value})&amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        _log.AppendLine(logMessage);&lt;br /&gt;
        _bl.LoggingFunctions?.LogError(logMessage);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public void ExportErrorReport(string filePath)&lt;br /&gt;
    {&lt;br /&gt;
        using var writer = new StreamWriter(filePath);&lt;br /&gt;
        writer.WriteLine(&amp;quot;Row,Field,Error,Value,Time&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        foreach (var error in _errors)&lt;br /&gt;
        {&lt;br /&gt;
            writer.WriteLine($&amp;quot;{error.RowNumber},{error.Field},\&amp;quot;{error.Message}\&amp;quot;,\&amp;quot;{error.Value}\&amp;quot;,{error.Timestamp:yyyy-MM-dd HH:mm:ss}&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ImportError&lt;br /&gt;
{&lt;br /&gt;
    public int RowNumber { get; set; }&lt;br /&gt;
    public string Field { get; set; }&lt;br /&gt;
    public string Message { get; set; }&lt;br /&gt;
    public string Value { get; set; }&lt;br /&gt;
    public DateTime Timestamp { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== File Upload and Validation ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class FileImportValidator&lt;br /&gt;
{&lt;br /&gt;
    private readonly long _maxFileSize = 50 * 1024 * 1024; // 50MB&lt;br /&gt;
    private readonly string[] _allowedExtensions = { &amp;quot;.csv&amp;quot;, &amp;quot;.xlsx&amp;quot;, &amp;quot;.xls&amp;quot;, &amp;quot;.xml&amp;quot;, &amp;quot;.json&amp;quot;, &amp;quot;.txt&amp;quot; };&lt;br /&gt;
    &lt;br /&gt;
    public ValidationResult ValidateFile(string filePath)&lt;br /&gt;
    {&lt;br /&gt;
        var result = new ValidationResult();&lt;br /&gt;
        &lt;br /&gt;
        // Check file exists&lt;br /&gt;
        if (!File.Exists(filePath))&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError(&amp;quot;File does not exist&amp;quot;);&lt;br /&gt;
            return result;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Check file extension&lt;br /&gt;
        var extension = Path.GetExtension(filePath).ToLower();&lt;br /&gt;
        if (!_allowedExtensions.Contains(extension))&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;File type {extension} is not allowed&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Check file size&lt;br /&gt;
        var fileInfo = new FileInfo(filePath);&lt;br /&gt;
        if (fileInfo.Length &amp;gt; _maxFileSize)&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;File size exceeds maximum allowed size of {_maxFileSize / (1024 * 1024)}MB&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Check file is readable&lt;br /&gt;
        try&lt;br /&gt;
        {&lt;br /&gt;
            using var stream = File.OpenRead(filePath);&lt;br /&gt;
            // Attempt to read first byte to ensure file is accessible&lt;br /&gt;
            stream.ReadByte();&lt;br /&gt;
        }&lt;br /&gt;
        catch (Exception ex)&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;Cannot read file: {ex.Message}&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Validate file content structure&lt;br /&gt;
        if (result.IsValid)&lt;br /&gt;
        {&lt;br /&gt;
            result = ValidateFileContent(filePath, extension);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private ValidationResult ValidateFileContent(string filePath, string extension)&lt;br /&gt;
    {&lt;br /&gt;
        var result = new ValidationResult();&lt;br /&gt;
        &lt;br /&gt;
        switch (extension)&lt;br /&gt;
        {&lt;br /&gt;
            case &amp;quot;.csv&amp;quot;:&lt;br /&gt;
                ValidateCsvStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;.xlsx&amp;quot;:&lt;br /&gt;
            case &amp;quot;.xls&amp;quot;:&lt;br /&gt;
                ValidateExcelStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;.xml&amp;quot;:&lt;br /&gt;
                ValidateXmlStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
            case &amp;quot;.json&amp;quot;:&lt;br /&gt;
                ValidateJsonStructure(filePath, result);&lt;br /&gt;
                break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private void ValidateCsvStructure(string filePath, ValidationResult result)&lt;br /&gt;
    {&lt;br /&gt;
        try&lt;br /&gt;
        {&lt;br /&gt;
            var lines = File.ReadAllLines(filePath).Take(10).ToList();&lt;br /&gt;
            &lt;br /&gt;
            if (lines.Count == 0)&lt;br /&gt;
            {&lt;br /&gt;
                result.AddError(&amp;quot;CSV file is empty&amp;quot;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Check for consistent column count&lt;br /&gt;
            var firstLineColumns = lines[0].Split(&#039;,&#039;).Length;&lt;br /&gt;
            for (int i = 1; i &amp;lt; lines.Count; i++)&lt;br /&gt;
            {&lt;br /&gt;
                var columnCount = lines[i].Split(&#039;,&#039;).Length;&lt;br /&gt;
                if (columnCount != firstLineColumns)&lt;br /&gt;
                {&lt;br /&gt;
                    result.AddWarning($&amp;quot;Inconsistent column count at line {i + 1}&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        catch (Exception ex)&lt;br /&gt;
        {&lt;br /&gt;
            result.AddError($&amp;quot;Failed to validate CSV structure: {ex.Message}&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ValidationResult&lt;br /&gt;
{&lt;br /&gt;
    public List&amp;lt;string&amp;gt; Errors { get; } = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    public List&amp;lt;string&amp;gt; Warnings { get; } = new List&amp;lt;string&amp;gt;();&lt;br /&gt;
    &lt;br /&gt;
    public bool IsValid =&amp;gt; Errors.Count == 0;&lt;br /&gt;
    &lt;br /&gt;
    public void AddError(string message) =&amp;gt; Errors.Add(message);&lt;br /&gt;
    public void AddWarning(string message) =&amp;gt; Warnings.Add(message);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Performance Optimization ==&lt;br /&gt;
&lt;br /&gt;
=== Memory-Efficient Large File Processing ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class LargeFileProcessor&lt;br /&gt;
{&lt;br /&gt;
    public async Task ProcessLargeFileAsync(string filePath, Func&amp;lt;string, Task&amp;lt;bool&amp;gt;&amp;gt; processLine)&lt;br /&gt;
    {&lt;br /&gt;
        const int bufferSize = 65536; // 64KB buffer&lt;br /&gt;
        &lt;br /&gt;
        using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync: true);&lt;br /&gt;
        using var reader = new StreamReader(fileStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize);&lt;br /&gt;
        &lt;br /&gt;
        string line;&lt;br /&gt;
        int lineNumber = 0;&lt;br /&gt;
        &lt;br /&gt;
        while ((line = await reader.ReadLineAsync()) != null)&lt;br /&gt;
        {&lt;br /&gt;
            lineNumber++;&lt;br /&gt;
            &lt;br /&gt;
            if (!await processLine(line))&lt;br /&gt;
            {&lt;br /&gt;
                throw new ImportException($&amp;quot;Failed to process line {lineNumber}&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Parallel Processing ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class ParallelImporter&amp;lt;T&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    public async Task&amp;lt;ImportResult&amp;gt; ImportParallelAsync(&lt;br /&gt;
        IEnumerable&amp;lt;T&amp;gt; data,&lt;br /&gt;
        Func&amp;lt;T, Task&amp;lt;bool&amp;gt;&amp;gt; processItem,&lt;br /&gt;
        int maxDegreeOfParallelism = 4)&lt;br /&gt;
    {&lt;br /&gt;
        var result = new ImportResult();&lt;br /&gt;
        var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);&lt;br /&gt;
        var tasks = new List&amp;lt;Task&amp;lt;bool&amp;gt;&amp;gt;();&lt;br /&gt;
        &lt;br /&gt;
        foreach (var item in data)&lt;br /&gt;
        {&lt;br /&gt;
            await semaphore.WaitAsync();&lt;br /&gt;
            &lt;br /&gt;
            tasks.Add(Task.Run(async () =&amp;gt;&lt;br /&gt;
            {&lt;br /&gt;
                try&lt;br /&gt;
                {&lt;br /&gt;
                    return await processItem(item);&lt;br /&gt;
                }&lt;br /&gt;
                finally&lt;br /&gt;
                {&lt;br /&gt;
                    semaphore.Release();&lt;br /&gt;
                }&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var results = await Task.WhenAll(tasks);&lt;br /&gt;
        &lt;br /&gt;
        result.SuccessCount = results.Count(r =&amp;gt; r);&lt;br /&gt;
        result.FailureCount = results.Count(r =&amp;gt; !r);&lt;br /&gt;
        &lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public class ImportResult&lt;br /&gt;
{&lt;br /&gt;
    public int SuccessCount { get; set; }&lt;br /&gt;
    public int FailureCount { get; set; }&lt;br /&gt;
    public int TotalCount =&amp;gt; SuccessCount + FailureCount;&lt;br /&gt;
    public double SuccessRate =&amp;gt; TotalCount &amp;gt; 0 ? (double)SuccessCount / TotalCount * 100 : 0;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Always validate before import&#039;&#039;&#039;&lt;br /&gt;
#* Check file format and structure&lt;br /&gt;
#* Validate required fields&lt;br /&gt;
#* Check data types and ranges&lt;br /&gt;
#* Identify duplicates&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Use transactions&#039;&#039;&#039;&lt;br /&gt;
#* Wrap imports in database transactions&lt;br /&gt;
#* Implement rollback on failure&lt;br /&gt;
#* Consider savepoints for partial commits&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Implement proper error handling&#039;&#039;&#039;&lt;br /&gt;
#* Log all errors with context&lt;br /&gt;
#* Provide detailed error reports&lt;br /&gt;
#* Allow partial success where appropriate&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Optimize for performance&#039;&#039;&#039;&lt;br /&gt;
#* Use batch processing for large datasets&lt;br /&gt;
#* Implement async/await patterns&lt;br /&gt;
#* Consider parallel processing where safe&lt;br /&gt;
#* Use streaming for large files&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Maintain data integrity&#039;&#039;&#039;&lt;br /&gt;
#* Check foreign key relationships&lt;br /&gt;
#* Validate business rules&lt;br /&gt;
#* Ensure referential integrity&lt;br /&gt;
#* Handle nullable fields properly&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Provide user feedback&#039;&#039;&#039;&lt;br /&gt;
#* Show progress indicators&lt;br /&gt;
#* Report success/failure counts&lt;br /&gt;
#* Export error logs&lt;br /&gt;
#* Allow preview before commit&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Security considerations&#039;&#039;&#039;&lt;br /&gt;
#* Validate file types and sizes&lt;br /&gt;
#* Scan for malicious content&lt;br /&gt;
#* Sanitize input data&lt;br /&gt;
#* Implement access controls&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Make imports resumable&#039;&#039;&#039;&lt;br /&gt;
#* Track processed records&lt;br /&gt;
#* Allow restart from failure point&lt;br /&gt;
#* Implement idempotent operations&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* [[Data Validation]]&lt;br /&gt;
* [[Transaction Management]]&lt;br /&gt;
* [[Error Handling]]&lt;br /&gt;
* [[Performance Optimization]]&lt;br /&gt;
* [[Database Operations]]&lt;br /&gt;
* [[File Management]]&lt;br /&gt;
* [[Logging System]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Import/Export]]&lt;br /&gt;
[[Category:Data Processing]]&lt;br /&gt;
[[Category:File Operations]]&lt;br /&gt;
[[Category:Technical Documentation]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=123</id>
		<title>Import PDF File Scripting - AI Importing</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Import_PDF_File_Scripting_-_AI_Importing&amp;diff=123"/>
		<updated>2025-11-10T02:53:05Z</updated>

		<summary type="html">&lt;p&gt;Mike: Created page with &amp;quot;Import PDF File Scripting - AI Importing&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Import PDF File Scripting - AI Importing&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Import_File_Scripting&amp;diff=122</id>
		<title>Import File Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Import_File_Scripting&amp;diff=122"/>
		<updated>2025-11-10T02:52:34Z</updated>

		<summary type="html">&lt;p&gt;Mike: Created page with &amp;quot;Import File Scripting&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Import File Scripting&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=121</id>
		<title>Current Release Features</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=121"/>
		<updated>2025-11-10T02:48:37Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Release Details ==&lt;br /&gt;
&#039;&#039;&#039;Current Release:&#039;&#039;&#039; 6.1.14.1&lt;br /&gt;
&lt;br /&gt;
=== Current Release Features: ===&lt;br /&gt;
Document Manager:&lt;br /&gt;
&lt;br /&gt;
* Advanced Scripting via new Business Layer.&lt;br /&gt;
&lt;br /&gt;
User Manager:&lt;br /&gt;
&lt;br /&gt;
* Additional Roles and Permissions available to the new Business Layer&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* Logo&lt;br /&gt;
* Postal and Physical Address for Documents&lt;br /&gt;
* Default Bank Account&lt;br /&gt;
* Advanced Invoice Settings&lt;br /&gt;
* Microsoft Graph Email Sending via Entra App.&lt;br /&gt;
* SMTP Settings&lt;br /&gt;
&lt;br /&gt;
Bank Manager:&lt;br /&gt;
&lt;br /&gt;
* Management of Bank Accounts&lt;br /&gt;
* New Bank Account Import Settings&lt;br /&gt;
** ANZ&lt;br /&gt;
** BNZ&lt;br /&gt;
** Westpac&lt;br /&gt;
** ASB (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
** Kiwibank (&#039;&#039;In Active Development&#039;&#039;)&lt;br /&gt;
Import File Admin&lt;br /&gt;
&lt;br /&gt;
* Generic CSV Import To allow for CSV/XLSX/JSON/XML file import to AP Invoice.&lt;br /&gt;
&lt;br /&gt;
Import PDF Admin (AI Enabled - Special License Required)&lt;br /&gt;
&lt;br /&gt;
* Reading of PDF AP Invoices Direct to AP Batch.&lt;br /&gt;
* Scripting to Control Every Aspect.&lt;br /&gt;
* Multi Page, Complex or Simple Invoices are handled quickly saving time on data import.&lt;br /&gt;
&lt;br /&gt;
Departments Admin&lt;br /&gt;
&lt;br /&gt;
* Management of Departments away from Zinform 5&lt;br /&gt;
* Additional Code Support to allow for up to 5 Codes&lt;br /&gt;
** Eg: Could be used to track Registration Number of a Vehicle to A Department, Trailers, ID Numbers etc for AP Invoices etc.&lt;br /&gt;
&lt;br /&gt;
=== Coming Soon: ===&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* IMAP Settings (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
CRM Module (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Email Processing (Coming Soon)&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=120</id>
		<title>Current Release Features</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Current_Release_Features&amp;diff=120"/>
		<updated>2025-11-10T02:40:29Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Release Details ==&lt;br /&gt;
&#039;&#039;&#039;Current Release:&#039;&#039;&#039; 6.1.14.1&lt;br /&gt;
&lt;br /&gt;
=== Current Release Features: ===&lt;br /&gt;
Document Manager:&lt;br /&gt;
&lt;br /&gt;
* Advanced Scripting via new Business Layer.&lt;br /&gt;
&lt;br /&gt;
User Manager:&lt;br /&gt;
&lt;br /&gt;
* Additional Roles and Permissions available to the new Business Layer&lt;br /&gt;
&lt;br /&gt;
Organisation Settings:&lt;br /&gt;
&lt;br /&gt;
* Logo&lt;br /&gt;
* Postal and Physical Address for Documents&lt;br /&gt;
* Default Bank Account&lt;br /&gt;
* Advanced Invoice Settings&lt;br /&gt;
* Microsoft Graph Email Sending via Entra App.&lt;br /&gt;
* SMTP Settings&lt;br /&gt;
* IMAP Settings (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Email Processing (Coming Soon)&lt;br /&gt;
&lt;br /&gt;
Bank Manager:&lt;br /&gt;
&lt;br /&gt;
* Management of Bank Accounts&lt;br /&gt;
* New Bank Account Import Settings&lt;br /&gt;
** ANZ&lt;br /&gt;
** BNZ&lt;br /&gt;
** Westpac&lt;br /&gt;
** ASB (Coming Soon)&lt;br /&gt;
** Kiwibank (Coming Soon)&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=119</id>
		<title>Document Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Document_Scripting&amp;diff=119"/>
		<updated>2025-11-10T00:14:39Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Document Scripting =&lt;br /&gt;
&lt;br /&gt;
Document Scripting allows you to create custom documents in ZinformAccounts by writing C# code that processes templates and data. This guide covers the core functions and patterns you&#039;ll use.&lt;br /&gt;
&lt;br /&gt;
== Getting Started ==&lt;br /&gt;
&lt;br /&gt;
Every document script follows this basic structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
    &lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        // Your code here&lt;br /&gt;
        return true; // or false if error&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; Your class must be named &amp;lt;code&amp;gt;DocumentScripter&amp;lt;/code&amp;gt; and have a &amp;lt;code&amp;gt;Main&amp;lt;/code&amp;gt; method with these exact parameters.&lt;br /&gt;
&lt;br /&gt;
== Working with ZinWord Documents ==&lt;br /&gt;
&lt;br /&gt;
=== Creating and Loading Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Create a new document&lt;br /&gt;
ZinWord document = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
// Load from file&lt;br /&gt;
document.LoadDocument(documentFileName);&lt;br /&gt;
&lt;br /&gt;
// Load from stream&lt;br /&gt;
document.LoadStream(memoryStream);&lt;br /&gt;
&lt;br /&gt;
// Clone an existing document (useful when processing multiple records)&lt;br /&gt;
ZinWord clonedDoc = sourceDocument.Clone(true);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Saving Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Save to file - format determined by extension&lt;br /&gt;
document.SaveDocument(saveFileName);&lt;br /&gt;
&lt;br /&gt;
// Examples:&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.pdf&amp;quot;);   // Saves as PDF&lt;br /&gt;
document.SaveDocument(&amp;quot;MyFile.docx&amp;quot;);  // Saves as Word document&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Checking Document Content ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Check if document has any content (text, tables, or images)&lt;br /&gt;
bool hasContent = zinBL.DocumentFunctions.DocumentHasContent(document);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Attaching Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Combine multiple documents into one (useful for bulk processing)&lt;br /&gt;
ZinWord bulkDocument = new ZinWord(zinBL);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument1);&lt;br /&gt;
bulkDocument.AttachDocument(sourceDocument2);&lt;br /&gt;
bulkDocument.SaveDocument(&amp;quot;Bulk_Output.pdf&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Working with Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
Bookmarks are placeholders in your Word templates that you replace with actual data.&lt;br /&gt;
&lt;br /&gt;
=== Setting Bookmark Values ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Set text value&lt;br /&gt;
document.SetBookmarkString(&amp;quot;BookmarkName&amp;quot;, &amp;quot;Value&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Set HTML content (for formatted text, tables, etc.)&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;BookmarkName&amp;quot;, htmlString);&lt;br /&gt;
&lt;br /&gt;
// Set hyperlink&lt;br /&gt;
document.SetBookmarkHyperLink(&amp;quot;BookmarkName&amp;quot;, hyperLinkItem);&lt;br /&gt;
&lt;br /&gt;
// Set image from file&lt;br /&gt;
document.SetBookmarkImageFromFile(&amp;quot;BookmarkName&amp;quot;, pictureFileName);&lt;br /&gt;
&lt;br /&gt;
// Set image from Picture object&lt;br /&gt;
document.SetBookmarkImage(&amp;quot;BookmarkName&amp;quot;, pictureItem);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Deleting Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Remove a bookmark and its content&lt;br /&gt;
document.DeleteBookmark(&amp;quot;BookmarkName&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Getting All Bookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Retrieve all bookmarks in the document&lt;br /&gt;
var bookmarks = document.GetBookmarks();&lt;br /&gt;
&lt;br /&gt;
foreach (var bookmark in bookmarks)&lt;br /&gt;
{&lt;br /&gt;
    // Process each bookmark&lt;br /&gt;
    string name = bookmark.Name;&lt;br /&gt;
    // ... do something with the bookmark&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Standard Bookmarks ==&lt;br /&gt;
&lt;br /&gt;
ZinformAccounts provides automatic processing for common bookmarks:&lt;br /&gt;
&lt;br /&gt;
=== ProcessStandardBookmarks ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
    document,           // ZinWord document to process&lt;br /&gt;
    otherParty,         // ZOtherParty object (customer/supplier)&lt;br /&gt;
    parameters,         // ZinParameters with document settings&lt;br /&gt;
    zinBL,             // Business layer instance&lt;br /&gt;
    logoWidth,         // Logo width in pixels (e.g., 193)&lt;br /&gt;
    logoHeight,        // Logo height in pixels (e.g., 70)&lt;br /&gt;
    minAddressLines,   // Minimum address lines (default: 4)&lt;br /&gt;
    addresseeFirstLineAddress,  // Use first line as addressee (default: false)&lt;br /&gt;
    stateAsCity        // Treat state as city (NZ DB) (default: false)&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Standard Bookmarks:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationLogo&amp;lt;/code&amp;gt; - Your company logo&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationName&amp;lt;/code&amp;gt; - Your company name&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationAddress&amp;lt;/code&amp;gt; - Your postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationPhone&amp;lt;/code&amp;gt; - Your phone number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationEmail&amp;lt;/code&amp;gt; - Your email address&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationWebsite&amp;lt;/code&amp;gt; - Your website URL&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccount&amp;lt;/code&amp;gt; - Bank account name and number&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountNumber&amp;lt;/code&amp;gt; - Bank account number only&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationBankAccountName&amp;lt;/code&amp;gt; - Bank account name only&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentDate&amp;lt;/code&amp;gt; - Current date (formatted as &amp;quot;dd MMMM yyyy&amp;quot;)&lt;br /&gt;
* &amp;lt;code&amp;gt;DocumentId&amp;lt;/code&amp;gt; - Document ID from parameters&lt;br /&gt;
* &amp;lt;code&amp;gt;GSTNumber&amp;lt;/code&amp;gt; - Tax registration number&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyId&amp;lt;/code&amp;gt; - Customer/Supplier ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyName&amp;lt;/code&amp;gt; - Customer/Supplier name&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartyAddress&amp;lt;/code&amp;gt; - Customer/Supplier postal address&lt;br /&gt;
* &amp;lt;code&amp;gt;OtherPartySalutation&amp;lt;/code&amp;gt; - Greeting (from Administrator/Manager contact)&lt;br /&gt;
* &amp;lt;code&amp;gt;ReferenceId&amp;lt;/code&amp;gt; - Reference ID&lt;br /&gt;
* &amp;lt;code&amp;gt;OrganisationNameFooter&amp;lt;/code&amp;gt; - Company name for footer&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For invoice-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ProcessInvoiceBookmarks ===&lt;br /&gt;
&lt;br /&gt;
Handles delivery address for invoices:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessInvoiceBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, transaction,&lt;br /&gt;
    minAddressLines, stateAsCity, &amp;quot;DELIVERY&amp;quot;&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported Bookmark:&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;DeliverToDetails&amp;lt;/code&amp;gt; - Delivery address (deleted if no delivery address exists)&lt;br /&gt;
&lt;br /&gt;
=== ProcessExtraStatementBookmarks ===&lt;br /&gt;
&lt;br /&gt;
For statement-specific bookmarks (duplicated bookmarks with &amp;quot;2&amp;quot; suffix):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
zinBL.DocumentFunctions.ProcessExtraStatementBookmarks(&lt;br /&gt;
    document, otherParty, parameters, zinBL, &lt;br /&gt;
    logoWidth, logoHeight, minAddressLines, &lt;br /&gt;
    addresseeFirstLineAddress, stateAsCity&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Working with Parameters ==&lt;br /&gt;
&lt;br /&gt;
Parameters pass data between the UI and your script.&lt;br /&gt;
&lt;br /&gt;
=== Getting Parameter Values ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get string value&lt;br /&gt;
string documentId = parameters.GetItemString(&amp;quot;DocumentId&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get datetime value&lt;br /&gt;
DateTime asAtDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get boolean value&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Get output extension&lt;br /&gt;
string extension = parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;);&lt;br /&gt;
string filter = parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setting Return Parameters ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Return the save location to the caller&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
&lt;br /&gt;
// Return error message if something fails&lt;br /&gt;
returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== File Operations ==&lt;br /&gt;
&lt;br /&gt;
=== Save File Dialog ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
    document.DocumentOutputFolder,           // Default folder&lt;br /&gt;
    String.Format(&amp;quot;Statement_{0}&amp;quot;, DateTime.Now.ToString(&amp;quot;yyyyMMdd_hhmmss&amp;quot;)), // Default filename&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),       // Default extension (e.g., &amp;quot;pdf&amp;quot;)&lt;br /&gt;
    parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)  // File type filter&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
if (saveLocation != null &amp;amp;&amp;amp; saveLocation.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // User selected a location&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    // User cancelled - handle error&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No File Save Location Chosen!&amp;quot;);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Creating Directories ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
string folderPath = Path.Combine(baseFolderPath, $&amp;quot;OtherParty_{otherPartyId}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
if (!Directory.Exists(folderPath))&lt;br /&gt;
{&lt;br /&gt;
    Directory.CreateDirectory(folderPath);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
string saveFileName = Path.Combine(folderPath, &amp;quot;Statement.pdf&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Working with DataTables ==&lt;br /&gt;
&lt;br /&gt;
When processing multiple records (e.g., statements for multiple customers):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Validate the DataTable parameter&lt;br /&gt;
DataTable otherPartyDataTable;&lt;br /&gt;
if (otherPartyDataTable != null &amp;amp;&amp;amp; otherPartyDataTable is DataTable)&lt;br /&gt;
{&lt;br /&gt;
    otherPartyDataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
}&lt;br /&gt;
else&lt;br /&gt;
{&lt;br /&gt;
    throw new InvalidOperationException(&amp;quot;otherPartyDataTable is not a valid DataTable&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Loop through rows&lt;br /&gt;
foreach (DataRow row in otherPartyDataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    // Get values from the row&lt;br /&gt;
    string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
    &lt;br /&gt;
    // Load the full OtherParty object&lt;br /&gt;
    var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
    &lt;br /&gt;
    // Process document for this party&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Progress Bar ==&lt;br /&gt;
&lt;br /&gt;
Show progress when processing multiple records:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, totalCount, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        // Check for cancellation&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Update progress&lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {customerName}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work here...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    // Mark as finished&lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Financial Functions ==&lt;br /&gt;
&lt;br /&gt;
=== Statement Data ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get statement details (aged balances)&lt;br /&gt;
ZinStatementDetails details = zinBL.DocumentFunctions.GetStatementDetails(&lt;br /&gt;
    zinBL, &lt;br /&gt;
    otherParty, &lt;br /&gt;
    statementDate, &lt;br /&gt;
    DocumentTypes.ARStatement  // or DocumentTypes.APRemittance&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// Access aged balances&lt;br /&gt;
decimal current = details.CurrentBalance;&lt;br /&gt;
decimal oneMonth = details.OneMonthOverdue;&lt;br /&gt;
decimal twoMonths = details.TwoMonthsOverdue;&lt;br /&gt;
decimal threeMonths = details.ThreeMonthsOverdue;&lt;br /&gt;
decimal total = details.TotalDue;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Transaction Data ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get monthly running transactions (preferred method)&lt;br /&gt;
var (transactions, statementDetails) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
    statementDate, &lt;br /&gt;
    otherPartyId, &lt;br /&gt;
    zinBL&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
// transactions is List&amp;lt;StatementTransaction2&amp;gt;&lt;br /&gt;
// statementDetails is ZinStatementDetails with aged balances&lt;br /&gt;
&lt;br /&gt;
// Process transactions&lt;br /&gt;
foreach (var tran in transactions)&lt;br /&gt;
{&lt;br /&gt;
    DateTime date = tran.Date;&lt;br /&gt;
    int type = tran.Type;&lt;br /&gt;
    string reference = tran.Reference;&lt;br /&gt;
    decimal value = tran.Value;&lt;br /&gt;
    decimal runningBalance = tran.RunningBalance;&lt;br /&gt;
    decimal subTotal = tran.SubTotal;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Combining Transaction Lists ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Combine and sort multiple transaction lists by date&lt;br /&gt;
var combined = zinBL.DocumentFunctions.CombineTransactions(listOne, listTwo);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Date Utilities ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get last day of month&lt;br /&gt;
DateTime monthEnd = DocumentFunctions.GetEndOfMonth(someDate);&lt;br /&gt;
// Example: 15/03/2024 returns 31/03/2024&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Building HTML Tables ==&lt;br /&gt;
&lt;br /&gt;
For inserting formatted tables into bookmarks:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var htmlTable = new StringBuilder();&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%; border-collapse: collapse;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Date&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Reference&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;td style=&#039;padding: 5px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;&amp;lt;b&amp;gt;Amount&amp;lt;/b&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/thead&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;tbody&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
foreach (var item in dataList)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine($&amp;quot;&amp;lt;td style=&#039;padding: 2px; text-align: right; font-family: \&amp;quot;Aptos\&amp;quot;, sans-serif;&#039;&amp;gt;{item.Amount:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/tbody&amp;gt;&amp;quot;);&lt;br /&gt;
htmlTable.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Insert into document&lt;br /&gt;
document.SetBookmarkHtml(&amp;quot;TableBookmark&amp;quot;, htmlTable.ToString());&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Tips for HTML Tables:&#039;&#039;&#039;&lt;br /&gt;
* Use inline styles (external CSS won&#039;t work)&lt;br /&gt;
* Specify &amp;lt;code&amp;gt;font-family: &amp;quot;Aptos&amp;quot;, sans-serif;&amp;lt;/code&amp;gt; for consistency&lt;br /&gt;
* Use &amp;lt;code&amp;gt;text-align: right&amp;lt;/code&amp;gt; for numbers&lt;br /&gt;
* Add padding for readability: &amp;lt;code&amp;gt;padding: 5px;&amp;lt;/code&amp;gt;&lt;br /&gt;
* Format currency: &amp;lt;code&amp;gt;{value:$#,##0.00}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Email Integration ==&lt;br /&gt;
&lt;br /&gt;
=== Sending Documents via Email ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Get email addresses for specific contacts&lt;br /&gt;
var emailAddresses = otherParty.GetEmailContactsAsEmailCompatibleStringById(document.EmailContactIds);&lt;br /&gt;
&lt;br /&gt;
if (emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Replace placeholders in email body&lt;br /&gt;
    string emailBody = document.EmailBody.Replace(&amp;quot;[OtherPartyName]&amp;quot;, otherParty.Name);&lt;br /&gt;
    &lt;br /&gt;
    // Send email with attachment&lt;br /&gt;
    var result = zinBL.Emailing.SendEmailWithImage(&lt;br /&gt;
        emailAddresses,              // To addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // CC addresses&lt;br /&gt;
        &amp;quot;&amp;quot;,                          // BCC addresses&lt;br /&gt;
        document.EmailSubject,       // Subject&lt;br /&gt;
        emailBody,                   // HTML body&lt;br /&gt;
        zinBL,                       // Business layer&lt;br /&gt;
        attachmentFilePath,          // Path to attachment&lt;br /&gt;
        false,                       // Is HTML&lt;br /&gt;
        70L,                         // Image quality&lt;br /&gt;
        444,                         // Max image width&lt;br /&gt;
        &amp;quot;unsubscribe@yourdomain.com&amp;quot; // Unsubscribe email&lt;br /&gt;
    );&lt;br /&gt;
    &lt;br /&gt;
    if (!result.success)&lt;br /&gt;
    {&lt;br /&gt;
        MessageBox.Show(result.errorMessage);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
try&lt;br /&gt;
{&lt;br /&gt;
    // Your code here&lt;br /&gt;
    &lt;br /&gt;
    if (somethingWentWrong)&lt;br /&gt;
    {&lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Clear error message here&amp;quot;);&lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
catch (Exception ex)&lt;br /&gt;
{&lt;br /&gt;
    MessageBox.Show($&amp;quot;An error occurred: {ex.Message}\n{ex.StackTrace}&amp;quot;, &amp;quot;Error&amp;quot;);&lt;br /&gt;
    returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, ex.Message);&lt;br /&gt;
    return false;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return true;  // Success&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Memory Management ===&lt;br /&gt;
&lt;br /&gt;
When processing many documents, clean up:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
{&lt;br /&gt;
    var clonedDoc = sourceDoc.Clone(true);&lt;br /&gt;
    &lt;br /&gt;
    // Process document...&lt;br /&gt;
    &lt;br /&gt;
    // If not saving this document, explicitly dispose&lt;br /&gt;
    clonedDoc = null;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Conditional Processing ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Only add to bulk document if not emailed&lt;br /&gt;
bool wasEmailed = false;&lt;br /&gt;
&lt;br /&gt;
if (document.EmailIfPossible &amp;amp;&amp;amp; emailAddresses.Length &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    // Send email&lt;br /&gt;
    wasEmailed = true;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
if (!wasEmailed)&lt;br /&gt;
{&lt;br /&gt;
    bulkDocument.AttachDocument(processedDoc);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Common Patterns ==&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Individual vs Bulk Documents ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
bool individual = parameters.GetItemBoolean(&amp;quot;IndividualDocuments&amp;quot;);&lt;br /&gt;
ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
&lt;br /&gt;
foreach (var otherParty in otherParties)&lt;br /&gt;
{&lt;br /&gt;
    var doc = ProcessDocument(otherParty);&lt;br /&gt;
    &lt;br /&gt;
    if (individual)&lt;br /&gt;
    {&lt;br /&gt;
        // Save individual file&lt;br /&gt;
        string fileName = $&amp;quot;{folderPath}/Document_{otherParty.OtherPartyId}.pdf&amp;quot;;&lt;br /&gt;
        doc.SaveDocument(fileName);&lt;br /&gt;
    }&lt;br /&gt;
    else&lt;br /&gt;
    {&lt;br /&gt;
        // Add to bulk document&lt;br /&gt;
        bulkDoc.AttachDocument(doc);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Save bulk document if not individual&lt;br /&gt;
if (!individual &amp;amp;&amp;amp; zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
{&lt;br /&gt;
    bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Processing with Progress ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
ProgressBarManager.Start(&amp;quot;Processing Documents&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    int currentCount = 1;&lt;br /&gt;
    &lt;br /&gt;
    foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
    {&lt;br /&gt;
        if (progressBar.CancellationPending || ProgressBarManager.IsCancelled())&lt;br /&gt;
        {&lt;br /&gt;
            break;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        progressBar.ReportProgress(currentCount, $&amp;quot;Processing: {currentItem}&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Do work...&lt;br /&gt;
        &lt;br /&gt;
        currentCount++;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    ProgressBarManager.Finished();&lt;br /&gt;
});&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Pattern: Empty Row Padding ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
int minLines = 13;&lt;br /&gt;
int currentLines = dataList.Count;&lt;br /&gt;
int emptyLines = Math.Max(0, minLines - currentLines);&lt;br /&gt;
&lt;br /&gt;
for (int i = 0; i &amp;lt; emptyLines; i++)&lt;br /&gt;
{&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;tr style=&#039;height: 12px;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
    htmlTable.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Add this using statement at top of your script:&lt;br /&gt;
using ZinformAccounts.Debugger;&lt;br /&gt;
&lt;br /&gt;
// Launch debugger at any point&lt;br /&gt;
DebuggerExtensions.LaunchNow(&amp;quot;Starting Main method&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
// Show debug information&lt;br /&gt;
MessageBox.Show($&amp;quot;Debug: {variableName}&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Complete Example ==&lt;br /&gt;
&lt;br /&gt;
Here&#039;s a simplified but complete statement script:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using System;&lt;br /&gt;
using System.Text;&lt;br /&gt;
using System.Windows;&lt;br /&gt;
using System.Data;&lt;br /&gt;
using ZinBusinessLayer;&lt;br /&gt;
using ZinBusinessLayer.HelperClasses;&lt;br /&gt;
using ZinBusinessLayer.Enums;&lt;br /&gt;
using ZinBusinessLayer.Word;&lt;br /&gt;
&lt;br /&gt;
public class DocumentScripter&lt;br /&gt;
{&lt;br /&gt;
    public static BusinessLayerMain zinBL;&lt;br /&gt;
    public static Document document;&lt;br /&gt;
&lt;br /&gt;
    public bool Main(BusinessLayerMain bl, object documentTemplate, object? otherPartyDataTable, &lt;br /&gt;
                     ZinParameters parameters, ZinParameters returnParameters, User? user)&lt;br /&gt;
    {&lt;br /&gt;
        zinBL = bl;&lt;br /&gt;
        &lt;br /&gt;
        if (!(documentTemplate is ZinWord sourceDoc))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;Invalid document template&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        document = zinBL.DocumentFunctions.GetDocument(parameters.GetItemString(&amp;quot;DocumentId&amp;quot;));&lt;br /&gt;
        DateTime statementDate = (DateTime)parameters.GetItemDateTime(&amp;quot;DateAsAt&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        string saveLocation = FileFunctions.OpenSaveFileDialog(&lt;br /&gt;
            document.DocumentOutputFolder,&lt;br /&gt;
            $&amp;quot;Statement_{DateTime.Now:yyyyMMdd_hhmmss}&amp;quot;,&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtension&amp;quot;),&lt;br /&gt;
            parameters.GetItemString(&amp;quot;OutputExtensionFilter&amp;quot;)&lt;br /&gt;
        );&lt;br /&gt;
        &lt;br /&gt;
        if (string.IsNullOrEmpty(saveLocation))&lt;br /&gt;
        {&lt;br /&gt;
            returnParameters.SetItemString(&amp;quot;ReturnError&amp;quot;, &amp;quot;No save location chosen&amp;quot;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        DataTable dataTable = (DataTable)otherPartyDataTable;&lt;br /&gt;
        ZinWord bulkDoc = new ZinWord(zinBL);&lt;br /&gt;
        &lt;br /&gt;
        ProgressBarManager.Start(&amp;quot;Processing Statements&amp;quot;, true, dataTable.Rows.Count, progressBar =&amp;gt;&lt;br /&gt;
        {&lt;br /&gt;
            int count = 1;&lt;br /&gt;
            &lt;br /&gt;
            foreach (DataRow row in dataTable.Rows)&lt;br /&gt;
            {&lt;br /&gt;
                if (progressBar.CancellationPending) break;&lt;br /&gt;
                &lt;br /&gt;
                string otherPartyId = (string)row[&amp;quot;OtherPartyId&amp;quot;];&lt;br /&gt;
                var otherParty = zinBL.OtherPartyFunctions.GetOtherPartyById(otherPartyId);&lt;br /&gt;
                &lt;br /&gt;
                progressBar.ReportProgress(count, $&amp;quot;Processing: {otherParty.Name}&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                var doc = sourceDoc.Clone(true);&lt;br /&gt;
                &lt;br /&gt;
                // Process bookmarks&lt;br /&gt;
                zinBL.DocumentFunctions.ProcessStandardBookmarks(&lt;br /&gt;
                    doc, otherParty, parameters, zinBL, 193, 70, 5&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Get statement data&lt;br /&gt;
                var (transactions, details) = zinBL.DocumentFunctions.GetMonthlyRunningTransactions2(&lt;br /&gt;
                    statementDate, otherPartyId, zinBL&lt;br /&gt;
                );&lt;br /&gt;
                &lt;br /&gt;
                // Build transaction table&lt;br /&gt;
                var html = new StringBuilder();&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;table style=&#039;width: 100%;&#039;&amp;gt;&amp;quot;);&lt;br /&gt;
                foreach (var tran in transactions)&lt;br /&gt;
                {&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;tr&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Date:dd/MM/yyyy}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td&amp;gt;{tran.Reference}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine($&amp;quot;&amp;lt;td style=&#039;text-align: right;&#039;&amp;gt;{tran.Value:$#,##0.00}&amp;lt;/td&amp;gt;&amp;quot;);&lt;br /&gt;
                    html.AppendLine(&amp;quot;&amp;lt;/tr&amp;gt;&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
                html.AppendLine(&amp;quot;&amp;lt;/table&amp;gt;&amp;quot;);&lt;br /&gt;
                &lt;br /&gt;
                doc.SetBookmarkHtml(&amp;quot;Transactions&amp;quot;, html.ToString());&lt;br /&gt;
                doc.SetBookmarkString(&amp;quot;TotalDue&amp;quot;, details.TotalDue.ToString(&amp;quot;#,##0.00&amp;quot;));&lt;br /&gt;
                &lt;br /&gt;
                bulkDoc.AttachDocument(doc);&lt;br /&gt;
                count++;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            ProgressBarManager.Finished();&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        if (zinBL.DocumentFunctions.DocumentHasContent(bulkDoc))&lt;br /&gt;
        {&lt;br /&gt;
            bulkDoc.SaveDocument(saveLocation);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        returnParameters.SetItemString(&amp;quot;SaveLocation&amp;quot;, saveLocation);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Problem !! Solution&lt;br /&gt;
|-&lt;br /&gt;
| Bookmark not found error || Check spelling - bookmarks are case-sensitive&lt;br /&gt;
|-&lt;br /&gt;
| HTML not rendering properly || Use inline styles only, no external CSS&lt;br /&gt;
|-&lt;br /&gt;
| Progress bar not showing || Ensure you call &amp;lt;code&amp;gt;ProgressBarManager.Finished()&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Document won&#039;t save || Check file path and ensure extension matches format&lt;br /&gt;
|-&lt;br /&gt;
| Changes not appearing || Make sure you&#039;re modifying the correct document instance (not the template)&lt;br /&gt;
|-&lt;br /&gt;
| Out of memory with many documents || Process in batches, dispose of documents after use&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Additional Resources ==&lt;br /&gt;
&lt;br /&gt;
* [[Transaction_Types|Transaction Types Reference]]&lt;br /&gt;
* [[Document_Templates|Creating Document Templates]]&lt;br /&gt;
* [[Bookmark_Reference|Complete Bookmark Reference]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Scripting]]&lt;br /&gt;
[[Category:Documents]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Minimum_System_Requirements&amp;diff=118</id>
		<title>Minimum System Requirements</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Minimum_System_Requirements&amp;diff=118"/>
		<updated>2025-11-10T00:05:41Z</updated>

		<summary type="html">&lt;p&gt;Mike: /* Microsoft SQL Server */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Supported Microsoft Windows Operating Systems ===&lt;br /&gt;
&#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; can operate on Windows 10 or Windows Server 2019 or later operating systems.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Minimum:&#039;&#039;&#039; Windows 10&lt;br /&gt;
* &#039;&#039;&#039;Recommended:&#039;&#039;&#039; Windows 11&lt;br /&gt;
* &#039;&#039;&#039;Minimum:&#039;&#039;&#039; Windows Server 2019&lt;br /&gt;
* &#039;&#039;&#039;Recommended:&#039;&#039;&#039; Windows Server 2019&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; might run under older versions of Windows, but they are not supported or recommended by Exodesk.&lt;br /&gt;
&lt;br /&gt;
These recommendations are based upon Microsoft&#039;s Windows product lifecycles. Please note that we do not support Windows versions where Microsoft has deemed them end of life.&lt;br /&gt;
&lt;br /&gt;
=== Microsoft .NET 9 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Zinform Accounts 6.x.x&#039;&#039;&#039; requires that the latest [https://dotnet.microsoft.com/en-us/download/dotnet/9.0 .net 9 runtime] is installed.&lt;br /&gt;
&lt;br /&gt;
=== Microsoft WebView2 Runtime ===&lt;br /&gt;
&#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; version 6.0.+ a requires the Microsoft WebView2 Runtime to be correctly installed on your computer. Click [https://developer.microsoft.com/en-us/microsoft-edge/webview2/?form=MA13LH here] for installer page.  We recommend that you install the Evergreen Standalone Installer.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Note, the Microsoft Edge WebView2 Runtime is automatically installed with Windows 11 and from June 2022 onwards with Windows 10.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Memory (RAM) ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Minimum:&#039;&#039;&#039; 8GB RAM.&lt;br /&gt;
* &#039;&#039;&#039;Recommended:&#039;&#039;&#039; 16GB RAM or more on 64-bit operating systems.&lt;br /&gt;
&lt;br /&gt;
=== Display Resolution ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Minimum:&#039;&#039;&#039; 1920 x 1080 or higher&lt;br /&gt;
&lt;br /&gt;
=== Network ===&lt;br /&gt;
&#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; should be run over a 1GB network if not running on a single PC / RDP Server due to data requirements.&lt;br /&gt;
&lt;br /&gt;
=== Microsoft SQL Server ===&lt;br /&gt;
The &#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; &#039;&#039;&#039;6&#039;&#039;&#039; requires Microsoft SQL Server 2019 and higher database software.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Required:&#039;&#039;&#039; SQL Server 2019 or 2022 (Express or Better).&lt;br /&gt;
* &#039;&#039;&#039;Min Compatibility Level: 150&#039;&#039;&#039;&lt;br /&gt;
[[File:CompatabilityLevel.png|thumb|510x510px|Compatibility Level]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=File:CompatabilityLevel.png&amp;diff=117</id>
		<title>File:CompatabilityLevel.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=File:CompatabilityLevel.png&amp;diff=117"/>
		<updated>2025-11-10T00:03:27Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Compatability Level&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Minimum_System_Requirements&amp;diff=116</id>
		<title>Minimum System Requirements</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Minimum_System_Requirements&amp;diff=116"/>
		<updated>2025-11-10T00:00:28Z</updated>

		<summary type="html">&lt;p&gt;Mike: .Net generic page&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Supported Microsoft Windows Operating Systems ===&lt;br /&gt;
&#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; can operate on Windows 10 or Windows Server 2019 or later operating systems.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Minimum:&#039;&#039;&#039; Windows 10&lt;br /&gt;
* &#039;&#039;&#039;Recommended:&#039;&#039;&#039; Windows 11&lt;br /&gt;
* &#039;&#039;&#039;Minimum:&#039;&#039;&#039; Windows Server 2019&lt;br /&gt;
* &#039;&#039;&#039;Recommended:&#039;&#039;&#039; Windows Server 2019&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; might run under older versions of Windows, but they are not supported or recommended by Exodesk.&lt;br /&gt;
&lt;br /&gt;
These recommendations are based upon Microsoft&#039;s Windows product lifecycles. Please note that we do not support Windows versions where Microsoft has deemed them end of life.&lt;br /&gt;
&lt;br /&gt;
=== Microsoft .NET 9 ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Zinform Accounts 6.x.x&#039;&#039;&#039; requires that the latest [https://dotnet.microsoft.com/en-us/download/dotnet/9.0 .net 9 runtime] is installed.&lt;br /&gt;
&lt;br /&gt;
=== Microsoft WebView2 Runtime ===&lt;br /&gt;
&#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; version 6.0.+ a requires the Microsoft WebView2 Runtime to be correctly installed on your computer. Click [https://developer.microsoft.com/en-us/microsoft-edge/webview2/?form=MA13LH here] for installer page.  We recommend that you install the Evergreen Standalone Installer.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Note, the Microsoft Edge WebView2 Runtime is automatically installed with Windows 11 and from June 2022 onwards with Windows 10.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Memory (RAM) ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Minimum:&#039;&#039;&#039; 8GB RAM.&lt;br /&gt;
* &#039;&#039;&#039;Recommended:&#039;&#039;&#039; 16GB RAM or more on 64-bit operating systems.&lt;br /&gt;
&lt;br /&gt;
=== Display Resolution ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Minimum:&#039;&#039;&#039; 1920 x 1080 or higher&lt;br /&gt;
&lt;br /&gt;
=== Network ===&lt;br /&gt;
&#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; should be run over a 1GB network if not running on a single PC / RDP Server due to data requirements.&lt;br /&gt;
&lt;br /&gt;
=== Microsoft SQL Server ===&lt;br /&gt;
The &#039;&#039;&#039;Zinform Accounts&#039;&#039;&#039; &#039;&#039;&#039;6&#039;&#039;&#039; requires Microsoft SQL Server 2019 and higher database software.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Required:&#039;&#039;&#039; SQL Server 2019 or 2022 (Express or Better).&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Permissions_and_Roles&amp;diff=115</id>
		<title>Permissions and Roles</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Permissions_and_Roles&amp;diff=115"/>
		<updated>2025-09-30T01:36:15Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
= User Roles &amp;amp; Permissions Guide =&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The system has six user roles, each with different permission levels. Think of it as a video game tier list, but for business operations. 🎮&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Min Zinform 6 Version ==&lt;br /&gt;
The minimum system version for to enable roles is 6.1.13.0&lt;br /&gt;
&lt;br /&gt;
== Role Hierarchy ==&lt;br /&gt;
&lt;br /&gt;
=== 🦸‍♂️ Super Administrator ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;God Mode Activated&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Full system access. Can do literally everything.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* All administrative functions&lt;br /&gt;
* Global settings configuration&lt;br /&gt;
* License management&lt;br /&gt;
* User management (create, update)&lt;br /&gt;
* Organization details&lt;br /&gt;
* Email &amp;amp; banking settings&lt;br /&gt;
* File &amp;amp; PDF imports&lt;br /&gt;
* Department management&lt;br /&gt;
* Document creation &amp;amp; updates&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; System owner, IT director, or that one person who knows where all the bodies are buried.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== 👑 Administrator ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Almost Everything Except the Nuclear Codes&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Company-level admin with near-complete access. Currently identical to SuperAdmin (you might want to revisit this).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* Same as SuperAdministrator&lt;br /&gt;
* Access to all operational and administrative functions&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Senior management, operations director, trusted lieutenant.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;⚠️ Note:&#039;&#039;&#039; The comments suggest &amp;lt;code&amp;gt;CanAccessGlobalSettings&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;CanUpdateLicense&amp;lt;/code&amp;gt; should probably be SuperAdmin-only. Worth reviewing.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== 💪 Super User ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Power User with Training Wheels Off&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Can handle most day-to-day operations but can&#039;t mess with the company structure or users.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* ✅ Document management (create, update)&lt;br /&gt;
* ✅ File &amp;amp; PDF imports&lt;br /&gt;
* ✅ Department management&lt;br /&gt;
* ✅ Admin menu access&lt;br /&gt;
* ❌ Organization settings&lt;br /&gt;
* ❌ User management&lt;br /&gt;
* ❌ Financial settings&lt;br /&gt;
* ❌ License updates&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Team lead, operations manager, power user who gets stuff done.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== ⚡ Power User ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Operational Access, Hold the Danger&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
More limited than SuperUser. Can manage departments but not much else.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* ✅ Department management&lt;br /&gt;
* ✅ Admin menu access&lt;br /&gt;
* ❌ Everything else&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Department head, mid-level manager, someone who needs organizational visibility but limited editing rights.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== 👤 Base User ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Standard Issue Employee&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Basic operational access. Can use the system but can&#039;t change anything structural.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* ❌ All administrative functions disabled&lt;br /&gt;
* Can view and use standard features (implied)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Regular employees, data entry staff, most of your workforce.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== 👀 Read-only User ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Look But Don&#039;t Touch&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
View-only access. The digital equivalent of a museum visitor.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* ❌ Everything is disabled&lt;br /&gt;
* ✅ IsReadOnly flag enabled&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Auditors, consultants, stakeholders, interns, or anyone who needs to see data but &lt;br /&gt;
shouldn&#039;t change anything.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Quick Reference Matrix ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Permission&lt;br /&gt;
!Super Admin&lt;br /&gt;
!Admin&lt;br /&gt;
!Super User&lt;br /&gt;
!Power User&lt;br /&gt;
!Base User&lt;br /&gt;
!Read Only&lt;br /&gt;
|-&lt;br /&gt;
|Organization Details&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Global Settings&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Admin Menu&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|License Management&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Document Management&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|User Management&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Email Settings&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Banking Settings&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|File/PDF Imports&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Department Management&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Read Only&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|}&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Start Restrictive:&#039;&#039;&#039; Assign the minimum role needed. You can always promote users later.&lt;br /&gt;
# &#039;&#039;&#039;Regular Audits:&#039;&#039;&#039; Review user roles quarterly. That intern from 2019 probably doesn&#039;t need SuperAdmin anymore.&lt;br /&gt;
# &#039;&#039;&#039;Separation of Duties:&#039;&#039;&#039; Keep financial and user management permissions limited to trusted admins.&lt;br /&gt;
# &#039;&#039;&#039;Document Changes:&#039;&#039;&#039; Log role changes, especially escalations to Admin/SuperAdmin.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Notes for Developers ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Admin vs SuperAdmin:&#039;&#039;&#039; Currently identical. Consider restricting &amp;lt;code&amp;gt;CanAccessGlobalSettings&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;CanUpdateLicense&amp;lt;/code&amp;gt; to SuperAdmin only.&lt;br /&gt;
* &#039;&#039;&#039;Money = Admin Territory:&#039;&#039;&#039; Financial permissions deliberately restricted to admin roles.&lt;br /&gt;
* &#039;&#039;&#039;BaseUser:&#039;&#039;&#039; Currently has no explicit permissions. Define what they &#039;&#039;can&#039;&#039; do in your application logic.&lt;br /&gt;
&lt;br /&gt;
== Role Configuration ==&lt;br /&gt;
Roles are setup on the Users form from the Admin menu.&lt;br /&gt;
[[File:Image RoleConfig.png|left|thumb]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=File:Image_RoleConfig.png&amp;diff=114</id>
		<title>File:Image RoleConfig.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=File:Image_RoleConfig.png&amp;diff=114"/>
		<updated>2025-09-30T01:35:35Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Role Config&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Permissions_and_Roles&amp;diff=113</id>
		<title>Permissions and Roles</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Permissions_and_Roles&amp;diff=113"/>
		<updated>2025-09-30T01:32:46Z</updated>

		<summary type="html">&lt;p&gt;Mike: /* Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
= User Roles &amp;amp; Permissions Guide =&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The system has six user roles, each with different permission levels. Think of it as a video game tier list, but for business operations. 🎮&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Min Zinform 6 Version ==&lt;br /&gt;
The minimum system version for to enable roles is 6.1.13.0&lt;br /&gt;
&lt;br /&gt;
== Role Hierarchy ==&lt;br /&gt;
&lt;br /&gt;
=== 🦸‍♂️ Super Administrator ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;God Mode Activated&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Full system access. Can do literally everything.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* All administrative functions&lt;br /&gt;
* Global settings configuration&lt;br /&gt;
* License management&lt;br /&gt;
* User management (create, update)&lt;br /&gt;
* Organization details&lt;br /&gt;
* Email &amp;amp; banking settings&lt;br /&gt;
* File &amp;amp; PDF imports&lt;br /&gt;
* Department management&lt;br /&gt;
* Document creation &amp;amp; updates&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; System owner, IT director, or that one person who knows where all the bodies are buried.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== 👑 Administrator ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Almost Everything Except the Nuclear Codes&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Company-level admin with near-complete access. Currently identical to SuperAdmin (you might want to revisit this).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* Same as SuperAdministrator&lt;br /&gt;
* Access to all operational and administrative functions&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Senior management, operations director, trusted lieutenant.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;⚠️ Note:&#039;&#039;&#039; The comments suggest &amp;lt;code&amp;gt;CanAccessGlobalSettings&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;CanUpdateLicense&amp;lt;/code&amp;gt; should probably be SuperAdmin-only. Worth reviewing.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== 💪 Super User ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Power User with Training Wheels Off&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Can handle most day-to-day operations but can&#039;t mess with the company structure or users.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* ✅ Document management (create, update)&lt;br /&gt;
* ✅ File &amp;amp; PDF imports&lt;br /&gt;
* ✅ Department management&lt;br /&gt;
* ✅ Admin menu access&lt;br /&gt;
* ❌ Organization settings&lt;br /&gt;
* ❌ User management&lt;br /&gt;
* ❌ Financial settings&lt;br /&gt;
* ❌ License updates&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Team lead, operations manager, power user who gets stuff done.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== ⚡ Power User ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Operational Access, Hold the Danger&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
More limited than SuperUser. Can manage departments but not much else.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* ✅ Department management&lt;br /&gt;
* ✅ Admin menu access&lt;br /&gt;
* ❌ Everything else&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Department head, mid-level manager, someone who needs organizational visibility but limited editing rights.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== 👤 Base User ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Standard Issue Employee&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Basic operational access. Can use the system but can&#039;t change anything structural.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* ❌ All administrative functions disabled&lt;br /&gt;
* Can view and use standard features (implied)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Regular employees, data entry staff, most of your workforce.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== 👀 Read-only User ===&lt;br /&gt;
&#039;&#039;&#039;&amp;quot;Look But Don&#039;t Touch&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
View-only access. The digital equivalent of a museum visitor.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key Permissions:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* ❌ Everything is disabled&lt;br /&gt;
* ✅ IsReadOnly flag enabled&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Use Case:&#039;&#039;&#039; Auditors, consultants, stakeholders, interns, or anyone who needs to see data but &lt;br /&gt;
shouldn&#039;t change anything.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Quick Reference Matrix ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Permission&lt;br /&gt;
!Super Admin&lt;br /&gt;
!Admin&lt;br /&gt;
!Super User&lt;br /&gt;
!Power User&lt;br /&gt;
!Base User&lt;br /&gt;
!Read Only&lt;br /&gt;
|-&lt;br /&gt;
|Organization Details&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Global Settings&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Admin Menu&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|License Management&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Document Management&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|User Management&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Email Settings&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Banking Settings&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|File/PDF Imports&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Department Management&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|-&lt;br /&gt;
|Read Only&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{no}}&lt;br /&gt;
|{{yes}}&lt;br /&gt;
|}&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Best Practices ==&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Start Restrictive:&#039;&#039;&#039; Assign the minimum role needed. You can always promote users later.&lt;br /&gt;
# &#039;&#039;&#039;Regular Audits:&#039;&#039;&#039; Review user roles quarterly. That intern from 2019 probably doesn&#039;t need SuperAdmin anymore.&lt;br /&gt;
# &#039;&#039;&#039;Separation of Duties:&#039;&#039;&#039; Keep financial and user management permissions limited to trusted admins.&lt;br /&gt;
# &#039;&#039;&#039;Document Changes:&#039;&#039;&#039; Log role changes, especially escalations to Admin/SuperAdmin.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Notes for Developers ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Admin vs SuperAdmin:&#039;&#039;&#039; Currently identical. Consider restricting &amp;lt;code&amp;gt;CanAccessGlobalSettings&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;CanUpdateLicense&amp;lt;/code&amp;gt; to SuperAdmin only.&lt;br /&gt;
* &#039;&#039;&#039;Money = Admin Territory:&#039;&#039;&#039; Financial permissions deliberately restricted to admin roles.&lt;br /&gt;
* &#039;&#039;&#039;BaseUser:&#039;&#039;&#039; Currently has no explicit permissions. Define what they &#039;&#039;can&#039;&#039; do in your application logic.&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
	<entry>
		<id>https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=112</id>
		<title>Zinform Wiki Home</title>
		<link rel="alternate" type="text/html" href="https://wiki.zinform.co.nz/index.php?title=Zinform_Wiki_Home&amp;diff=112"/>
		<updated>2025-09-30T01:31:14Z</updated>

		<summary type="html">&lt;p&gt;Mike: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== &amp;lt;strong&amp;gt;Zinform Main Help Page&amp;lt;/strong&amp;gt; ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Getting started ==&lt;br /&gt;
[[Minimum System Requirements]]&lt;br /&gt;
&lt;br /&gt;
[[Guide to installing Zinform Accounts V6|Installation of Zinform Accounts]]&lt;br /&gt;
&lt;br /&gt;
[[Invoice Printing Direct via New Zinform]]&lt;br /&gt;
&lt;br /&gt;
[[Installing SQL and Zinform]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Features ==&lt;br /&gt;
[[Current Release Features|List of features in current release]]&lt;br /&gt;
&lt;br /&gt;
[[Floating Views]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Scripting ==&lt;br /&gt;
[[Document Scripting|Document scripting]]&lt;br /&gt;
&lt;br /&gt;
== Zinform Accounts 6 Permissions and Roles ==&lt;br /&gt;
[[Permissions and Roles]]&lt;/div&gt;</summary>
		<author><name>Mike</name></author>
	</entry>
</feed>