Codebasis als Dateien ins Arbeitsrepo statt als Gitlink

QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

+15
View File
@@ -0,0 +1,15 @@
LandingPage
How-to-release-stop
How-to-update-DevExpress
GeneralStructure
How-to-add-webservice-methods
How-to-check-userrights
How-to-create-module
How-to-create-settings-page
Our-build-server-and-automated-builds
On-MVVM-in-centron
How-to-create-settings
How-to-create-dialog
How-to-end-to-end-test
How-to-create-scripts
Tapi
@@ -0,0 +1,171 @@
# DataQualityService
## Purpose
The `DataQualityService` is an ASP.NET Core `BackgroundService` that runs periodically to:
- Clean up outdated data
- Fix data inconsistencies
- Update missing database values
- Validate and repair relationships between database entities
- Perform regular maintenance tasks on database records
## Implementation Details
### Core Structure
- Inherits from `BackgroundService` (Microsoft.Extensions.Hosting)
- Runs every hour on a continuous schedule while the application is active
- Each maintenance task is executed sequentially in the `ExecuteAsync` method
### Execution Pattern
- The service runs in an infinite loop until a cancellation is requested
- Tasks are executed with 1-hour intervals between full cycle executions
- Cancellation is checked between each task to allow for graceful shutdown
## Task Implementation Rules
When implementing tasks in the `DataQualityService`:
### 1. Session Management
- Each task must create its own `BLSession` instance within a `using` statement
- Sessions should be short-lived and disposed immediately after the task completes
```csharp
using (var session = new BLSession())
{
// Task implementation here
}
```
### 2. Error Handling
- Every task must be wrapped in a try-catch block
- Exceptions should be logged with the specific task name in the error message
- Tasks should not crash the service; errors should be contained
```csharp
try
{
// Task implementation
}
catch (Exception e)
{
Logger.Error(e, "Error while executing data quality service - [TASK NAME]");
}
```
### 3. Cancellation Checking
- After each task, check if cancellation has been requested
- Break the execution loop if cancellation is detected
```csharp
if (stoppingToken.IsCancellationRequested)
{
break;
}
```
### 4. Business Logic Access
- Use the session's `GetBL<T>()` method to access the appropriate business logic class
- Call only methods that are designed for data quality operations
```csharp
session.GetBL<BusinessLogicClassName>().DataQualityMethodName();
```
## Adding New Data Quality Tasks
When adding a new task to the `DataQualityService`:
### 1. Task Placement
- Add the new task after existing tasks
- Follow the established pattern of try-catch-cancellation check
### 2. Business Logic Implementation
- Create a dedicated method in the appropriate BL class
- Prefix data quality specific methods with `DataQuality` (e.g., `DataQualityCleanupSecondaryStockArticles`)
- For specialized operations that need filters, create appropriate filter classes (e.g., `TicketPatternUpdateCustomerMappingsFilter`)
### 3. Performance Considerations
- Design tasks to be efficient and lightweight
- Avoid long-running operations that might block other tasks
- Consider implementing pagination for large datasets
### 4. Documentation
- Add a descriptive error message that clearly identifies the task
- Comment the task implementation if it performs complex operations
## Existing Task Reference
The `DataQualityService` currently handles these tasks:
1. **Ticket pattern customer mappings updates**
- Updates customer mappings for ticket patterns
- Uses `TicketPatternUpdateCustomerMappingsFilter`
2. **Directory checks**
- Executes directory validation and checking
- Uses `DirectoryCheckBL.ExecuteDirectoryCheck()`
3. **Cleanup of Centron notifications**
- Removes old or expired notifications
- Uses `CentronNotificationsBL.CleanupCentronNotifications()`
4. **Checklist customer mappings updates**
- Updates customer mappings for checklists
- Uses `CentronChecklistUpdateCustomerMappingsFilter`
5. **Profiler entries reorganization**
- Reorganizes and optimizes profiler entries
- Uses `ProfilerBL.ReorganizeProfilerEntries()`
6. **Todo-list missing Account I3D filling**
- Fills missing Account I3Ds in Todo items
- Uses `ToDoBL.DataQualityFillAccountI3D()`
7. **AccountTypeToAccounts table repair**
- Checks and repairs AccountTypeToAccounts relationships
- Uses `AccountBL.CheckAndRepairAccountTypeToAccountsTable()`
8. **HelpdeskTimer properties updates**
- Updates missing properties in HelpdeskTimer records
- Uses `HelpdeskTimerBL.DataQualityUpdateMissingHelpdeskTimerProperties()`
9. **Secondary stock articles cleanup**
- Cleans up secondary stock article records
- Uses `SecondStockArticleBL.DataQualityCleanupSecondaryStockArticles()`
## Best Practices
### 1. Task Independence
- Each task should be independent of other tasks
- Failure in one task should not affect subsequent tasks
### 2. Resource Utilization
- Consider the server load when implementing new tasks
- Use database indexes for heavy operations
- Run operations during off-peak hours if possible
### 3. Logging
- Log the start and completion of tasks at the Debug or Info level for monitoring
- Include sufficient context in error messages for troubleshooting
### 4. Testing
- Test new tasks thoroughly in a development environment
- Verify that new tasks fix the intended issues without side effects
## Example Implementation
Adding a new data quality task:
```csharp
if (stoppingToken.IsCancellationRequested)
{
break;
}
try
{
using (var session = new BLSession())
{
session.GetBL<YourBusinessLogic>().DataQualityYourNewTask();
}
}
catch (Exception e)
{
Logger.Error(e, "Error while executing data quality service - Your new task description");
}
```
+80
View File
@@ -0,0 +1,80 @@
# c-entron.NET Documentation
This directory contains documentation for the c-entron.NET project, organized by category.
## Documentation Categories
### [Getting Started](getting-started/)
- [General Structure](getting-started/general-structure.md)
- [AI / codebase navigation](getting-started/ai-codebase-navigation.md) (large-repo map: where to search)
- [Documentation Rules](getting-started/documentation-rules.md)
### Guides
#### [Development Guides](guides/development/)
- [Add a New Right](guides/development/add-a-new-right.md)
- [Check User Rights](guides/development/check-userrights.md)
- [Create Mail Templates](guides/development/create-mail-templates.md)
- [End-to-End Testing](guides/development/end-to-end-testing.md)
- [Fixed Memory Leaks](guides/development/fixed-memory-leaks.md)
- [Settings Management](guides/development/settings-management.md)
- [XRechnung](guides/development/xrechnung.md)
#### [Database Guides](guides/database/)
- [Create Scripts](guides/database/create-scripts.md)
- [Database Conventions](guides/database/database-conventions.md)
#### [UI Guides](guides/ui/)
- [Create Dialog](guides/ui/create-dialog.md)
- [Create Module](guides/ui/create-module.md)
- [Create Settings Pages](guides/ui/create-settings-page.md)
- [Localization](guides/ui/localization.md)
#### [Services Guides](guides/services/)
- [Add Webservice Methods](guides/services/add-webservice-methods.md)
- [Web Service on Linux](guides/services/web-service-on-linux.md)
### Reference Documentation
#### [Database Reference](reference/database/)
- [Script Rules](reference/database/script-rules.md)
#### [Security Reference](reference/security/)
- [Developer Security](reference/security/developer-security.md)
- [Licensing System](reference/security/licensing-system.md)
#### [Receipts Reference](reference/receipts/)
- [ActionPrice System](reference/receipts/actionprice-system.md)
- [Receipts Backend Architecture](reference/receipts/receipts-backend-architecture.md)
- [Contracts Backend](reference/receipts/contracts-backend.md)
- [Contract Billing RMM Article Logic](reference/receipts/contract-billing-rmm-article-logic.md)
- [Receipt Search Architecture](reference/receipts/receipt-search-architecture.md)
#### [Architecture Reference](reference/architecture/)
- [DTOs and Entities](reference/architecture/dtos-and-entities.md)
- [MVVM in Centron](reference/architecture/mvvm-in-centron.md)
- [Requests and Responses](reference/architecture/requests-and-responses.md)
- [Results and Responses](reference/architecture/results-and-responses.md)
- [Stanislau's Secret API Documentation](reference/architecture/stanislaus-secret-api-documentation.md)
- [TAPI](reference/architecture/tapi.md)
#### [EDI Reference](reference/edi/)
- [EDI Architecture](reference/edi/edi-architecture.md)
- [EDI Import Rules](reference/edi/edi-import-rules.md)
#### Data Exchange Reference
- [ZUGFeRD Field Mapping](reference/zugferd-field-mapping.md) (Technical)
- [ZUGFeRD Feldzuordnung](reference/zugferd-feldzuordnung-anwender.md) (Anwenderdokumentation)
### [Operations](operations/)
- [Build Server and Automated Builds](operations/build-server-and-automated-builds.md)
- [Release Stop](operations/release-stop.md)
- [Update DevExpress](operations/update-devexpress.md)
## Contributing to Documentation
When adding new documentation:
1. Place it in the appropriate category folder
2. Use lowercase filenames with hyphens (e.g., `my-document-name.md`)
3. Update this README.md to include a link to your new document
4. Follow the existing documentation style
@@ -0,0 +1,720 @@
# Automatic Helpdesk Creation Templates
**Feature Branch**: `ral/T-162548-vorlage-autom-ticket-erstellung`
**Implementation Date**: 2026-02-12
**Status**: ✅ Completed
## Overview
This feature implements a comprehensive template system for automatic helpdesk ticket creation from orders. Users can now create, save, and manage multiple preset configurations for ticket creation, with one template designated as the standard default.
## Table of Contents
1. [Features](#features)
2. [User Interface](#user-interface)
3. [Technical Implementation](#technical-implementation)
4. [Database Schema](#database-schema)
5. [Architecture](#architecture)
6. [Usage Guide](#usage-guide)
7. [Configuration](#configuration)
---
## Features
### Core Functionality
- ✅ **Template Management**: Create, save, load, update, and delete ticket creation templates
- ✅ **Standard Template**: Designate one template as the standard (only one at a time)
- ✅ **Auto-Load**: Automatically load the last selected template when opening the UI
- ✅ **Template Persistence**: Selected template is saved and restored across sessions
- ✅ **Field Preservation**: Form fields remain unchanged during template operations
- ✅ **Visual Indicators**: Standard templates are displayed in bold with a checkmark icon
- ✅ **Template Updates**: Automatically update templates when saving settings
### New Fields Added
1. **Category (Kategorie)**: Main category for helpdesk tickets
2. **CreateSeparateTicketsMode**: Choice between Single, Group, or Custom ticket creation
3. **OpenAfterwards**: Whether to open tickets after creation
4. **CreateTicketForAll**: Create tickets for all order positions or only those without tickets
---
## User Interface
### Template Dropdown
**Location**: `Belege → Einstellungen → Bestellung → Automatische Helpdeskfallerstellung → Vorlagen`
**Components**:
- ComboBox with template selection (shows "Keine Vorlage ausgewählt" when none selected)
- Standard templates display with:
- ✓ Checkmark icon
- **Bold text**
**Actions**:
- Three action buttons:
1. **Als Vorlage speichern**: Save current form values as a new template
2. **Als Standard setzen**: Set selected template as the standard
3. **Vorlage löschen**: Delete selected template
### Template Workflow
```
┌─────────────────────────────────────────────────────────────┐
│ 1. User opens UI │
│ → Last selected template is loaded (if exists) │
│ → Or standard template is shown (if no previous selection)│
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. User selects a template │
│ → All form fields are populated with template values │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. User modifies fields │
│ → Form values change │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. User clicks "Speichern" │
│ → ApplicationSettings are saved │
│ → Selected template is updated with new values │
│ → Template selection is persisted │
└─────────────────────────────────────────────────────────────┘
```
### Field Clearing
When "Keine" (none) is selected in the template dropdown:
- All form fields are cleared to default/empty values
- User can manually fill in values
- Clicking "Speichern" saves only to ApplicationSettings (no template update)
---
## Technical Implementation
### Backend Components
#### 1. Database Script
**File**: `ScriptMethod11751.cs`
Creates the `HelpdeskCreationTemplate` table with:
- All template configuration fields
- Standard tracking columns (CreatedByI3D, CreatedDate, ChangedByI3D, ChangedDate, IsDeleted, DeletedByI3D, DeletedDate)
- Indexes on `IsStandard` and `IsDeleted` for performance
```csharp
ScriptHelpers.AddTableIfNotExists("HelpdeskCreationTemplate",
("Name", "nvarchar(255)", false, null),
("IsStandard", "bit", false, "0"),
("TypeI3D", "int", true, null),
("CategoryI3D", "int", true, null),
// ... additional fields
);
```
#### 2. Entity Class
**File**: `HelpdeskCreationTemplate.cs`
```csharp
public class HelpdeskCreationTemplate : BaseEntity
{
public virtual string Name { get; set; }
public virtual bool IsStandard { get; set; }
public virtual int? TypeI3D { get; set; }
public virtual int? CategoryI3D { get; set; }
// ... additional properties
}
```
#### 3. NHibernate Mapping
**File**: `HelpdeskCreationTemplateMaps.cs`
FluentNHibernate mapping with proper nullability and column lengths.
#### 4. Data Transfer Object
**File**: `HelpdeskCreationTemplateDTO.cs`
```csharp
[DataContract]
public class HelpdeskCreationTemplateDTO
{
[DataMember]
public int I3D { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public bool IsStandard { get; set; }
// ... additional properties
}
```
#### 5. Business Logic
**File**: `HelpdeskCreationTemplateBL.cs`
Key methods:
- `GetTemplateByI3D(int i3D)`: Retrieve template by ID
- `GetAllTemplates()`: Get all active templates
- `GetStandardTemplate()`: Get the standard template
- `SaveTemplate(entity, currentUserI3D)`: Create or update template
- `DeleteTemplate(templateI3D, currentUserI3D)`: Soft delete template
- `SetStandardTemplate(templateI3D, currentUserI3D)`: Set as standard (ensures only one)
**Business Rules**:
- Only one template can be marked as standard at a time
- Cannot delete the standard template
- Soft delete pattern (sets `IsDeleted = true`)
#### 6. Web Service Layer
**File**: `HelpdeskCreationTemplateWebServiceBL.cs`
Handles DTO ↔ Entity conversion and delegates to BL.
#### 7. Logic Interfaces
**Files**: `ITicketLogic.cs`, `BLTicketLogic.cs`, `WSTicketLogic.cs`
Added methods:
```csharp
Task<Result<List<HelpdeskCreationTemplateDTO>>> GetAllHelpdeskCreationTemplates();
Task<Result<HelpdeskCreationTemplateDTO>> GetHelpdeskCreationTemplateByI3D(int templateI3D);
Task<Result<HelpdeskCreationTemplateDTO>> GetStandardHelpdeskCreationTemplate();
Task<Result<HelpdeskCreationTemplateDTO>> SaveHelpdeskCreationTemplate(HelpdeskCreationTemplateDTO template);
Task<Result> DeleteHelpdeskCreationTemplate(int templateI3D);
Task<Result> SetStandardHelpdeskCreationTemplate(int templateI3D);
```
### Frontend Components
#### 1. ViewModel
**File**: `AutoHelpdeskCreationOrderSettingsViewModel.cs`
**Properties**:
```csharp
public ObservableCollection<HelpdeskCreationTemplateDTO> Templates { get; set; }
public HelpdeskCreationTemplateDTO SelectedTemplate { get; set; }
private bool _isLoadingTemplate; // Flag to prevent unwanted form reloads
```
**Commands**:
- `SaveTemplateCommand`: Save current form values as a new template
- `DeleteTemplateCommand`: Delete selected template
- `SetStandardTemplateCommand`: Set selected template as standard
**Key Methods**:
- `LoadTemplates()`: Load all templates from database
- `LoadTemplateIntoForm(template)`: Populate form fields from template
- `SaveAsTemplate(name, isStandard)`: Create new template from form values
- `ClearAllFields()`: Clear all form fields when "Keine" is selected
- `SaveSettings(settings)`: Save to ApplicationSettings and update selected template
**Important Implementation Details**:
1. **Field Preservation**: Uses `_isLoadingTemplate` flag to prevent `LoadTemplateIntoForm()` or `ClearAllFields()` from being called during template operations.
2. **Template Updates on Save**: When saving settings with a template selected, automatically updates the template with current form values.
3. **Template Selection Persistence**:
- On load: Retrieves saved template ID from ApplicationSettings and selects it
- On save: Persists currently selected template ID
#### 2. View (XAML)
**File**: `AutoHelpdeskCreationOrderSettingsView.xaml`
**Template Section**:
```xml
<dxlc:LayoutGroup Header="Vorlagen" View="GroupBox" Orientation="Vertical">
<dxlc:LayoutItem Label="Vorlage: ">
<dxe:ComboBoxEdit ItemsSource="{Binding Templates}"
SelectedItem="{Binding SelectedTemplate}"
DisplayMember="Name"
NullText="(Keine Vorlage ausgewählt)">
<dxe:ComboBoxEdit.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<!-- Checkmark icon for standard template -->
<Image Visibility="{Binding IsStandard, Converter={...}}"/>
<!-- Template name (bold if standard) -->
<TextBlock Text="{Binding Name}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsStandard}" Value="True">
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
</dxe:ComboBoxEdit.ItemTemplate>
</dxe:ComboBoxEdit>
</dxlc:LayoutItem>
<!-- Action buttons -->
<dxlc:LayoutItem>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<dx:SimpleButton Content="Als Vorlage speichern"
Command="{Binding SaveTemplateCommand}"/>
<dx:SimpleButton Content="Als Standard setzen"
Command="{Binding SetStandardTemplateCommand}"
IsEnabled="{Binding SelectedTemplate, ...}"/>
<dx:SimpleButton Content="Vorlage löschen"
Command="{Binding DeleteTemplateCommand}"
IsEnabled="{Binding SelectedTemplate, ...}"/>
</StackPanel>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
```
**New Fields Section**:
```xml
<dxlc:LayoutGroup Header="Aktionen einstellung" View="GroupBox">
<!-- CreateTicketForAll checkbox -->
<dxe:CheckEdit Content="Für jeden Artikel ein Ticket erstellen"
EditValue="{Binding CreateTicketForAll}"/>
<!-- SendEmailToProcessor checkbox -->
<dxe:CheckEdit Content="E-Mail an Bearbeiter"
EditValue="{Binding SendEmailToProcessor}"/>
<!-- OpenAfterwards checkbox -->
<dxe:CheckEdit Content="Tickets nach Erstellung öffnen"
EditValue="{Binding OpenAfterwards}"/>
<!-- CreateSeparateTicketsMode icon selection -->
<dxlc:LayoutItem Label="Ticket einzeln erstellen: ">
<StackPanel Orientation="Horizontal">
<!-- Yes/Single option -->
<dxe:CheckEdit IsChecked="{Binding CreateSeparateTicketsMode,
Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static createTickets:CreateTicketsMode.Single}}">
<dxe:CheckEdit.Content>
<StackPanel Orientation="Vertical">
<Image Source="{dx:DXImageOffice2013 Image=Apply_32x32.png}"/>
<TextBlock Text="Ja"/>
</StackPanel>
</dxe:CheckEdit.Content>
</dxe:CheckEdit>
<!-- No/Group option -->
<dxe:CheckEdit IsChecked="{Binding CreateSeparateTicketsMode,
Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static createTickets:CreateTicketsMode.Group}}">
<dxe:CheckEdit.Content>
<StackPanel Orientation="Vertical">
<Image Source="{dx:DXImageOffice2013 Image=Cancel_32x32.png}"/>
<TextBlock Text="Nein"/>
</StackPanel>
</dxe:CheckEdit.Content>
</dxe:CheckEdit>
<!-- Custom option -->
<dxe:CheckEdit IsChecked="{Binding CreateSeparateTicketsMode,
Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static createTickets:CreateTicketsMode.Custom}}">
<dxe:CheckEdit.Content>
<StackPanel Orientation="Vertical">
<Image Source="{dx:DXImageOffice2013 Image=Properties_32x32.png}"/>
<TextBlock Text="Eigen"/>
</StackPanel>
</dxe:CheckEdit.Content>
</dxe:CheckEdit>
</StackPanel>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
<dxlc:LayoutGroup Header="Fallbackwerte" View="GroupBox">
<!-- Category dropdown -->
<dxlc:LayoutItem Label="Kategorie: ">
<dxe:ComboBoxEdit ItemsSource="{Binding HelpdeskCategories}"
SelectedItem="{Binding SelectedMainCategory}"
DisplayMember="Name"/>
</dxlc:LayoutItem>
<!-- Existing fields: Type, Bearbeiter, Priorität, etc. -->
</dxlc:LayoutGroup>
```
### ApplicationSettings
#### New Settings Added
| ID | Name | Type | Description |
|---|---|---|---|
| 10441 | `AutomHelpdeskCategoryI3D` | int | Main category I3D for automatic helpdesk creation |
| 10442 | `AutomHelpdeskCreateSeparateTicketsMode` | int | Create separate tickets mode (0=Single, 1=Group, 2=Custom) |
| 10443 | `AutomHelpdeskOpenAfterwards` | bool | Whether to open tickets after creation |
| 10444 | `AutomHelpdeskCreateTicketForAll` | bool | Create tickets for all positions or only those without tickets |
| 10445 | `AutomHelpdeskSelectedTemplateI3D` | int | I3D of selected template (0 = no template) |
#### Files Modified
1. **ApplicationSettingID.cs**: Added enum entries with XML documentation
2. **ApplicationSettingDefinitions.cs**: Added English descriptions
3. **ReceiptSettingsDTO.cs**: Added properties with `[DataMember]` attributes
4. **ReceiptWebServiceBL.cs**: Added load/save logic in four locations:
- Settings load list
- Settings load logic
- Settings update list
- Settings update logic
---
## Database Schema
### Table: `HelpdeskCreationTemplate`
| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| `I3D` | int | No | Primary key (IDENTITY) |
| `Name` | nvarchar(255) | No | Template name |
| `IsStandard` | bit | No | Whether this is the standard template |
| `TypeI3D` | int | Yes | Helpdesk type I3D |
| `CategoryI3D` | int | Yes | Main category I3D |
| `SubCategory1I3D` | int | Yes | Sub-category 1 I3D |
| `SubCategory2I3D` | int | Yes | Sub-category 2 I3D |
| `PriorityI3D` | int | Yes | Priority I3D |
| `StatusI3D` | int | Yes | Status I3D |
| `ProcessorI3D` | int | Yes | Processor (Bearbeiter) employee I3D |
| `ResponsiblePersonI3D` | int | Yes | Responsible person employee I3D |
| `AdditionalText1` | nvarchar(max) | Yes | Additional text 1 |
| `AdditionalText2` | nvarchar(max) | Yes | Additional text 2 |
| `SendEmailToProcessor` | bit | No | Send email to processor flag |
| `CreateTicketForAll` | bit | No | Create ticket for all positions flag |
| `CreateSeparateTicketsMode` | int | No | Separate tickets mode (0/1/2) |
| `OpenAfterwards` | bit | No | Open after creation flag |
| `OnlyInternal` | bit | No | Only internal visibility flag |
| `CreatedByI3D` | int | Yes | Created by user I3D |
| `CreatedDate` | datetime2(2) | Yes | Creation timestamp |
| `ChangedByI3D` | int | Yes | Last modified by user I3D |
| `ChangedDate` | datetime2(2) | Yes | Last modification timestamp |
| `IsDeleted` | bit | No | Soft delete flag |
| `DeletedByI3D` | int | Yes | Deleted by user I3D |
| `DeletedDate` | datetime2(2) | Yes | Deletion timestamp |
**Indexes**:
- Primary key on `I3D`
- Index on `IsStandard` (performance optimization for finding standard template)
- Index on `IsDeleted` (performance optimization for filtering active templates)
---
## Architecture
### Layered Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ WPF UI Layer │
│ AutoHelpdeskCreationOrderSettingsView (XAML) │
│ AutoHelpdeskCreationOrderSettingsViewModel (C#) │
└─────────────────────────────────────────────────────────────┘
↓
ClassContainer.Instance
↓
┌─────────────────────────────────────────────────────────────┐
│ Logic Interface Layer │
│ ITicketLogic (Interface) │
│ ├─ BLTicketLogic (SQL Connection) │
│ └─ WSTicketLogic (REST API Connection) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Web Service Business Logic │
│ HelpdeskCreationTemplateWebServiceBL │
│ - DTO ↔ Entity conversion │
│ - Delegates to BL │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Business Logic Layer │
│ HelpdeskCreationTemplateBL │
│ - Business rules enforcement │
│ - Data validation │
│ - Standard template management │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Data Access Layer │
│ DAOSession / NHibernate │
│ - HelpdeskCreationTemplateMaps (FluentNHibernate) │
│ - HelpdeskCreationTemplate (Entity) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Database │
│ SQL Server 2019+ │
│ - HelpdeskCreationTemplate table │
└─────────────────────────────────────────────────────────────┘
```
### Design Patterns Used
1. **MVVM (Model-View-ViewModel)**: Separation of UI and business logic
2. **Result Pattern**: `Result<T>` for error handling
3. **Repository Pattern**: DAO layer with NHibernate
4. **DTO Pattern**: Data transfer between layers
5. **Command Pattern**: DelegateCommand for UI actions
6. **Soft Delete Pattern**: `IsDeleted` flag instead of physical deletion
7. **Singleton Pattern**: ClassContainer for dependency injection
8. **Observer Pattern**: INotifyPropertyChanged for data binding
---
## Usage Guide
### Creating a New Template
1. Fill in all desired values in the form
2. Click **"Als Vorlage speichern"**
3. Enter a template name
4. Choose whether to set it as standard
5. Click OK
6. Template is saved and automatically selected
### Loading a Template
1. Select template from dropdown
2. All form fields populate with template values
3. Modify fields as needed
4. Click **"Speichern"** to save changes (updates both ApplicationSettings AND the template)
### Setting a Template as Standard
1. Select template from dropdown
2. Click **"Als Standard setzen"**
3. Template is marked as standard (only one template can be standard)
4. Standard template displays with checkmark icon and bold text
### Deleting a Template
1. Select template from dropdown
2. Click **"Vorlage löschen"**
3. Confirm deletion
4. Template is soft-deleted (marked as `IsDeleted = true`)
5. **Note**: Cannot delete the standard template
### Updating a Template
1. Select template from dropdown
2. Modify form fields
3. Click **"Speichern"** (main save button)
4. Template is automatically updated with new values
### Clearing Fields
1. Select **"Keine"** from the dropdown
2. All form fields are cleared
3. Manually enter values
4. Click **"Speichern"** to save only to ApplicationSettings (no template)
---
## Configuration
### Dialog Settings
All dialogs use the codebase standard pattern:
```csharp
// Input dialog
var result = await CentronApplication.Instance.DialogManager.ShowInputDialog(
title,
new InputDialogOptions { ... }
);
// Confirmation dialog
await CentronApplication.Instance.DialogManager.ShowConfirmationDialog(
message,
title
);
// Choice dialog
var choice = await CentronApplication.Instance.DialogManager.ShowDialog(
message,
title,
"Button1",
"Button2"
);
```
### Enum Values
#### CreateTicketsMode
```csharp
public enum CreateTicketsMode
{
Single = 0, // Create separate ticket for each position
Group = 1, // Create one ticket for all positions
Custom = 2 // User selects which positions get tickets
}
```
---
## Testing Scenarios
### Test Case 1: First Time User
- **Given**: User opens settings for the first time
- **When**: UI loads
- **Then**: No template selected, fields show ApplicationSettings values
### Test Case 2: Saved Template Exists
- **Given**: User previously selected template ID 5
- **When**: UI loads
- **Then**: Template ID 5 is selected in dropdown, fields show ApplicationSettings values
### Test Case 3: Saved Template Deleted
- **Given**: User previously selected template ID 5, but it was deleted
- **When**: UI loads
- **Then**: Dropdown shows "Keine", fields show ApplicationSettings values
### Test Case 4: Create Template
- **Given**: User fills form with values
- **When**: User clicks "Als Vorlage speichern" and enters name
- **Then**: New template created, automatically selected, fields unchanged
### Test Case 5: Update Template
- **Given**: User has template selected, modifies fields
- **When**: User clicks "Speichern"
- **Then**: ApplicationSettings saved, template updated, fields unchanged, template stays selected
### Test Case 6: Delete Template
- **Given**: User has template selected
- **When**: User clicks "Vorlage löschen" and confirms
- **Then**: Template soft-deleted, dropdown shows "Keine", fields unchanged
### Test Case 7: Set Standard
- **Given**: User has template selected (not standard)
- **When**: User clicks "Als Standard setzen"
- **Then**: Template marked as standard, displays bold with checkmark, fields unchanged
### Test Case 8: Clear Fields
- **Given**: User has template selected with values
- **When**: User selects "Keine" from dropdown
- **Then**: All fields cleared, ready for manual entry
---
## Known Limitations
1. **Standard Template Deletion**: Cannot delete a template marked as standard (must set another template as standard first)
2. **Single Standard**: Only one template can be marked as standard at a time
3. **Soft Delete**: Deleted templates remain in database with `IsDeleted = true`
---
## Future Enhancements
Potential improvements for future versions:
1. **Template Export/Import**: Export templates to file for sharing between systems
2. **Template Versioning**: Track template changes over time
3. **User-Specific Templates**: Allow users to have private templates
4. **Template Categories**: Organize templates into categories
5. **Template Permissions**: Control who can create/edit/delete templates
6. **Bulk Template Operations**: Apply template to multiple orders at once
7. **Template Statistics**: Track how often each template is used
---
## Migration Notes
### Upgrading from Previous Version
1. **Database**: ScriptMethod11751 runs automatically during update
2. **ApplicationSettings**: New settings are auto-created on first access
3. **Existing Data**: All existing automatic ticket creation settings remain intact
4. **Templates**: Start with empty template list (users create as needed)
### Rollback Considerations
If rollback is needed:
- ApplicationSettings remain (no data loss)
- HelpdeskCreationTemplate table can be safely dropped
- No impact on existing ticket creation functionality
---
## Support and Troubleshooting
### Common Issues
**Issue**: Template name disappears after saving
**Solution**: Fixed - template now updates in-place without losing reference
**Issue**: Fields clear when setting template as standard
**Solution**: Fixed - `_isLoadingTemplate` flag prevents unwanted clearing
**Issue**: Cannot update template
**Solution**: Fixed - main "Speichern" button now updates selected template
**Issue**: Template not loading on UI open
**Solution**: Verify ApplicationSettingID 10445 contains valid template I3D
### Debug Tips
1. Check `HelpdeskCreationTemplate` table for template data
2. Verify `ApplicationSettings` table for setting ID 10445
3. Check `_isLoadingTemplate` flag state during operations
4. Verify `Templates` ObservableCollection contains templates
5. Check `SelectedTemplate` property value
---
## Code Review Checklist
- ✅ Result<T> pattern used consistently
- ✅ Soft delete pattern implemented
- ✅ Standard tracking columns present
- ✅ UTF-8 with BOM encoding for C# files
- ✅ German localization for UI text
- ✅ English descriptions for ApplicationSettings
- ✅ NHibernate mappings correct
- ✅ Business logic in BL layer (not UI)
- ✅ DTO conversion in WebServiceBL
- ✅ Dialog patterns follow codebase standards
- ✅ MVVM pattern properly implemented
- ✅ Commands use DelegateCommand
- ✅ ObservableCollection for data binding
- ✅ Proper disposal of ClassContainer instances
- ✅ Async/await used correctly
- ✅ Error handling with Result pattern
- ✅ No hardcoded strings (use LocalizedStrings)
---
## Related Documentation
- [ApplicationSettings Pattern](../patterns/application-settings.md)
- [Dialog Manager Usage](../patterns/dialog-manager.md)
- [MVVM Guidelines](../patterns/mvvm-guidelines.md)
- [Database Script Creation](../database/script-creation.md)
- [NHibernate Best Practices](../data-access/nhibernate.md)
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2026-02-12 | Initial implementation with full template system |
---
## Contributors
- **Developer**: Implementation of template system and UI
- **Code Review**: Verified patterns and architecture
- **Testing**: User acceptance testing
---
**End of Documentation**
@@ -0,0 +1,429 @@
# Exchange Sync – QS-Protokoll
> **Erstellt:** 2026-03-10
> **Abgeschlossen:** 2026-03-11
> **Status:** ✅ Vollständig implementiert
> **Bearbeiter:** Entwicklung (VÖD)
> **Branch:** `vöd/Exchange_Sync`
---
## Geänderte Dateien
| Datei | Änderungsart |
|-------|-------------|
| `src/backend/Centron.BL/Sales/Calendar/ScheduleBL.cs` | Bugfixes + Refactoring |
| `src/nexus/CentronNexus/ServiceBoard/Scheduler/SchedulerPage.razor` | Frontend-Fix Wochenanzeige |
| `src/centron/Centron.WPF.UI/Modules/MyCentron/CentronInspectors/Inspectors/Schedule/OrphanedSeriesScheduleInspector.cs` | Neu – Inspector für verwaiste Serientermine |
| `src/centron/Centron.WPF.UI/Modules/MyCentron/CentronInspectors/Inspectors/InspectorManager.cs` | Inspector registriert |
| `src/centron/Centron.WPF.UI/Modules/MyCentron/CentronInspectors/Inspectors/Schedule/TimeChangedByGraphInspector.cs` | Namespace-Umbenennung (Time → Schedule) |
| `src/centron/Centron.WPF.UI/Modules/MyCentron/CentronInspectors/Inspectors/Schedule/DuplicateMailEntryIdInspector.cs` | Namespace-Umbenennung (Time → Schedule) |
---
## Übersicht
| # | Ticket | Problem | Schweregrad | Status |
|---|--------|---------|-------------|--------|
| 1 | [164020](#ticket-164020) | CenSU überschreibt fremde Outlook-Termine | 🔴 Kritisch | ✅ Implementiert |
| 2 | [163184](#ticket-163184) | Doppelter Inhalt in Exchange-Termineinladung | 🔴 Kritisch | ✅ Implementiert |
| 3 | [164121](#ticket-164121) | Gelöschte Serientermin-Instanzen bleiben in Nexus | 🟠 Hoch | ✅ Implementiert |
| 4 | [160145](#ticket-160145) | Ganztages-Events um +1 Tag verschoben | 🟠 Hoch | ✅ Implementiert |
| 5 | [158813](#ticket-158813) | Geplante Zeiten mit falschem Datum nach Sync | 🟠 Hoch | ✅ Implementiert |
| 6 | [157036](#ticket-157036) | Fehlerhafte Wochenanzeige in Nexus | 🟡 Mittel | ✅ Implementiert |
| 7 | [162826](#ticket-162826) | Outlook-Erinnerungsflut bei synchronisierten Zeiten | 🟡 Mittel | ✅ Implementiert |
| 8 | [156991](#ticket-156991) | Doppelte Feiertage / falsches Datum in Nexus | 🟡 Mittel | ✅ Implementiert |
| + | [Zusatz](#zusatz-serientermin-kaskade) | Gelöschter Serienmaster löscht Instanzen nicht | 🟠 Hoch | ✅ Implementiert |
| + | [Inspector](#zusatz-inspector) | Manueller Cleanup verwaister Serientermine | — | ✅ Implementiert |
---
## Ticket 164020
**Betreff:** CenSU überschreibt fremde Outlook-Termine mit alten Zeitdaten
### Root Cause
`SyncOldSchedule()` (Nexus → Outlook) hatte keinen Filter auf den Termintyp. Auch Helpdesk-Zeiterfassungs-Schedules (`ObjectType = HelpdeskTimerClass / HelpdeskClass`) wurden über diesen Pfad nach Exchange geschrieben – obwohl diese bereits über `CreateOrUpdateTimeSchedule()` verwaltet werden. Parallel lief `UpdateScheduleByGraph()` (Outlook → Nexus) und schrieb Outlook-Daten in Helpdesk-Schedules zurück (Datum/Uhrzeit), was zu einem Teufelskreis führte.
### Implementierter Fix
**`SyncOldSchedule()` – Helpdesktypen ausschließen:**
```csharp
baseExpression = baseExpression.And(x =>
x.ObjectType != (int)CentronObjectKindNumeric.HelpdeskTimerClass &&
x.ObjectType != (int)CentronObjectKindNumeric.HelpdeskClass);
```
**`UpdateScheduleByGraph()` – Datum-Schutz für Helpdesk-Schedules:**
```csharp
if (!this.IsHelpdeskSchedule(schedule))
{
// Datum nur für reine Outlook-Termine zurückschreiben
schedule.DateStart = this.GetDateTimeFromGraphDateTime(graphEvent.Start, localTimeZone);
schedule.DateEnd = this.GetDateTimeFromGraphDateTime(graphEvent.End, localTimeZone);
}
```
**Neue Hilfsmethode `IsHelpdeskSchedule()`:**
```csharp
private bool IsHelpdeskSchedule(Schedule schedule)
=> schedule.ObjectType == (int)CentronObjectKindNumeric.HelpdeskTimerClass
|| schedule.ObjectType == (int)CentronObjectKindNumeric.HelpdeskClass;
```
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. Helpdeskzeit in Nexus anlegen (z.B. 11.03.2026, 09:00–10:00) | Zeit wird angelegt |
| 2. Webservice-Sync abwarten (60s) | Zeit erscheint als Termin in Outlook mit korrektem Datum |
| 3. Termin in Outlook manuell bearbeiten (Betreff ändern) | — |
| 4. Webservice-Sync abwarten | **Datum der Nexus-Zeit bleibt 11.03.2026, 09:00–10:00** (kein Überschreiben durch Outlook) |
| 5. Techniker legt weiteren eigenen Outlook-Termin an | Exchange-Termin bleibt unverändert – CenSU schreibt nicht in ihn |
---
## Ticket 163184
**Betreff:** Doppelter Inhalt in Exchange-Termineinladung (Body enthält Text 3× und altes Datum)
### Root Cause
In `CreateOrUpdateTimeSchedule()` wurde `schedule.Caption = timer.ExternalNote` gesetzt, bevor `ReplaceVariables(body, schedule)` aufgerufen wurde. Das Body-Template enthält `@@TerminText@@`, welches intern `schedule.Caption` liest. Dadurch wurde `ExternalNote` doppelt in den Body eingebettet: einmal über `@@TerminText@@` und einmal direkt durch das Template `@@WorkOrTime@@`.
### Implementierter Fix
```csharp
// Subject-Auflösung: Caption temporär setzen damit @@TerminText@@ im Subject funktioniert
schedule.Caption = timer.ExternalNote;
schedule.Subject = this.ReplaceVariables(subject, schedule);
// Body-Auflösung: Caption leeren damit @@TerminText@@ im Body leer ist
schedule.Caption = string.Empty;
schedule.Caption = this.ReplaceVariables(body, schedule);
```
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. Helpdeskzeit mit externem Hinweis anlegen (z.B. "Vor-Ort-Einsatz Drucker") | Zeit wird gespeichert |
| 2. Sync abwarten | Outlook-Termin wird erstellt |
| 3. Outlook-Termin öffnen, Body prüfen | Body enthält "Vor-Ort-Einsatz Drucker" **exakt einmal** |
| 4. Nochmals Zeit speichern (Update) | Termin wird aktualisiert, Body enthält Text weiterhin nur einmal |
---
## Ticket 164121
**Betreff:** Gelöschte Serientermin-Instanzen bleiben in Nexus sichtbar
### Root Cause
Microsoft Graph sendet bei gelöschten Serientermin-Instanzen kein `@removed`-Flag, sondern setzt `isCancelled = true`. Der bisherige Code prüfte nur `@removed` → gelöschte Instanzen wurden nie in Nexus als inaktiv markiert.
### Implementierter Fix
**`StoreEvent()` – `isCancelled` gleichwertig zu `@removed` behandeln:**
```csharp
bool isRemoved = graphEvent.AdditionalData.ContainsKey("@removed")
|| graphEvent.IsCancelled is true;
```
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. In Outlook einen Serientermin anlegen (z.B. täglich, 5 Instanzen) | Alle 5 Instanzen erscheinen in Nexus nach Sync |
| 2. Eine einzelne Instanz in Outlook löschen ("Nur diesen Termin") | — |
| 3. Sync abwarten | **Die gelöschte Instanz verschwindet aus Nexus** |
| 4. Verbleibende Instanzen prüfen | Übrige 4 Instanzen bleiben unverändert sichtbar |
---
## Ticket 160145
**Betreff:** Ganztages-Events in Nexus um +1 Tag verschoben
### Root Cause
Microsoft Graph liefert Ganztages-Events mit `Start.TimeZone = "UTC"` und einem exklusiven Enddatum (`End = nächster Tag 00:00 UTC`). Die bisherige UTC→CET-Konvertierung verschob das Datum: `2025-08-16T00:00 UTC` → `2025-08-16T02:00 CEST`. Nexus interpretierte den 16.08 als Enddatum → Termin erschien auf dem falschen Tag.
### Implementierter Fix
**`InsertScheduleByGraph()` und `UpdateScheduleByGraph()` – neue `ParseAllDayEventDates()`-Hilfsmethode:**
```csharp
if (graphEvent.IsAllDay == true)
(schedule.DateStart, schedule.DateEnd) = ParseAllDayEventDates(graphEvent);
```
```csharp
private static (DateTime Start, DateTime End) ParseAllDayEventDates(Event graphEvent)
{
var start = DateTime.Parse(graphEvent.Start.DateTime).Date;
var end = DateTime.Parse(graphEvent.End.DateTime).Date.AddDays(-1); // exklusiv → inklusiv
return (start, end);
}
```
**Zusätzlich: IANA → Windows Timezone-Mapping in `GetDateTimeFromGraphDateTime()`:**
```csharp
// "Europe/Berlin" wird korrekt auf "W. Europe Standard Time" gemappt
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(graphDateTime.TimeZone, out var windowsId))
graphTimeZone = TimeZoneInfo.FindSystemTimeZoneById(windowsId);
```
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. In Outlook einen ganztägigen Termin am **15.08.2026** anlegen | Termin in Outlook korrekt am 15.08 |
| 2. Sync abwarten | **Termin in Nexus erscheint am 15.08.2026** (nicht 16.08) |
| 3. Termin in Outlook auf **10.–12.08.2026** (3 Tage) ändern | — |
| 4. Sync abwarten | Nexus zeigt Termin vom **10.08. bis 12.08.** |
---
## Ticket 158813
**Betreff:** Geplante Zeiten erhalten nach Sync falsches Datum
### Root Cause
`UpdateScheduleByGraph()` überschrieb `DateStart`/`DateEnd` von Helpdesk-Schedules mit den aus Exchange zurückgegeben Werten (Round-Trip-Problem). Zusätzlich: IANA-Zeitzonennamen (`"Europe/Berlin"`) wurden nicht als Windows-Zeitzone erkannt → unnötige Konvertierung, mögliche 1h-Abweichung je Sommerzeit.
### Implementierter Fix
Beide Ursachen behoben:
1. **Helpdesk-Datum-Schutz** (identisch zu Ticket 164020) – `UpdateScheduleByGraph()` überschreibt Datum bei Helpdesk-Schedules nicht mehr.
2. **Timezone-Mapping** (identisch zu Ticket 160145) – IANA → Windows-ID, `HasSameRules()`-Prüfung verhindert unnötige Konvertierungen bei gleicher Zeitzone.
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. In Nexus eine Planung anlegen: **02.03.2026, 08:00–09:00** | Nexus zeigt 02.03.2026 |
| 2. Sync abwarten (Nexus → Outlook) | Outlook zeigt Termin am 02.03.2026 korrekt |
| 3. Erneuten Sync abwarten (Outlook → Nexus, Delta-Rückweg) | **Nexus zeigt weiterhin 02.03.2026** (kein Überschreiben) |
| 4. Prüfen ob Uhrzeit korrekt bleibt | 08:00–09:00 unverändert |
---
## Ticket 157036
**Betreff:** Fehlerhafte Wochenanzeige in Nexus (Arbeitswoche zeigt nur Freitag)
### Root Cause
Der `DxSchedulerWorkWeekView` in `SchedulerPage.razor` hatte kein `WorkDays`-Attribut. Ohne diese Angabe verwendet DevExpress einen internen Default, der in dieser Version nur den letzten Werktag (Freitag) anzeigte.
### Implementierter Fix
**`SchedulerPage.razor` – `WorkDays`-Attribut ergänzt:**
```razor
<DxSchedulerWorkWeekView VisibleTime="VisibleTime"
WorkTime="_workTime"
TimeScale="WeekViewTimeScale"
WorkDays="DayOfWeek.Monday | DayOfWeek.Tuesday | DayOfWeek.Wednesday | DayOfWeek.Thursday | DayOfWeek.Friday">
```
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. Nexus öffnen → Zeitplanung → Ansicht "Arbeitswoche" wählen | **Alle 5 Werktage (Mo–Fr) werden angezeigt** |
| 2. Termin am Mittwoch prüfen | Termin erscheint korrekt auf Mittwoch |
| 3. Ansicht wechseln (Woche, Monat) und zurück zu Arbeitswoche | Arbeitswoche zeigt weiterhin Mo–Fr |
---
## Ticket 162826
**Betreff:** Outlook-Erinnerungsflut bei synchronisierten Helpdeskzeiten
### Root Cause
`UpdateTerminplanungPersonTable()` → `AddScheduleToExchange()` setzte `isReminderOn` nicht explizit auf `false` für Helpdesk-Zeiten. Outlook aktiviert standardmäßig eine 15-Minuten-Erinnerung für alle neuen Termine.
### Implementierter Fix
**`UpdateTerminplanungPersonTable()` – Reminder für Helpdesk-Schedules deaktivieren:**
```csharp
bool? reminderOn = this.IsHelpdeskSchedule(schedule) ? false : (bool?)null;
var result = await this.AddScheduleToExchange(schedule, schedulePerson, isReminderOn: reminderOn);
```
`null` bedeutet: Reminder-Einstellung nicht ändern (Standard-Outlook-Verhalten für normale Termine).
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. Helpdeskzeit speichern → Sync abwarten | Termin in Outlook erscheint |
| 2. Outlook-Termin öffnen → Eigenschaften prüfen | **Feld "Erinnerung" ist deaktiviert** (kein Häkchen) |
| 3. Normalen Nexus-Kalendertermin anlegen → Sync | Erinnerung bleibt auf Standard (15 Min.) |
---
## Ticket 156991
**Betreff:** Doppelte Feiertage / falsches Datum in Nexus
### Root Cause
**A) Datumsversatz:** Identisch zu Ticket 160145 (UTC-Konvertierungsfehler bei `IsAllDay = true`).
**B) Duplikate:** Wenn derselbe Feiertag in mehreren Exchange-Kalendern (z.B. persönlicher Kalender + Firmenkalender) enthalten ist, liefert Graph Delta denselben Feiertag mit unterschiedlichen `MailEntryID`s. Der bisherige Code prüfte nur auf `MailEntryID`-Duplikate → zwei identische Schedules wurden angelegt.
### Implementierter Fix
**A)** UTC-Fix identisch zu Ticket 160145 über `ParseAllDayEventDates()`.
**B) Duplikat-Prüfung in `StoreEvent()` für Ganztages-Events:**
```csharp
if (graphEvent.IsAllDay == true && graphEvent.Subject is not null)
{
var (allDayStart, _) = ParseAllDayEventDates(graphEvent);
var existingAllDay = GetSchedulesByExpression(x =>
x.Subject == graphEvent.Subject &&
x.DateStart == allDayStart &&
x.FullDay == true &&
x.IsActive);
if (existingAllDay.Any(s => GetSchedulePersonsByFilter(
new SchedulePersonsFilter { ScheduleI3D = s.I3D })
.Any(sp => sp.PersonalI3D == employee.I3D)))
return; // Bereits vorhanden → kein Duplikat anlegen
}
```
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. In Outlook einen Feiertag prüfen (z.B. Ostersonntag 05.04.2026) | — |
| 2. Sync abwarten | **Ostersonntag erscheint in Nexus genau einmal am 05.04.2026** |
| 3. Weiteren ganztägigen Termin am selben Tag mit gleichem Betreff anlegen (zweiter Kalender) | — |
| 4. Sync abwarten | **Kein zweiter Eintrag** – Duplikat wird erkannt und übersprungen |
---
## Zusatz: Serientermin-Kaskade
**Problem:** Wenn ein kompletter Serienmaster in Exchange gelöscht wird, wurden die zugehörigen Instanz-Schedules in Nexus nicht mitgelöscht. Graph sendet beim Master-Delete nicht immer einzelne `@removed`-Events für alle Instanzen.
### Implementierter Fix
**`StoreEvent()` – Kaskade bei Master-Delete:**
```csharp
if (isRemoved && graphEvent.SeriesMasterId is null) // SeriesMasterId null = dieser Event IST der Master
await this.DeleteSeriesInstancesByMasterId(graphEvent.Id, employee, cenSU);
```
**Neue Methode `DeleteSeriesInstancesByMasterId()`:**
- Sucht alle `SchedulePerson`-Einträge mit `SeriesMasterId = masterEventId` für den Mitarbeiter
- Lädt nur noch aktive Schedules (`IsActive = true`)
- Setzt diese auf `IsActive = false` via `SaveSchedule()` (korrekte BL-Kette inkl. Exchange-Löschung)
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. In Outlook Serientermin anlegen (z.B. täglich, 7 Instanzen) | Alle 7 Instanzen erscheinen in Nexus |
| 2. Gesamte Serie in Outlook löschen ("Alle Termine der Serie") | — |
| 3. Sync abwarten | **Alle 7 Nexus-Instanzen werden als inaktiv markiert und verschwinden** |
---
## Zusatz: Inspector für verwaiste Serientermine
**Problem:** Historisch bereits entstandene verwaiste Serientermine (vor Einführung des Kaskaden-Fix) müssen manuell bereinigt werden können.
### Implementierter Fix
Neuer Inspector **"Verwaiste Serientermine (Exchange Sync)"** im c-entron WPF-Client unter `MyCentron → c-entron Inspektor`:
- **Check:** Findet alle aktiven Schedules mit `SeriesMasterId`, für die kein aktiver Serienmaster-Schedule mehr in der DB existiert
- **Repair:** Deaktiviert alle gefundenen verwaisten Instanzen per Knopfdruck
- **Kategorie:** Zeiten (alphabetisch sortiert)
### QS-Testfall
| Schritt | Erwartetes Ergebnis |
|---------|-------------------|
| 1. c-entron WPF öffnen → MyCentron → Inspektor | Inspector-Liste erscheint |
| 2. "Verwaiste Serientermine (Exchange Sync)" auswählen und Inspektor ausführen | Check läuft durch |
| 3a. Keine verwaisten Daten vorhanden | Grünes Häkchen: "Es wurden keine verwaisten Serientermine gefunden." |
| 3b. Verwaiste Daten vorhanden | Rotes X mit Anzahl + **Reparieren**-Button |
| 4. Reparieren klicken | Instanzen werden deaktiviert, Check läuft erneut → grünes Häkchen |
---
## Technische Grundlagen
### Sync-Architektur
```
ExchangeSyncService (HostedService, Intervall konfigurierbar, default 60s)
├── ScheduleBL.SyncByGraphV2() ← Outlook → Nexus (Graph API Delta)
└── ScheduleBL.SyncOldSchedule() ← Nexus → Outlook (Schedules ohne MailEntryID)
HelpdeskTimerBL / ScheduleBL
├── CreateOrUpdateTimeSchedule() ← Zeiterfassung → Outlook-Termin
└── UpdateTerminplanungPersonTable() ← Termin gespeichert → Exchange aktualisieren
```
### Schlüssel-Entitäten
| Entity | Beschreibung |
|--------|-------------|
| `Schedule` | Termin in c-entron (DateStart, DateEnd, FullDay, Subject, IsActive, ObjectType) |
| `SchedulePerson` | Verknüpfung Schedule ↔ Mitarbeiter ↔ Exchange (MailEntryID, SeriesMasterId) |
| `CreatedByApp` | Herkunft: `GraphSync = 6`, `HelpdeskTime`, `GraphSyncTimeDelete` |
| `CenSU` | CentronSystemUser – Systemkonto für alle Hintergrundprozesse |
| `IsHelpdeskSchedule()` | Neue Hilfsmethode: prüft ob Schedule an Helpdesk/Zeit gebunden ist |
### Source of Truth
| Schedule-Typ | Datum/Zeit Source of Truth |
|---|---|
| `HelpdeskTimerClass` / `HelpdeskClass` | **Nexus** – Exchange darf nie überschreiben |
| Reine Outlook-Termine (`GraphSync`) | **Exchange/Outlook** |
| Ganztages-Events (`FullDay = true`) | **Datumswert aus Graph, ohne Zeitzonenkonvertierung** |
---
## Bekannte Einschränkungen / Nicht umgesetzt
| Thema | Begründung |
|-------|-----------|
| Kalenderfilter (welche Exchange-Kalender synchronisiert werden) | Konfigurationsthema, kein Code-Fix – muss über Exchange-Einstellungen gesteuert werden |
| Blocker-Termine (`ShowAs = Free`) in Nexus anzeigen | Kein Kundenauftrag für diese Version |
| ApplicationSetting für Erinnerungen (An/Aus im UI) | Für Helpdesk-Zeiten ist `isReminderOn: false` als Standard ausreichend |
---
## Offene Fragen (für Rollout-Planung)
- [x] **Gibt es Kunden mit Exchange On-Premise (kein Graph API)?** → **Ja.** Diese Kunden nutzen den alten EWS-basierten Agent. Für sie greifen die Graph-spezifischen Fixes **nicht** (Delta-Token, `isCancelled`, `ParseAllDayEventDates`, Kaskaden-Delete, Duplikat-Erkennung). Separate Analyse für On-Premise-Kunden erforderlich – ggf. eigener Bugfix-Zyklus.
- [x] **Welche Exchange-Kalender werden synchronisiert – nur Primärkalender?** → **Alle Kalender.** Der Endpunkt `_graphClient.Users[id].CalendarView.Delta` liefert Events aus *allen* Kalendern des Nutzers aggregiert (Primär + abonnierte Kalender wie „Feiertage in Deutschland", Teamkalender etc.). Das erklärt u.a. den Doppel-Feiertage-Bug (Ticket 156991). So gewollt – bleibt so.
- [x] **Sollen Kunden nach dem Update den Inspector manuell ausführen (Kommunikation nötig)?** → **Ja.** In den Release Notes darauf hinweisen, dass Kunden mit Exchange Sync nach dem Update den Inspector „Verwaiste Serientermine (Exchange Sync)" einmalig ausführen sollen, um historisch entstandene verwaiste Serientermine zu bereinigen.
- [x] **Ist der Inspector nur für Admins sichtbar (Rechteprüfung)?** → Zugriff wird über das Modul „c-entron Inspektor" gesteuert – keine separate Rechteprüfung im Inspector selbst erforderlich.
## Geltungsbereich der Fixes
| Fix | Exchange Online (Graph API) | Exchange On-Premise (alter EWS-Agent) |
|-----|-----------------------------|---------------------------------------|
| Ticket 164020 – CenSU überschreibt Helpdesk-Zeiten | ✅ | ⚠️ Teilweise: `SyncOldSchedule()`-Filter greift, EWS-Rückweg unklar |
| Ticket 163184 – Doppelter Body | ✅ | ✅ (`CreateOrUpdateTimeSchedule()` ist gemeinsamer Pfad) |
| Ticket 164121 – Instanz-Delete via `isCancelled` | ✅ | ❌ EWS-Agent nutzt anderen Delete-Mechanismus |
| Ticket 160145 – Ganztages +1 Tag | ✅ | ❌ Graph-spezifisch |
| Ticket 158813 – Falsches Datum nach Sync | ✅ | ⚠️ Helpdesk-Schutz greift, Timezone-Fix ist Graph-spezifisch |
| Ticket 157036 – Wochenanzeige Nexus | ✅ | ✅ (Frontend-Fix, kein Exchange-Bezug) |
| Ticket 162826 – Erinnerungsflut | ✅ | ✅ (`AddScheduleToExchange()` gemeinsamer Pfad) |
| Ticket 156991 – Doppelte Feiertage | ✅ | ❌ Graph Delta-spezifisch |
| Zusatz – Serienkaskade | ✅ | ❌ Graph Delta-spezifisch |
| Inspector – Cleanup verwaiste Serientermine | ✅ | ✅ (DB-Level, unabhängig vom Sync-Pfad) |
@@ -0,0 +1,72 @@
# AI and developer navigation (large repository)
This document helps **find code quickly** in a very large solution. It does **not** replace [CLAUDE.md](../../CLAUDE.md) (rules and patterns) or [general-structure.md](general-structure.md) (layering and ILogic details). Use it as a **map**: where to look before running broad searches.
## How this fits other docs
| Need | Primary doc |
|------|-------------|
| Mandatory patterns, Result, dual API, DB, encoding | [CLAUDE.md](../../CLAUDE.md) |
| Layering, ClassContainer, ILogic / BL / WS | [general-structure.md](general-structure.md) |
| Adding REST methods (legacy + modern) | [../guides/services/add-webservice-methods.md](../guides/services/add-webservice-methods.md) |
| MVVM, modules, UI | [../guides/ui/](../guides/ui/) and [../reference/architecture/mvvm-in-centron.md](../reference/architecture/mvvm-in-centron.md) |
## Top-level layout
| Path | Role |
|------|------|
| `src/backend/` | `Centron.BL`, `Centron.DAO`, `Centron.Entities`, `Centron.Interfaces`, `Centron.Common`, `Centron.Gateway` |
| `src/centron/` | WPF client (`Centron.WPF.UI`, `Centron.WPF.UI.Extension`) |
| `src/webservice/` | `Centron.WebServices.Core` (legacy REST), `Centron.Controllers` (ASP.NET Core API), hosts (`Centron.Host*`) |
| `src/nexus/` | Blazor Server portal (`CentronNexus`, hosts, Outlook add-in) |
| `src/apis/` | External integration assemblies (FinAPI, GLS, Shipcloud, ITscope, Icecat, etc.) |
| `src/shared/` | `Centron.Core`, `Centron.Controls`, reusable UI |
| `tests/` | Unit, integration, E2E, API tests, Playwright, Nexus tests |
| `scripts/` | `Centron.Scripts` build orchestration |
| `deployment/` | WiX / installers |
| `.claude/` | Claude Code agents, commands, hooks (not used by Cursor rules) |
Solution file: `Centron.sln` (groups many projects; not every folder under `src/` is listed above—search the `.sln` for exact project names).
## Find code by concern
| You need | Where to look |
|----------|----------------|
| **NHibernate entity** | `src/backend/Centron.Entities/` (namespaces like `Centron.Data.Entities.*`) |
| **FluentNHibernate mapping** | `src/backend/Centron.DAO/Mappings/` (mirrors domain folders, e.g. `Mappings/Accounting/`) |
| **Business logic (BL)** | `src/backend/Centron.BL/` (`*BL.cs`, script methods under `Administration/Scripts/ScriptMethods/Scripts/`) |
| **DAO / queries** | `src/backend/Centron.DAO/` |
| **ILogic interface** | `src/centron/Centron.WPF.UI/Services/Logics/**/I*Logic.cs` |
| **BLLogic / WSLogic** | Same tree: `src/centron/Centron.WPF.UI/Services/Logics/**/BL*Logic.cs`, `WS*Logic.cs` |
| **WebServiceBL + DTO mapping** | `src/backend/Centron.BL/` (often `WebServices/` or domain folders; AutoMapper profiles in `WebServices/ObjectMapperConfiguration/`) |
| **Legacy REST contract** | `src/backend/Centron.Interfaces/` (`ICentronRestService`), implementation `src/webservice/Centron.WebServices.Core/RestService/CentronRestService.cs` |
| **Modern REST controllers** | `src/webservice/Centron.Controllers/Controllers/` (`v1/{Domain}/`, `Unversioned/`) |
| **WPF module registration** | `src/centron/Centron.WPF.UI/Modules/ModuleRegistration.cs` |
| **Application settings IDs** | `src/backend/Centron.Interfaces/Administration/Settings/ApplicationSettingID.cs`, `ApplicationSettingDefinitions.cs` |
| **User rights constants** | `src/webservice/Centron.WebServices.Core/EntitiesWrongPlace/Administration/Rights/UserRightsConst.cs` |
**Search tip:** For a feature name (e.g. `Receipt`), search the **interface** name first (`IReceipt*Logic`, `*Receipt*WebServiceBL`, `Receipt*Controller`) to narrow the tree.
## Test projects (targeted runs)
Use `dotnet test Centron.sln` for everything; for **faster feedback** narrow to a project:
| Project path | Typical use |
|--------------|-------------|
| `tests/backend/Centron.Tests.BL` | Business logic unit tests |
| `tests/backend/Centron.Tests.DAO` | DAO / persistence tests |
| `tests/shared/Centron.Tests.Core` | Shared test utilities |
| `tests/Centron.Tests.Integration` | Integration |
| `tests/Centron.Tests.EndToEnd` | E2E |
| `tests/apis/*` | External API wrapper tests |
| `tests/CentronNexusTests` | Blazor portal |
| `tests/PlaywrightTests` | Browser E2E |
Example: `dotnet test tests/backend/Centron.Tests.BL/Centron.Tests.BL.csproj`
## Claude Code vs Cursor
- **`.claude/`** — agents and commands for Claude Code.
- **`.cursor/rules/`** — Cursor project rules; still follow **CLAUDE.md** as the canonical playbook.
Keeping behavioral rules in **one place** ([CLAUDE.md](../../CLAUDE.md)) avoids drift; this file only improves **orientation** in the tree.
@@ -0,0 +1,138 @@
# Documentation Organization and Maintenance
This guide explains how the documentation in the c-entron.NET project is organized and what steps to follow when adding new documentation files or directories.
## Documentation Structure
The project documentation uses a hierarchical directory structure to organize content by topic and purpose:
```
docs/
├── .order # Controls documentation ordering in some documentation viewers
├── README.md # Main navigation and documentation entry point
├── getting-started/ # Beginner guides and introductory material
├── guides/ # Step-by-step instruction guides
│ ├── development/ # Guides for development tasks
│ ├── database/ # Database-related guides
│ ├── ui/ # UI development guides
│ └── services/ # Web services guides
├── reference/ # Reference documentation
│ ├── architecture/ # Architecture specifications
│ ├── database/ # Database reference documentation
│ └── security/ # Security documentation
└── operations/ # Operational procedures and guides
```
### Directory Organization Rules
1. Use **kebab-case** for all documentation file and directory names (lowercase with hyphens)
2. Organize documentation into logical sections based on their purpose and audience
3. Keep documentation files focused on a single topic or task
4. Group related documentation files in appropriate subdirectories
## Adding New Documentation
### Creating a New Documentation File
When adding a new documentation file, follow these steps:
1. **Choose the right location** within the existing structure
2. **Name the file appropriately** using kebab-case (e.g., `how-to-configure-settings.md`)
3. **Create the file** with appropriate content, using Markdown format
4. **Update the README.md navigation file**:
- Open `docs/README.md` in your preferred editor
- Find the appropriate section corresponding to the directory where you added the file
- Add a new entry with a link to your documentation file following the existing pattern
- This step is **mandatory** to ensure discoverability of your documentation
5. **Update the Solution File**:
- Open `Centron.sln` in your preferred editor
- Find the appropriate solution folder section for the directory where you added the file
- Add a reference to your new file in the ProjectSection(SolutionItems)
Example of adding a file reference to the solution:
```
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "getting-started", "getting-started", "{01F71AAA-B574-490A-A241-E905C4E4F358}"
ProjectSection(SolutionItems) = preProject
docs\getting-started\general-structure.md = docs\getting-started\general-structure.md
docs\getting-started\landing-page.md = docs\getting-started\landing-page.md
docs\getting-started\your-new-file.md = docs\getting-started\your-new-file.md
EndProjectSection
EndProject
```
### Creating a New Documentation Directory
When adding a new documentation directory, follow these steps:
1. **Create the physical directory** in the appropriate location
2. **Create any initial documentation files** within the directory
3. **Update the Solution File**:
- Create a new solution folder entry for your directory
- Add references to any files in the directory
- Set up the proper nesting relationship in the NestedProjects section
#### Step 1: Add Solution Folder Entry
Add a new solution folder entry before the `EndGlobal` section:
```
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "your-new-folder", "your-new-folder", "{GENERATE-NEW-GUID}"
ProjectSection(SolutionItems) = preProject
docs\path\to\your-new-folder\file1.md = docs\path\to\your-new-folder\file1.md
EndProjectSection
EndProject
```
**Note:** Generate a new GUID for each new solution folder. You can use tools like Visual Studio's GUID generator or online GUID generators.
#### Step 2: Define the Nesting Relationship
In the `GlobalSection(NestedProjects)` section, add an entry that defines where your new folder belongs in the hierarchy:
```
{GUID-OF-NEW-FOLDER} = {GUID-OF-PARENT-FOLDER}
```
Example:
```
{A1B2C3D4-E5F6-1234-5678-ABCDEF123456} = {2F71ED3A-FF09-4148-BEBA-FE06257EFCE4}
```
## Documentation File Format
1. Use **UTF-8 with BOM encoding** for all documentation files
2. Start with a # heading that clearly describes the content
3. Use proper Markdown formatting for headings, lists, code blocks, etc.
4. Include links to related documentation when appropriate
5. For internal links, use relative paths to other documentation files
## Example: Adding a New Guide
Let's say you want to add a new guide for configuring database connections:
1. Determine the proper location: `docs/guides/database/configure-database-connection.md`
2. Create the file with appropriate content
3. Update `Centron.sln`:
- Find the "database" solution folder under "guides" (GUID: `6DB9540C-9A72-4494-81C3-254BC21214BF`)
- Add your file to its ProjectSection
- Ensure the path is correct relative to the solution file
## Updating Existing Documentation
When updating existing documentation:
1. Maintain the same file encoding (UTF-8 with BOM)
2. Follow the established formatting patterns
3. Check and update any internal references if needed
4. The solution file does not need to be updated if only modifying an existing file
## Best Practices
1. **Keep documentation up-to-date** when the code changes
2. **Use clear, concise language** that is easy for readers to understand
3. **Include examples** where appropriate to illustrate concepts
4. **Use screenshots** for UI-related documentation
5. **Link to external resources** when they provide valuable additional information
Following these guidelines will help maintain a well-organized, easily navigable documentation structure that enhances developer productivity and understanding of the c-entron.NET project.
@@ -0,0 +1,166 @@
# General structure of our c-entron<span>.NET for developers
*Lets get this out of the way first: there are tons of places where this general structure does not apply, places that use more or less layers, places that use the wrong type of object and all other sorts of horrific code. You're welcome to fix this wherever you think it needs to be fixed, but atleast all new code should follow this structure.*
layer|objectype|description
--|--|--
*UI*||*the UI with which the horrifiyng User interacts*
ViewModel|DTO/ViewModel|converting DTOs to ViewModels so they can be interacted with via Bindings or similar
ILogic/BLLogic/WSLogic|DTO|clientside interaction with the DB (BLLogic) or a remote webservice (WSLogic)
ICentronRestService/CentronRestService|DTO|the actual webservice methods that can be called from other apps
WebServiceBL|Entity/DTO|converting entities to DTOs
BL|Entity|interaction with [NHibernate](https://nhibernate.info/) and the database itself
*database*||*the horrifiyng land of the database*
## Client-Side Data Access (WPF UI)
The c-entron.NET WPF client uses a sophisticated data access pattern that supports both direct database access and web service communication through a unified interface system. If the naming guidelines are followed, the client will automatically register the ILogic with the corresponding BLLogic and WSLogic.
### ClassContainer and ILogic Pattern
The client accesses data through the `ClassContainer` singleton using the `ILogic` interface pattern:
```csharp
var result = await ClassContainer
.Instance
.WithInstance((IAccountContractsLogic logic) => logic.GetAccountContracts(filter))
.ThrowIfError();
```
This pattern provides:
- **Dependency injection** through ClassContainer
- **Unified interface** for data access
- **Error handling** with Result<T> pattern
- **Async/await support** for all operations
### Dual Implementation Architecture
**Every module MUST implement both data access methods:**
#### 1. ILogic Interface
Defines the contract for data operations:
```csharp
public interface IAccountContractsLogic
{
Task<Result<IList<AccountContractDTO>>> GetAccountContracts(GetAccountContractsFilter filter);
Task<Result<AccountContractDTO>> SaveAccountContract(AccountContractDTO accountContract);
// ... other methods
}
```
#### 2. BL Implementation (Direct Database Access)
```csharp
public class BLAccountContractsLogic : IAccountContractsLogic
{
private readonly ConnectionInfo _connectionInfo;
public Task<Result<IList<AccountContractDTO>>> GetAccountContracts(GetAccountContractsFilter filter)
{
return Task.Run(() =>
{
using (var session = new BLSession())
{
return session.GetBL<AccountContractWebServiceBL>()
.GetAccountContracts(this._connectionInfo.GetLoggedInUser(), filter);
}
});
}
}
```
#### 3. WS Implementation (Web Service Access)
```csharp
public class WSAccountContractsLogic : IAccountContractsLogic
{
private readonly ICentronWebServiceConnection _connection;
public Task<Result<IList<AccountContractDTO>>> GetAccountContracts(GetAccountContractsFilter filter)
{
return this._connection.CallWebServiceMethodWithListResultAsync(f =>
f.GetAccountContracts(this._connection.GetRequest(filter)));
}
}
```
### Connection Type Support
Modules declare supported connection types in their `AppModuleController`:
```csharp
public CentronConnectionType[] SupportsConnectionTypes => new[]
{
CentronConnectionType.CentronWebServices, // Uses WSLogic implementation
CentronConnectionType.SqlServer // Uses BLLogic implementation
};
```
### Benefits of This Architecture
- **Flexibility**: Same module works with direct database or web service
- **Testability**: Easy to mock ILogic interfaces for unit testing
- **Consistency**: Unified error handling and async patterns
- **Maintainability**: Clear separation of concerns
- **Scalability**: Can switch between local and remote data access
### Implementation Guidelines
1. **Always create the ILogic interface first** defining all required operations
2. **Implement both BL and WS classes** - this is mandatory for all modules
3. **Use consistent naming**: `I{Module}Logic`, `BL{Module}Logic`, `WS{Module}Logic`
4. **Return `Result<T>`** from all logic methods for consistent error handling
5. **Support async operations** using `Task<Result<T>>` return types
6. **Register in ClassContainer** to enable dependency injection
## Localization and UI Language Requirements
Because c-entron.NET is developed specifically for the German market, all user-facing content must adhere to the following guidelines:
### German-First Language Policy
- **All UI labels** must be written in German
- **All user messages** must be written in German
- **All documentation visible to end users** must be in German
- **Error messages** displayed to users must be in German
### Language Requirements
- **All documentation visible to end users** must be in German
- **Error messages** displayed to users must be in German
- **Multi-language Support**: The application supports both German (default) and English through separate resource files
- German text is stored in base resource files (`LocalizedStrings.resx`)
- English translations are stored in language-specific resource files (`LocalizedStrings.en.resx`)
- When adding new localized strings, provide translations for both languages
### Implementation Guidelines
For detailed information on implementing localization in the WPF client, including XAML usage, code-behind usage, and business logic integration, see the [Localization Guide](../guides/ui/localization.md).
- Use German terminology consistent with the business domain
- Maintain consistent capitalization and formatting according to German language rules
- For technical terms without direct German equivalents, prefer the established German technical term over creating a new translation
## File Encoding Requirements
To ensure consistent character representation and prevent encoding-related issues, the following encoding rules must be followed for all source files:
### Required Encoding
- **All C# source files (*.cs)** must use UTF-8 with BOM encoding
- **All XAML files (*.xaml)** must use UTF-8 with BOM encoding
### Benefits of UTF-8 with BOM
- Ensures proper handling of special characters and international text
- Prevents encoding-related merge conflicts
- Maintains consistent line endings across development environments
- Enables correct display of all characters in the IDE
### IDE Configuration
In Visual Studio:
1. Go to Tools > Options > Text Editor > [Language] > File Extension
2. Set "Encoding" to "Unicode (UTF-8 with signature) - Codepage 65001"
### When Creating New Files
When creating new files, always ensure the encoding is set to UTF-8 with BOM. This applies to all new source code files added to the project.
@@ -0,0 +1,190 @@
# How to create scripts
In order to do any changes over some database table, there are couple of things you need to do first.
## Reserve the script number
The .excel sheet can be found in Teams `c-entron Entwickler` group in the files tab `Datenbankupdate 2 1.xlsx`.
What you have to do there is just write a number - known as a Script number, which has to be for one bigger than the
number used before.
Enter the date of the day when the script number is being reserved. Write a short description about what the script is.
Enter the version from which the change will be included and in the last column your own name.
![Reserve script number](../../../.attachments/How-to-create-scripts/ReserveScriptNumber.png)
> Note: If there are more scripts that have to be executed, all of them can use the same Script number.
## Writing your script method class
Navigate to `Centron.BL/Administration/Scripts/ScriptMethods/Scripts` and create a class with the name `ScriptMethod<x>.cs` where x is your script number.
In the class you have to implement `BaseScriptMethod` and with that the property `ApplicationVersion` and the method `GetSqlQueries()`.
`ApplicationVersion` should be the current version and only ever change the `Buildversion` part (the one marked with <y> in the example).
`GetSqlQueries()` should `yield return` your sql script(s) - SEE NEXT SECTION BEFORE WRITING IT.
```csharp
internal class ScriptMethod<x> : BaseScriptMethod
{
public override Version ApplicationVersion => new(2, 0, <y>, 0);
public override IEnumerable<string> GetSqlQueries()
{
yield return "<here goes your script>";
OR
yield return ScriptHelpers.AddColumnIfNotExists("<your table>", "<your column>", ..);
}
}
```
## Writing your script
There are 2 main ways to write your sql scripts, depending on your needs.
### Using ScriptHelpers
For certain tasks the `ScriptHelpers.cs` will generate the actual sql for you and you just have to call the correct method.
Here are some of the main tasks you can use it for, but check for yourself if scripthelpers can help you.
- `AddColumnIfNotExists()`
- `AddTableIfNotExists()`
- `AddRightIfNotExists()`
- `AddIndexIfNotExists()`
- `AddForeignKeyIfNotExists()`
*If there is a scripthelper method that can help you, you really should use it.*
***Using a scripthelper does not exonerate you from testing your script!***
See (disregard the properties in these examples):
ScriptMethod11355.cs
ScriptMethod11358.cs
### Plain SQL
This is used for everything not covered by `ScriptHelpers` and allows you to just write your own scripts.
If you feel like what you're writing can be automated relativly easily, think about adding method(s) to `ScriptHelpers` so it's reusable.
See (disregard the properties in these examples):
ScriptMethod11359.cs
ScriptMethod11364.cs
### Writing a C# script
If you need to or prefer to write data migration using C# instead of pure SQL, you can do so by overriding the `ExecuteScript()` method instead.
```csharp
public override Result ExecuteScript(DAOSession session)
{
// your code here
return Result.Success();
}
```
You can use the `DAOSession` to interact with the database while having access to the full C# codebase.
If you need both C# and SQL, it is recommended to separate the migration into two separate scripts.
## Adding comments
Depending on the complexity of your script you either should (if less complex) or must (if complex) add a comment on what changed, why and for views it'd be nice to have a 'this script supercedes script <xyz>' with xyz being the last change to that view.
You can mostly reuse the text from `Datenbankupdate 2 1.xlsx` here.
# Legacy ways
The below section is an old way to write script methods and should no longer be used.
## Write the script
In c-entron.NET solution, under the `Centron.BL.Administration.Scripts.ScriptMethods.SqlStatements` can be found a file named
SQLScriptCollection`X`.xaml, where `X` represents the latest collestion of scripts and is the file that is supposed to be edited.
By scrolling to the bottom, you can find templates that are being used:
```xml
<Script Name="" ScriptNumber="">
<Create>
<![CDATA[ { Create Query } ]]>
</Create>
<Alter>
<![CDATA[ { Alter Query } ]]>
</Alter>
</Script>
```
```xml
<Script Name="" ScriptNumber="">
<UpdateQuery>
<![CDATA[ { Update Query } ]]>
</UpdateQuery>
</Script>
```
#### Which template to use?
First template is used to do manipulations with views, functions and store procedures.
Second template is supposed to be used for any table create or update actions, for example: add table, add/remove new column,
add/delete data, drop,...
For any table updates, especially if it's about adding or removing column or adding the whole new table, you should
do a check first if the column or table exists and only then the rest of the query should be executed.
> :exclamation: New scripts should be added at the bottom of the file, before the templates. :exclamation:
> :exclamation: All database changes should be tested by using local or test database before commiting any changes. :exclamation:
## Write new class
For every database change, there has to be a new script method class created. In c-entron.NET solution, under the
`Centron.BL.Administration.Scripts.ScriptMethods` can be found a file named `ScriptMethodsCollection.cs`. In this
file a new internal class has to be created, which contains the script number in the name, eg. `ScriptMethod10806`.
Here is a basic template for it:
```csharp
internal class ScriptMethod10806 : BaseScriptMethod
{
public override int ScriptNumber => 10806;
public override Version ApplicationVersion => new Version(2, 0, 1912, 0);
public override ScriptMethodKind MethodKind => ScriptMethodKind.TableManipulation;
public override ScriptCollectionSource ScriptCollection => ScriptCollectionSource.SQLScriptCollection4;
public override IEnumerable<string> GetScripts()
{
yield return "SomeScriptName";
}
}
```
> Note: New script method class should be added at the bottom of the file.
There are few things that need to be considered before blindly using this template. Depending on what the script is about,
you will have to do some changes to the template.
If your script is a creation or change of view, then you are supposed to do the override of `GetViews()` method. If the script has to do
something with triggers, then you should use `GetTriggers()`, or based on what the script is about just use the propper override method.
All of the methods provided can be found in `BaseScriptMethod.cs` class.
```csharp
public override IEnumerable<string> GetViews()
{
yield return "SomeScriptName";
}
```
Also, one other thing that should be considered, while using the template, is the `ScriptMethodKind`. Depending on what the action
of the script is, the proper `ScriptMethodKind` should be used. There are several of them:
- TableManipulation - this kind is very self explanatory, includes any table change/update
- SystemData - inserting or updating data in the table
- Script
- WithoutTransaction
After doing all those steps, the best way to test if the script is going to execute without any issues is to actually
start the web servie, but with a small change.
In c-entron.NET solution, under `solution items` folder, there is a file named `AssemblyVersionInfo.cs`
which contains the versions.
Normally, the versions are set to `1.0.0.0`. For the testing purposes or just to execute scripts that were previously added
and keep your local database in sync, you should change the versions to `3.0.0.0`.
Save it and start the web service alone or in combination with the UI (doesn't really matter).
In the console window, you can track what scripts were executed and if the execution was successful, otherwise you will know
that something went wrong because it will be displayed in red.
> :exclamation: After testing return the version number to `1.0.0.0` or undo changes done to `AssemblyVersionInfo.cs` file. :exclamation:
@@ -0,0 +1,144 @@
# Centron SQL Server Database Conventions
This document outlines the standardized conventions for SQL Server database tables in the Centron system. These conventions ensure consistency across the database schema and enable efficient data management.
## 1. Table Structure
### Schema Convention
- All database objects must be created in the `dbo` schema
- Always reference tables and other objects using the `[dbo]` schema prefix
- Example: `[dbo].[TableName]`
### Primary Key Convention
Every table must include:
- A primary key column named `I3D` (ID 3develop)
- Defined as `int IDENTITY(1,1) NOT NULL`
- Configured as a clustered primary key index
Example:
```sql
CREATE TABLE [dbo].[TableName](
[I3D] [int] IDENTITY(1,1) NOT NULL,
-- other columns
CONSTRAINT [PK_TableName] PRIMARY KEY CLUSTERED
(
[I3D] ASC
)
)
```
### Foreign Key Convention
- Foreign key columns must end with the suffix `I3D`
- The prefix before `I3D` identifies the referenced table
- Example: `AccountI3D` references the `Account` table's `I3D` column
## 2. Standard Tracking Columns
### Creation Tracking
Include in all tables:
- `CreatedByI3D` [int] NOT NULL - References the Personal.I3D of the user who created the record
- `CreatedDate` [datetime2(2)] NOT NULL - Timestamp when the record was created
### Modification Tracking
Include in all tables:
- `ChangedByI3D` [int] NOT NULL - References the Personal.I3D of the user who last modified the record
- `ChangedDate` [datetime2(2)] NOT NULL - Timestamp when the record was last modified
### Deletion Tracking
Implement soft delete pattern with:
- `IsDeleted` [bit] NOT NULL - Flag indicating if the record is deleted (true) or active (false)
- `DeletedByI3D` [int] NULL - References the Personal.I3D of the user who deleted the record
- `DeletedDate` [datetime2(2)] NULL - Timestamp when the record was deleted
## 3. Data Types
### Text Fields
- Use `nvarchar` instead of `varchar` to support Unicode characters
- Specify appropriate length limits:
- Short names/codes: `nvarchar(60)` to `nvarchar(256)`
- Medium text: `nvarchar(512)` to `nvarchar(2000)`
- Long text/descriptions: `nvarchar(max)`
### Date and Time
- Use `datetime2(2)` for standard date/time fields
- Use `datetime2(0)` for date/time fields without milliseconds
### Boolean Values
- Use `bit` data type for boolean values
## 4. Naming Conventions
### Language Considerations
- Historical tables and columns may use German names
- All new tables and columns must use English names
- Do not rename existing German table/column names to maintain compatibility
### Table Naming
- Use PascalCase for table names
- Use plural form for entity collection tables (e.g., `Accounts`, `Employees`)
- Use singular form for lookup tables (e.g., `Status`, `Category`)
### Parent-Child Relationships
- Name child tables to reflect their relationship to the parent
- Example: `AccountDevices` and `AccountDeviceUris`
## 5. Indexing Guidelines
### Clustered Indexes
- Each table must have exactly one clustered index on the primary key (`I3D`)
### Non-Clustered Indexes
- Create non-clustered indexes on foreign key columns and frequently queried columns
- Follow naming pattern: `IX_TableName_Column1_Column2`
- Example:
```sql
CREATE NONCLUSTERED INDEX [IX_AccountDevices_AccountI3D] ON [dbo].[AccountDevices]([AccountI3D] ASC)
```
## 6. Example Tables
### Parent Table Example
```sql
CREATE TABLE [dbo].[AccountDevices](
[I3D] [int] IDENTITY(1,1) NOT NULL,
[AccountI3D] [int] NOT NULL,
[ShortName] [nvarchar](256) NOT NULL,
[DescriptionRTF] [nvarchar](max) NULL,
[Description] [nvarchar](max) NOT NULL,
[DeviceId] [nvarchar](256) NULL,
[WarrantyExpiryDate] [datetime2](0) NULL,
[CreatedDate] [datetime2](2) NOT NULL,
[CreatedByI3D] [int] NOT NULL,
[ChangedDate] [datetime2](2) NOT NULL,
[ChangedByI3D] [int] NOT NULL,
[SerialNumber] [nvarchar](60) NULL,
[Model] [nvarchar](512) NOT NULL,
[Manufacturer] [nvarchar](255) NOT NULL,
[Location] [nvarchar](255) NOT NULL,
[IsDeleted] [bit] NOT NULL,
[DeletedDate] [datetime2](2) NULL,
[DeletedByI3D] [int] NULL,
[OriginKind] [int] NOT NULL,
[BranchI3D] [int] NOT NULL,
CONSTRAINT [PK_AccountDevices] PRIMARY KEY CLUSTERED
(
[I3D] ASC
)
)
```
### Child Table Example
```sql
CREATE TABLE [dbo].[AccountDeviceUris](
[I3D] [int] IDENTITY(1,1) NOT NULL,
[AccountDeviceI3D] [int] NOT NULL,
[Kind] [int] NOT NULL,
[Uri] [nvarchar](2000) NOT NULL,
CONSTRAINT [PK_AccountDeviceUris] PRIMARY KEY CLUSTERED
(
[I3D] ASC
)
)
```
@@ -0,0 +1,65 @@
# **How to add a new right**
Creating a new right is quite simple:
First you should open `UserRightsConst.cs`, since it'll give you the infos you need. You can also open the rightsmodule in c-entron.NET, select a right, then click "I3D kopieren" in the ribbon to find the `I3D` of a specific right.
You are creating a [script](How-to-create-scripts.md) that adds a new entry to the `Sichrech` table:
- the `I3D` will later become the value of the right. The next `I3D` is on top of `UserRightsConst.cs`
- `Nummer` is not important for centron, it can be 0.
- `FomName` is important for Delphi, but not for Centron, so it can remain null.
- the same for `FomCont`.
- With `Text` the name of the right is entered as it is displayed in the rights management.
- In `OwnerRecht` the I3D of the right comes in, which is required for your new right.
- If, on the other hand, another right (or rights) requires your new right(s), `NumChildren` will show the number of them.
- at `Beschreibung`, as the name suggests, a short description comes in, which makes the right.
###ScriptHelpers.AddRightIfNotExists(..)
__It is highly advised to use `ScriptHelpers.AddRightIfNotExists(..)` and not write your own script.__
The call should look something like this.
If you need more examples check `ScriptMethod11250.cs`.
```c#
yield return ScriptHelpers.AddRightIfNotExists(
UserRightsConst.Sales.Customer.Helpdesk.SHOW_HOURLYSURCHARGERATES,
UserRightsConst.Sales.Customer.Helpdesk.ID,
"Aufschläge Stundensätze anzeigen",
"Dieses Recht gibt an, ob ein Nutzer Aufschläge Stundensätze anzeigen kann.");
```
###Raw sql script
If you have to, your script could look like this:
```xml
<Script Name="AddNewMitarbeiterauslastungRight" ScriptNumber="10870">
<UpdateQuery>
<![CDATA[
IF NOT EXISTS (SELECT * FROM Sichrech WHERE I3D = 20800042) BEGIN
INSERT INTO Sichrech
(I3D,Nummer, Text, OwnerRecht, NumChildren, Beschreibung)
VALUES
(20800042, 0, 'Mitarbeiterauslastung - nur eigene Filiale', 10930, 0, 'Die Gruppen denen dieses Recht zugewiesen wurde, können nur die eigene Filiale in der Mitarbeiterauslastung sehen.');
UPDATE Sichrech
SET NumChildren = NumChildren + 1
WHERE I3D = 10930;
END;
]]>
</UpdateQuery>
</Script>
```
At the end you write a constant for your right in `UserRightsConst` et voilà you can use your right.
@@ -0,0 +1,45 @@
# How to check user rights
Rights and groups can be managed in the `Rechteverwaltung` module.
The IDs for each user right can be found in `UserRightsConst.cs`. Always use these constants instead of the id itself.
## In viewmodels (Centron.WPF.UI)
In viewmodels it's quite easy to check for user rights:
``` csharp
CentronCache.Instance.CurrentUserAppRights.Any(f => f.I3D == UserRightsConst.T);
```
## For modules
Checking if an user has access to a module in `ModuleRegistration.cs` can be done via the first overload on:
``` csharp
ModuleRegistrationItem.For<TController>(() => Helper.HasRights(UserRightsConst.T))
```
## In BL (Centron.BL)
To check out user rights in BLs simply call `AppRightsBL.CheckRightsFromUser(currentUserI3D, your rightids)` then check if the result contains the necessary right.
``` csharp
var checkRightsResult = new AppRightsBL(Session).CheckRightsFromUser(currentUserI3D, new List<int>
{
UserRightsConst.Sales.Customer.CustomerCommon.CREATE_CUSTOMER,
UserRightsConst.Sales.Customer.CustomerCommon.EDIT_CUSTOMER,
UserRightsConst.Sales.Customer.CustomerCommon.DELETE_CUSTOMER,
UserRightsConst.Sales.Customer.CustomerCommon.SEARCH_CUSTOMER,
UserRightsConst.Sales.Customer.CustomerCommon.UNLOCK_CUSTOMER
});
```
Then check via:
``` csharp
if (checkRightsResult.Contains(UserRightsConst.Sales.Customer.CustomerCommon.CREATE_CUSTOMER) == false)
{
return Result.AsError("Fehlende Rechte um Accounts zu erstellen");
}
```
(from `AccountBL.cs`)
@@ -0,0 +1,134 @@
# How to create Mail Template
Since we are migrating all mail templates to new structure, there are few things that changed.
## New Mail Template structure
From now on, all mail templates are supposed to be in the `MailVorlagen` table, where we are saving every mail template with its own classification.
In order to accomplish that, in the class `MailTemplateReference`, you can see what defines an actual mail template (`ObjectKind`, `ObjectI3D`, `SubObjectKind`, `TemplatePrio`).
Why are these properties important? – Every type of mail-template can be uniquely identified by the combination of these values. For example Offer-Mails have ObjectKind=Offer, and all other values are NULL. If there is a MailTemplate in the database with ObjectKind=Offer, and all other values NULL, we know it has to be of type “Offer-Mail”.
ObjectI3D can be used to attach this mail-template to some other database row (for example a mail-template with ObjectKind=HelpdeskType, and ObjectI3D is that specific Helpdesk-Type it is used for).
SubObjectKind can be used as another unique identifier, if you want to group similar mail-templates together. For example, ObjectKind=Escalation has multiple mail-templates for different cases. They don’t depend on other database rows tho, so we can’t use the ObjectI3D to distinguish them. That’s where we use the SubObjectKind then.
How do we do that? – We have created a class named `MailTemplateType` which will contain the mail template skeleton definition. Here is an example how it can be added.
```csharp
public static class AccountActivities
{
public static MailTemplateReference CrmActivity => MailTemplateReference.Create(
CentronObjectKindNumeric.AccountActivity, (int)AccountActivityKind.CRM, null, defaultSubject: string.Empty, defaultBody: string.Empty);
public static MailTemplateReference Note => MailTemplateReference.Create(
CentronObjectKindNumeric.AccountActivity, (int)AccountActivityKind.Note, null, defaultSubject: string.Empty, defaultBody: string.Empty);
public static MailTemplateReference PhoneNote => MailTemplateReference.Create(
CentronObjectKindNumeric.AccountActivity, (int)AccountActivityKind.PhoneNote, null, defaultSubject: string.Empty, defaultBody: string.Empty);
}
```
Please make sure you also add default text for Subject and Body. This can be done in the same file, in class named MailTemplateDefaultText. The Body text can be added as plain text, because we are going to make sure that it’s converted to RTF in the central method.
```csharp
public class MailTemplateDefaultText
{
public static string WebOfferDefaultSubject = @"Default subject";
public static string WebOfferDefaultBody = @"Default body";
}
```
Now after this step is done, you should make sure that Mail Templates Module uses the correct Mail Template Type for the corresponding Mail Template.
This step is quite easy, but if there are questions just ask me and I will gladly help.
In the `MailTemplatsViewModel` that is in Controls, in the method `CreateMailTemplateTreeViewItemChildren` where all children are created, go to the method that is creating mail templates with the corresponding `CentronObjectKindNumeric` value.
There you must replace the parameters that are sent to `CreateMailTemplate` method with the `MailTemplateType` values for the needed parameters.
> Tip: For example, you can check how it’s done in the `CreateAccountActivitiesMailTemplates` method.
> Note: This approach will be much more simplified after we are done with the migration part, but for now we stick with it.
## Using new methods
Now that we have covered that part, it’s time to replace the way we get those mail templates and replace each call with the new method that is returning correct new mail template.
This step is also easy and very simple, but it is a bit different if we are using the logic in c-entron.NET or one of our other applications.
In `MailTemplateBL.cs` there are 2 methods. One is for the internal (in the Web-Service) usage
```csharp
public Result<MailTemplate> GetMailTemplate(MailTemplateReference mailTemplateReference, int? branchI3D)
```
and the other one is for external (SBO, Outlook AddIn, …) usage.
```csharp
public Result<MailTemplate> GetMailTemplate(CentronObjectKindNumeric objectKind, int? subObjectKind, int? objectId, int? branchI3D)
```
You probably ask yourself what the difference is and why do we have two of them. - Well, there’s no difference. The method for internal usage is just calling the method
for external usage, just because of the method parameters. Like I have already mentioned, we have this `MailTemplateType` class which is accessible only in c-entron.NET, so external
apps must pass each of the parameters, that are defining each mail template skeleton.
## Migration/Creation of the Mail Template
Now one more thing that we need to cover is migration of the existing mail templates that are saved elsewhere and creation of new mail templates.
No matter if you are creating a new template, or migrating an existing one, the process is the same.
In order to make this step easier and to avoid mistakes, we have created a new method in `ScriptHelper`. You just have to make sure to pass the scripts for getting the
correct subject and body and the other of the parameters, the rest will be handeled by the method itself.
This method will also make sure that the body is inserted as RTF.
```csharp
internal static void InsertMailTemplate(DAOSession session, string subjectScript, string bodyScript, CentronObjectKindNumeric objectKind, int? subObjectKind, int? objectI3D, int? templatePrio = null)
```
Here is one example of how the migration can be done.
```csharp
public override Result ExecuteScript(DAOSession session)
{
ScriptHelpers.InsertMailTemplate(session,
GenerateAppSettingsSQL(AppSettingsConst.HelpdeskForwardingInternalEmailSubject),
GenerateAppSettingsSQL(AppSettingsConst.HelpdeskForwardingInternalEmailBody),
CentronObjectKindNumeric.HelpdeskClass,
1,
null);
ScriptHelpers.InsertMailTemplate(session,
GenerateAppSettingsSQL(AppSettingsConst.HelpdeskForwardingExternalEmailSubject),
GenerateAppSettingsSQL(AppSettingsConst.HelpdeskForwardingExternalEmailBody),
CentronObjectKindNumeric.HelpdeskClass,
2,
null);
}
private string GenerateAppSettingsSQL(AppSettingsConst settingsConst)
{
return $@"SELECT WertMemo FROM Stammdat WHERE I3D = {(int)settingsConst}";
}
private string GenerateTextModuleSQL(TextModuleType type)
{
return $@"SELECT Text FROM Textbaustein WHERE Art = {(int)type} AND SichbenuI3D = 0 AND KundenI3D = 0";
}
```
> :exclamation: Please, make sure that this step is required only if the mail template is not in the MailVorlagen table.
> Tip: In the `MailTemplateBL.cs` there is still a method `GetAllDelphiMailTemplates`, which can help you to get the needed parameters info faster as well.
@@ -0,0 +1,339 @@
# How to EndToEnd test
## General
EndToEnd tests are supposed to test everything from the `WebServiceBL` layer and downwards (see the diagram [here](GeneralStructure.md)).
In other words, with EndToEnd tests we are testing the `WebServiceBL`, the `BL` and the `database`.
The project can be found at `/tests/Centron.Tests.EndToEnd` and all the tests are supposed to be in the `Tests` namespace.
For example, there are tests for the c-entron.NET `Receipt Area` are at `/tests/Centron.Tests.EndToEnd/Tests/Receipts/ReceiptTest.cs`.
The tests are executed as part of every Pull-Request.
If one of the tests fails, the PR cannot be merged.
> Caution!
> Some tests are a bit flaky sometimes. That means they fail even tho they should not.
> In these cases you can just let the PR get built again, and the tests should run successful.
We should try to make all tests NOT flaky.
## Different kinds of tests
We can roughly categorize all tests into 2 categories:
1. [General purpose tests](#general-purpose-tests)
2. [Specialized tests](#specialized-tests)
They are only different in what they do, and it's not a hard categorization either.
You still create both kinds of tests the same way, and they will look the same.
They both inherit from `EndToEndTest.cs` and only differ in **what** and **how much** they do.
You can also inherit from `CentronTest.cs`, if you don't need database access for your test.
And also, there is a `PerformanceTest.cs`, that measures the performance of some code, and fails if the performance-goal is not met.
We can use this kind of test to make sure, that we don't have too harsh performance-regressions when we change some things.
For example, we have a test that makes sure big receipts can be saved in less than 5 seconds.
If we add something to the SaveReceipt method, or change some other things around, and the receipt suddenly takes 8 seconds to save, then we made a mistake and have to correct it.
### General purpose tests
These tests are general purpose, doing every day things that the user does too.
For example `creating a new customer`, `adding addresses` and `contact-persons` to it, `renaming contact-persons`, `removing addresses` and so on.
The general purpose tests are supposed to cover **really much code**.
We want them to fail if we break a workflow that the user is doing every day.
### Specialized tests
The specialized tests are way smaller than the general purpose tests and are usually created as a response to a ticket that was fixed, or a new feature that was created.
For example `make sure that a setting works as expected`, or `make sure that a bug was fixed`.
These specialized tests are supposed to cover **really small amounts of code**, just enough to make sure the specific thing works as expected.
We want them to fail if we break a specialized setting that some users might be using.
## How does it work?
1. Before each test is executed a database backup (`/Infrastructure/DatabaseBackup.zip`) is restored
2. The `DAOFactory` is configured to use that new database
3. Any new scripts (`IScriptMethod`) are executed
4. The `Test.Execute` method is executed
5. The database backup is removed
This workflow means that every test always has the same starting configuration.
This fact is extremely useful because it allows us to execute these tests today, or in a month, or in a year, and we will still get the same result in the end.
If you create new entries in a table for example, they will get the same I3D every time.
Actually, that's the most important thing about these tests, and why they even work at all: **The tests are repeatable!**
If they were not repeatable, they would fail without any real breaking changes, and then they are not useful to find the breaking changes.
## How to run the tests on your machine
Because we restore the database backup before each test, and the backup is on your local machine, the **SQL Server also needs to be on your local machine**.
So if I work on my own machine `CS-ULM-DANHAE`, and I want to run some tests, I absolutely need a SQL Server installed on my own machine (`CS-ULM-DANHAE\SQL2017` in my case).
**You cannot use a SQL Server on a different machine.**
> You need at least an SQL Server 2017.
To **configure the connection to the SQL Server** you have to **change some small things** in the `/Infrastructure/Database.cs` class.
You will see three properties at the top: `Server`, `Username` and `Password`. They all look similar to this:
``` csharp
public static string Server
{
get
{
var server = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_SERVER");
if (string.IsNullOrWhiteSpace(server) == false)
return server;
throw new Exception("You have to configure the SQL server for EndToEndTests");
return "YOUR-MACHINE\\SQL2017"; //Enter your local SQL server here
}
}
```
You can see, it at first tries to get the configured value from an **environment variable** - that is exactly how the code gets the connection when it's run by the **build server**.
The build-server will set the environment variables to a SQL Server installed on the build-server itself (so it's locally to where the tests will be executed).
But if the environment variable is not set, it will **throw an Exception**.
If you want to configure the EndToEnd tests to use your own local SQL Server, you just have to **comment out the exception**, and **change the return value of the property**.
```csharp
public static string Server
{
get
{
var server = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_SERVER");
if (string.IsNullOrWhiteSpace(server) == false)
return server;
//throw new Exception("You have to configure the SQL server for EndToEndTests");
return "CS-ULM-DANHAE\\SQL2017"; //Enter your local SQL server here
}
}
```
If you do the same thing for the `Username` and `Password` properties, you are done and can execute the tests locally on your machine.
> :exclamation: Make sure to not commit these changes! :exclamation:
## How data is validated
When executing the test, you will probably create some data or some objects.
For example, you will create a new customer, a new ticket, or a new receipt.
The test has to validate, that these objects look exactly how expected.
This is done with a class called `CentronVerifier`. It has a `Verify` method that takes the data you want to verify, and a name for it.
```csharp
this.Verifier.Verify("CreatedOffer", offer);
```
This will create a file called `CreatedOffer.actual.txt` that is basically the `offer` json serialized.
It will then compare this file, to a `CreatedOffer.expected.txt` file.
If they **differ**, **something has changed**, and the **test fails**.
If they are **the same**, **everything still works** as expected, and the **test continues**.
The first time you execute a test, of course there will be no `*.expected.txt` file, and thus the test will fail.
You're supposed to go ahead, and rename the `*.actual.txt` one to `*.expected.txt`.
So when you execute the test again, a new `*.actual.txt` file will be created, and compared to the `*.expected.txt`.
This also means, the `*.expected.txt` should be added to source-control, but the `*.actual.txt` files should **NOT** be added to source-control.
You can also verify data from a table by using the `VerifyTable` method.
```csharp
this.Verifier.VerifyTable("TheCustomer", "dbo.Kunden", where: "I3D = 12345");
```
With that you have to provide a name again, the table that you want to verify, and a optional where filter.
A `SELECT * FROM [Table] WHERE [Filter]` statement will be executed, and the result will be used as data for the `Verify` method.
So everything with the `*.expected.txt` and `*.actual.txt` files works exactly the same as with the `Verify` method.
## Tips and other useful things
### Run any sql statement in your test
The base class for all tests `EndToEndTest.cs` provides a useful method `Sql` to run any sql statement in your test.
You can use it for example if you easily wanna change a setting in the database before running your test. Or you want to change a configuration property on a customer or something else.
```csharp
this.Sql("UPDATE Stammdat SET Wert = 1 WHERE I3D = 1234");
```
### Debugging a test
You can also very easily debug a test by setting breakpoints in your code, and then `Right-Click` -> `Debug` the test in the `Test Explorer`.
![Debug a test](./.attachments/How-to-end-to-end-test/DebugTest.png)
### Diff view for *.expected.txt and *.actual.txt
If a test fails because something has changed between a `*.expected.txt` and a `*.actual.txt` file, you can see a diff between those two files.
This allows you to more easily spot the differences.
It is pretty easy to do. If a test fails because of a `this.Verifier.Verify` call, you just have to [debug the test](#debugging-a-test).
Now if the `Verify` call fails the test will fail as usual, but at the same time a Diff-View will open in Visual Studio with both the `*.expected.txt` and the `*.actual.txt` in it.
You can then more easily decide whether the changes are fine, or whether you broke something.
## How to create a test
Lets say we want to write a [general purpose test](#general-purpose-tests) for some `Helpdesk` stuff - creating a new ticket for a customer, setting some values and saving it.
> The real general purpose HelpdeskTest is of course supposed to cover even more of the helpdesk code.
> For example forwarding it, adding documents and history entries, and closing the ticket.
### Step 1
Make sure to [configure the database connection](#How-to-run-the-tests-on-your-machine) so you can run them on your computer.
### Step 2
Create a new namespace/folder under the `/Tests` namespace: `WikiHelpdesk`.
### Step 3
In that namespace, create a new class `WikiHelpdeskTests.cs`.
### Step 4
Make that class inherit from `EndToEndTest`.
### Step 5
Implement the constructor for the class, and override the `Execute` method.
Your class should look like this now:
```csharp
using Centron.Tests.EndToEnd.Infrastructure;
using Xunit.Abstractions;
namespace Centron.Tests.EndToEnd.Tests.WikiHelpdesks
{
public class WikiHelpdeskTests : EndToEndTest
{
public WikiHelpdeskTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
}
public override void Execute()
{
}
}
}
```
> This is the basic setup you always have to do for a test.
> Everything that follows now is actually implementing the test and [validating data](#how-data-is-validated).
### Step 6
Create a new method `CreateTicket`
```csharp
private HelpdeskDTO CreateTicket()
{
using (var session = new BLSession())
{
//Get a LoggedInUser for the current user using the base class method
var loggedInUser = this.GetLoggedInUser(11); // Default is 11 if no parameter is provided
//Create a new ticket
var ticket = session.GetBL<HelpdeskWebServiceBL>().GetNewTicketFromCustomer(loggedInUser.User, 10022);
//Set some values
ticket.ShortDescription = "Mein super wichtiges Ticket!";
ticket.Description = "Mit einer nicht sehr ausführlichen Beschreibung.";
//Save the helpdesk
var saveHelpdeskResult = session.GetBL<HelpdeskWebServiceBL>().SaveHelpdesk(ticket, loggedInUser);
//Verify the saveHelpdeskResult
this.Verifier.Verify("SaveHelpdeskResult", saveHelpdeskResult);
//Load the complete helpdesk object
var savedTicket = session.GetBL<HelpdeskWebServiceBL>().GetHelpdeskByI3D(saveHelpdeskResult.Data, loggedInUser);
//Verify the savedTicket
this.Verifier.Verify("SavedTicket", savedTicket);
return savedTicket.Data;
}
}
```
I wanna especially highlight the two lines that use the `this.Verifier`:
```csharp
this.Verifier.Verify("SaveHelpdeskResult", saveHelpdeskResult);
```
and
```csharp
this.Verifier.Verify("SavedTicket", savedTicket);
```
These calls make sure that the [data is actually validated](#how-data-is-validated).
### Step 7
Call the new `CreateTicket` method in the `Execute` method.
```csharp
public override void Execute()
{
var ticket = this.CreateTicket();
}
```
You created your first test! :tada:
### Step 8
As you know from the chapter on [how data is actually validated](#how-data-is-validated) the test will fail right now.
There is no `SaveHelpdeskResult.expected.txt` and no `SavedTicket.expected.txt`.
So lets run the test by opening the `Test Explorer` in Visual Studio and `Right-Click` -> `Run` the test.
![Run the test](./.attachments/How-to-end-to-end-test/RunTest.png)
It will take a little while, but after like 40 seconds you should get a failed test result.
![Failed test run](./.attachments/How-to-end-to-end-test/FailedTest.png)
### Step 9
If you activate `Show All Files` in the solution explorer for the `Centron.Tests.EndToEnd` project, you should see the `SaveHelpdeskResult.actual.txt` file.
![SaveHelpdeskResult.actual.txt file in solution explorer](./.attachments/How-to-end-to-end-test/SaveHelpdeskResult-actual-txt.png)
Go ahead and rename it to `SaveHelpdeskResult.expected.txt`.
It automatically gets picked up by the csproj file too.
![SaveHelpdeskResult.expected.txt file in solution explorer](./.attachments/How-to-end-to-end-test/SaveHelpdeskResult-expected-txt.png)
### Step 10
Run the test again!
...
As expected, it still fails because of the second `Verify` call.
But you now know what to do.
### Step 11
A new file `SavedTicket.actual.txt` was created.
Feel free to open it, and look at the data that is verified.
Every single line in that JSON document has to stay exactly the same.
Every property in that file is not allowed to change.
![SavedTicket.actual.txt file in solution explorer](./.attachments/How-to-end-to-end-test/SavedTicket-actual-txt.png)
Rename it to `SavedTicket.expected.txt`.
![SavedTicket.expected.txt file in solution explorer](./.attachments/How-to-end-to-end-test/SavedTicket-expected-txt.png)
### Step 12
Run the test once again!
...
IT WORKS! :tada:
![Successful test](./.attachments/How-to-end-to-end-test/SuccessfulTest.png)
The test is finished!
You can now add even more logic to the `Execute` method.
@@ -0,0 +1,15 @@
# Fixed Memory leaks
A collection of memory leaks we fixed and where they were.
# 1. DependencyPropertyDescriptor.FromProperty
Previously found in "ContentControlContentChangedBehavior".
Switching to PropertyChangeNotifier which just creates a normal Binding fixes the issue.
# 2. DispatcherTimer
Previously found in "TicketDetailView.xaml.cs"
A DispatcherTimer started with '.Start()' need a '.Stop()' call.
If you use '+=' u likewise need a '-='.
@@ -0,0 +1,183 @@
# Settings Management in c-entron.NET
This guide explains how application settings are managed in the c-entron.NET project, covering both legacy and current approaches, and providing best practices for working with settings.
## Overview
The c-entron.NET application uses two separate database tables for storing application settings:
1. **Stammdat** - Legacy settings table with historical settings
2. **ApplicationSettings** - Current table for new settings
This dual-table approach exists for historical reasons, and all new settings should be added to the `ApplicationSettings` table.
## Settings Tables
### Legacy: Stammdat Table
The Stammdat table contains many historical settings that are accessed through the `AppSettingsConst` enum.
- **Enum File**: `src/backend/Centron.BL/Administration/Settings/AppSettingsConst.cs`
- **Access**: Settings are accessed through the `AppSettingsBL.GetSettings(AppSettingsConst)` method
- **Updates**: Although we maintain these settings, no new settings should be added to this table
### Current: ApplicationSettings Table
The ApplicationSettings table is the current standard for all new application settings.
- **Enum File**: `src/backend/Centron.Interfaces/Administration/Settings/ApplicationSettingID.cs`
- **Next Free ID**: Tracked in a comment at line 15 of `ApplicationSettingID.cs`
- **Access**: Settings are accessed through the `AppSettingsBL.GetSettings(ApplicationSettingID)` method
- **Settings Descriptions**: Defined in `src/backend/Centron.Interfaces/Administration/Settings/ApplicationSettingDefinitions.cs`
## ID Management
When adding new settings, you must:
1. Check the next available ID from the comment in `ApplicationSettingID.cs`:
```csharp
// Next Centron Settings ID : 10370
// Current Riverbird Settings ID : 50035
```
2. Use the next ID in sequence (in this case, 10370)
3. Update the comment with the next available ID after adding your setting
The Riverbird setting IDs (starting with 50xxx) are not used by c-entron.NET.
## Setting Definitions
For each new setting in `ApplicationSettingID.cs`, you must add a corresponding description in the `ApplicationSettingDefinitions.cs` file:
```csharp
case ApplicationSettingID.YourNewSetting:
return "Description of what this setting does and how it's used.";
```
The description should clearly explain:
- The purpose of the setting
- What data it stores
- Any relevant format or validation constraints
## Accessing Settings
### Group Setting Classes
The client never accesses settings tables directly. Instead, we use "group setting classes" to manage related settings. These classes:
1. Load settings from the database
2. Provide a strongly-typed interface for accessing settings
3. Manage updating settings back to the database
### Example: Loading Settings
```csharp
// Example from ReceiptWebServiceBL.GetReceiptInvoiceSettings()
var appSettings = this._appSettingsBL.GetSettings
(
ApplicationSettingID.InvoiceArchiveActive,
ApplicationSettingID.IsZugferdInvoiceActive,
// additional settings...
);
var settings = new ReceiptInvoiceSettingsDTO
{
IsInvoiceArchiveActive = appSettings.GetBool(ApplicationSettingID.InvoiceArchiveActive, false),
IsZugferdInvoiceActive = appSettings.GetBool(ApplicationSettingID.IsZugferdInvoiceActive, false),
// map other settings...
};
return Result<ReceiptInvoiceSettingsDTO>.AsSuccess(settings);
```
### Example: Saving Settings
```csharp
// Example from ReceiptWebServiceBL.SaveReceiptInvoiceSettings()
var updateSettings = this._appSettingsBL.GetSettingsForUpdate
(
ApplicationSettingID.InvoiceArchiveActive,
ApplicationSettingID.IsZugferdInvoiceActive,
// additional settings...
);
// Update values
updateSettings.UpdateBool(ApplicationSettingID.InvoiceArchiveActive, settings.IsInvoiceArchiveActive.Value);
updateSettings.UpdateBool(ApplicationSettingID.IsZugferdInvoiceActive, settings.IsZugferdInvoiceActive.Value);
// update other settings...
// Save all changes
updateSettings.SaveSettings();
return Result<bool>.AsSuccess(true);
```
## API Integration
Settings are exposed through API methods, allowing client applications to retrieve and update settings.
### API Patterns
1. All setting API methods must use HTTP POST
2. Get methods return a DTO containing the settings
3. Save methods accept a DTO with the settings to update
### Example API Methods
```csharp
// In ICentronRestService.cs
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json)]
Result<ReceiptInvoiceSettingsDTO> GetReceiptInvoiceSettings();
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json)]
Result<bool> SaveReceiptInvoiceSettings(ReceiptInvoiceSettingsDTO settings);
```
## Best Practices
### Adding New Settings
1. Use the next available ID from the comment in `ApplicationSettingID.cs`
2. Add your setting to the `ApplicationSettingID` enum
3. Update the "Next Centron Settings ID" comment
4. Add a detailed description in `ApplicationSettingDefinitions.cs`
5. Create or update group setting classes to access your setting
### Setting Types
Application settings support multiple data types:
- **Boolean**: Use `GetBool()` / `UpdateBool()` methods
- **Integer**: Use `GetInt()` / `UpdateInt()` methods
- **String**: Use `GetString()` / `UpdateString()` methods
- **Large String**: Use `GetLargeString()` / `UpdateLargeString()` methods
- **Enum**: Use `GetEnum<T>()` / `UpdateEnum<T>()` methods
- **Decimal**: Use `GetDecimal()` / `UpdateDecimal()` methods
### Default Values
When retrieving settings, always provide a default value in case the setting doesn't exist:
```csharp
// Example with default value
bool isActive = appSettings.GetBool(ApplicationSettingID.SomeSetting, false);
```
## Common Scenarios
### Creating a New Group Settings Class
1. Define a DTO class to hold the settings
2. Create Get method that loads settings from AppSettingsBL
3. Create Save method that updates settings with AppSettingsBL
4. Add corresponding API methods
### Migrating Legacy Settings
When migrating settings from Stammdat to ApplicationSettings:
1. Add the new setting to ApplicationSettingID
2. Add its description to ApplicationSettingDefinitions
3. Update code to read from both sources during transition
4. Eventually remove the old setting access after migration
@@ -0,0 +1,15 @@
#A couple of Links useful for XRechnung
Offical Documentation PDF is embedded in the source code (ZUGFeRD-2.1.1 - Spezifikation_TA (1)).
Here the link for this document in the official Version archive: https://www.ferd-net.de/standards/zugferd-version-archive/zugferd-2.1.html
##Opensource Implementation
https://github.com/stephanstapel/ZUGFeRD-csharp
Nice place to check out some Stuff, has a couple of Demo files.
Would be nice to switch to this in the future instead of creating our own implementation.
##KOSIT (Koordinierungsstelle für IT-Standards)
https://github.com/itplr-kosit
Different helpful repositories for XRechnung.
For example a cli tool to verify the xml.
@@ -0,0 +1,241 @@
# How to add webservice methods
Please refer to the [General Structure](../../getting-started/general-structure.md) page to get an overview of the general structure.
Please refer to the [On DTOs and Entities](../../reference/architecture/dtos-and-entities.md) to get a better understanding on what types to use where.
For a reference with a correct implementation check out `MyDayBL.cs` and associated classes.
## Businesslogic
### BL
For this we will assume that `ThingyBL.cs` contains the method `public Result<Thingy> SaveOrUpdateThingy(Thingy item, LoggedInUser currentUser)`. This method checks the input, checks userrights and saves the `Thingy` to the database.
BL methods should always return a result with actual useable results (don't just `return Result.AsSuccess()`). Save methods must always return the entity itself so we get an entity with a valid I3D.
### WebServiceBL
The webserviceBL must be named the same as the BL (`ThingyWebServiceBL.cs` in this case) and the method itself should be named the same as the BL one (`public Result<ThingyDTO> SaveOrUpdateThingy(ThingyDTO item, LoggedInUser currentUser)` in our example).
It's sole purpose is to convert and incoming `ThingyDTO` to a `Thingy` entity and back. Please consult [On DTOs and Entities](../../reference/architecture/dtos-and-entities.md) on how to convert correctly.
This must also return a `Result<Thingy>`. The best way to ensure the result from the BL gets carried over is using `Result<Thingy>.FromResult(<your ThingyDTO>, <yourBLResult>);`.
Our method should look something like this:
``` csharp
public Result<ThingyDTO> SaveOrUpdateThingy(ThingyDTO item, LoggedInUser currentUser)
{
var entity = this.ConvertThingyDTOToEntity(item);
var result = this._thingyBL.SaveOrUpdate(entity, currentUser);
var mappedResult = this.ConvertThingyToDTO(result.Data);
return Result<ThingyDTO>.FromResult(mappedResult, result);
}
```
## RestService
Next we'll create the method in the actual webservice. Please note that both `CentronRestService` and `ICentronRestService` are partial classes and have parts for some categories (`CentronRestService.Helpdesk`, `CentronRestService.Docuboard` etc.). Also note that the methods here are wildly inconistently placed and are pretty much all over the place.
If you find a partial part for the method you want to create use that part, if you know you'll create more than 2-3 methods consider making your own partial part and if neither of those just throw it into `CentronRestService` (or `CentronRestService.DTOPart`).
As our API has a flat surface methods here need to be named more specific than they are in the BL-layers. They **must** contain the name of your entity or something else that make them identifiable on what category they belong in. For example we work with `Thingy` here, so all webservice methods doing anything with them (load/saveorupdate/delete etc.) have to include `Thingy` somewhere (LoadThingies, SaveOrUpdateThingy etc.).
### CentronRestService
It **must** always accept a `Request<T>`/`Request` and return a `Response<T>`/`Response` and nothing else.
The method itself can only contain 2 rows:
1. call the webserviceBL
2. convert the `Result<T>` to a `Response<T>` and return
This is how our method should look like:
``` csharp
public Response<ThingyDTO> SaveOrUpdateThingy(Request<ThingyDTO> request)
{
var result = this.Session.GetBL<ThingyWebServiceBL>().SaveOrUpdateThingy(request.Data, this.GetLoggedInUserByTicket(request.Ticket));
return Response<ThingyDTO>.FromBLResult(result);
}
```
Note the `GetLoggedInUserByTicket(request.Ticket)`-Method to get a `LoggedInUser`.
### ICentronRestService
Creating the method in the interface is very simple, just copy the method signature from the CentronRestService-method over, then add the needed attributes. We always need the following 3 attributes to register the method for the webservice. Please note that the [WebInvoke..] method **must** be 'POST' and the UriTemplate **must** be the same as the method name.
``` csharp
[WebInvoke(Method = "POST", UriTemplate = "SaveOrUpdateThingy")]
[Authenticate]
Response<ThingyDTO> SaveOrUpdateThingy(Request<ThingyDTO> request)
```
With this we have a working webservice method that can be used by external apps via the `Centron.WebServices.Core` / `Centron.Interfaces` reference or a WSDL-Service-Reference or similar.
*Remember to connect to your self-hosted webservice when creating methods or you'll be very confused when it doesnt work (speaking from experience ;) )*
### Request classes
Quite often you need to send several properties in your request for example in filters. For this simply create a `<yourWebServiceMethodName>Request` that aggragates all the needed properties and then use `Request<YourRequest>` as our parameter in the restservice. So if we wanted to filter our `Thingies` we would write our request like this:
``` csharp
[DataContract]
public class GetThingiesRequest
{
[DataMember]
public List<int> ThingyI3Ds { get; set; }
[DataMember]
public DateTime? Before { get; set; }
[DataMember]
public DateTime? After { get; set; }
}
```
``` csharp
[WebInvoke(Method = "POST", UriTemplate = "GetThingies")]
[Authenticate]
Response<ThingyDTO> GetThingies(Request<GetThingiesRequest> request)
```
Note, that these requests are basically DTOs and all restrications/problems of DTOs apply.
Request go into the `Centron.WebServices.Core` project and then the `RestRequests` directory.
## Logics
To use the webservice method in c-entron<span>.NET we must also implement the ILogic interfaces.
Again search for an existing fitting `ILogic` for your method or create your own (`IThingyLogic` for us).
### ILogic
Methods in your ILogic **must** return a `Task<Result<T>>` and should be named the same as the webservice method. But here we can be unspecific again as the context comes from the Logic itself, so instead of `SaveOrUpdateThingy` we can just use `SaveOrUpdate` if we want.
``` csharp
public interface IThingyLogic
{
Task<Result<ThingyDTO>> SaveOrUpdateThingy(ThingyDTO item);
}
```
### BLLogic
This is necessary for the direct database connection and must be named `BL..Logic` (`BLThingyLogic` for us).
It must have 1 constructur that accepts a `ConnectionInfo`:
``` csharp
private readonly ConnectionInfo _connectionInfo;
public BLThingyLogic(ConnectionInfo connectionInfo)
{
Guard.NotNull(connectionInfo, nameof(connectionInfo))
this._connectionInfo = connectionInfo;
}
```
The method itself should only start a task via `Task.Run(..)`, in this task create the webserviceBL and call the correct method. It should look something like this:
``` csharp
public Task<Result<ThingyDTO>> SaveOrUpdateThingy(ThingyDTO item)
{
return Task.Run(() =>
{
using (var session = new BLSession())
{
return session.GetBL<MyThingyWebServiceBL>().SaveOrUpdate(item);
}
});
}
```
### WSLogic
This is necessary for the webservice connection and must be named `WS..Logic` (`WSThingyLogic` for us).
It must have 1 constructur that accepts a `CentronWebServiceConnection`:
``` csharp
private readonly CentronWebServiceConnection _webServiceConnection;
public WSThingyLogic(CentronWebServiceConnection webServiceConnection)
{
Guard.NotNull(webServiceConnection, nameof(webServiceConnection))
this._webServiceConnection = webServiceConnection;
}
```
The method itself simply uses an expression to call the ICentronRestService method you just created.
It is important to never create a request yourself, instead always create it via _webServiceConnection, as this ensures the ticket is set properly.
If you don't need to await the call:
``` csharp
public Task<Result<ThingyDTO>> SaveOrUpdateThingy(ThingyDTO item)
{
return this._webServiceConnection.CallWebServiceMethodWithSingleResultAsync(f => f.SaveOrUpdateThingy(this._webServiceConnection.GetRequest(item)));
}
```
or, if you need to actually await it:
``` csharp
public async Task<Result<ThingyDTO>> SaveOrUpdateThingy(ThingyDTO item)
{
var result = await this._webServiceConnection.CallWebServiceMethodWithSingleResultAsync(f => f.SaveOrUpdateThingy(this._webServiceConnection.GetRequest(item))).ConfigureAwait(false);
// your logic
return result;
}
```
If you await the call, you also need to add the `.ConfigureAwait(false)`. Always try to return the task instead of awaiting it.
The webservice can be called with several methods depending the signature of your webservice method:
returntype | method to use
---|---
Result|CallWebServiceMethodWithResultAsync\<TRequest>
Result\<T>|CallWebServiceMethodWithSingleResultAsync<TRequest, TResult>
Result\<IList\<T>>|CallWebServiceMethodWithListResultAsync<TRequest, TResult>
`TRequest` must be equal to `T` in your `Request<T>` in `ICentronRestService`. As such there can never be a `TRequest` of type `Request<T>`/`Request`.
These methods can also be used without request objects (when your `ICentronRestService` methods parameter is a simple `Request`):
returntype | method to use
---|---
Result|CallWebServiceMethodWithResultAsync
Result\<T>|CallWebServiceMethodWithSingleResultAsync\<TResult>
Result\<IList\<T>>|CallWebServiceMethodWithListResultAsync\<TResult>
## How to use in c-entron<span>.NET
There are two ways to call a webservice method from a viewmodel in Centron.WPF.UI.
If you need the Logic just once simply use `ClassContainer.Instance.WithInstance(..)` like this:
``` csharp
var thingy = await ClassContainer.Instance.WithInstance((IThingyLogic logic) => logic.GetThingyByI3D(thingyI3D)).ThrowIfError();
```
If you need the logic several times, you should create it once via `ClassContainer.Instance.GetInstance<T>()` and use that in your class. This means you also need to implement the `IDisposable` interface and release the instance upon disposing by calling `ClassContainer.Instance.ReleasInstance(yourInstance)`.
``` csharp
public class ThingiesViewModel : IDisposable
{
..
public readonly IThingyLogic ThingyLogic;
public ThingiesViewModel()
{
this.ThingyLogic = ClassContainer.Instance.GetInstance<IThingyLogic>();
}
public void Dispose()
{
ClassContainer.Instance.ReleaseInstance(this.ThingyLogic);
}
..
public void Get()
{
var thingy = await this.ThingyLogic.GetThingyByI3D(thingyI3D).ThrowIfError();
..
}
}
```
Please note the use of `await` and `ThrowIfError()` in both cases.
@@ -0,0 +1,127 @@
# Run the c-entron Web-Service on Linux
Right now we don't create Linux version of our web-service by default.
But you can create a version manually by running the `build-web-service-linux` target from the `Centron.Scripts` project.
It creates a `c-entron Web-Service Linux.zip` file which contains the web-service for Linux.
# How to prepare the Linux server
The Linux web-service is a framework-dependent version, which means you need to install .NET on the Linux server.
[How to install .NET on Linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
The above link also contains a list of all Linux distributions that are supported by .NET.
As far as I know, we should support all of them, but we only tested with Ubuntu and Debian.
Make sure to install the correct version of .NET that is needed for our web-service.
As of right now (Jan. 2024), c-entron.NET and the web-service are running on .NET 8.
Also some other dependencies are required for the web-service to work correctly.
[See here which other dependencies are required](https://docs.devexpress.com/OfficeFileAPI/401441/use-office-file-api-on-linux#prerequisites)
# How to install the c-entron Web-Service on Linux
Copy the `c-entron Web-Service Linux.zip` archive to the server, and extract it wherever you want to install it.
For example purposes, lets use `/opt/centronws`.
Now we need a `WebServiceConfig.xml`, which on windows is created by the `c-entron Web-Service Connection Manager` - but this tool is not available on linux, and right now we don't have a linux-alternative for it.
So I recommend to copy the `WebServiceConfig.xml` from a windows-installation of the web-service.
After the web-service is copied and configured, you can start it by executing the `Centron.Host.Console` application.
You might get some permission errors along the way - fix them by making the `Centron.Host.Console` executable, and giving read-permissions to the directory.
You should see some command-line output from the web-service, and after a couple of seconds the web-service should be started and available per HTTP.
# How to use HTTPS
HTTPS configuration is very different between Linux and Windows, because on Windows its using Windows-only configuration.
So on Linux, you have to edit the `WebServiceConfig.xml`.
Add a `WebServiceCertificateFilePath` XML-node with the path to your pfx-certificate.
And add another XML-node `WebServiceCertificatePassword` containing your password for the pfx-certificate.
For example, if you placed the certificate next to the web-service `/opt/centronws/https-certificate.pfx`, and the password is `123456`, adjust the `WebServiceConfig.xml` to look like this.
```xml
<WebServiceConfig>
<!-- Usual settings that also work for windows -->
<WebServiceAddress>https://localhost:443</WebServiceAddress>
<DatabaseConnectionString>VGhpcyBpcyB3aGVyZSB5b3VyIERhdGFiYXNlQ29ubmVjdGlvblN0cmluZyBzaG91bGQgYmU=</DatabaseConnectionString>
<!-- The 2 following nodes should be added manually -->
<WebServiceCertificateFilePath>./https-certificate.pfx</WebServiceCertificateFilePath>
<WebServiceCertificatePassword>123456</WebServiceCertificatePassword>
<!-- Other settings omitted -->
</WebServiceConfig>
```
Restart the `Centron.Host.Console`, and the c-entron Web-Service should be available per HTTPS with a valid certificate!
# How to make the web-service run in the background
You can use whatever mechanism or service you want for that, just execute the `Centron.Host.Console` application.
One possibility is, to use `systemd` to run the web-service (this obviously only works if `systemd` is available on your Linux distribution).
To use `systemd` create a new file `/etc/systemd/system/centronws.service` with the following contents (adjust paths and user if necessary):
```ini
[Unit]
Description=c-entron web-service
[Service]
WorkingDirectory=/opt/centronws # will set the Current Working Directory (CWD)
ExecStart=/opt/centronws/Centron.Host.Console # systemd will run this executable to start the service
SyslogIdentifier=centronws # to query logs using journalctl
User=c-entron # which user should execute the service, use 'chown yourusername -R /opt/centronws' to take ownership of the folder and files, use 'chmod +x /opt/centronws/Centron.Host.Console' to allow execution of the executable file.
Restart=always # ensure the service restarts after crashing
RestartSec=5 # amount of time to wait before restarting the service
KillSignal=SIGINT # copied from dotnet documentation at https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-3.1#code-try-7
AmbientCapabilities=CAP_NET_BIND_SERVICE # give the executed process the CAP_NET_BIND_SERVICE capability. This capability allows the process to bind to well known ports.
[Install]
WantedBy=multi-user.target
```
After creating this `.service` file, we have to execute some commands.
Reload the systemd services using `sudo systemctl daemon-reload`
Make sure the new service was discovered correctly using `sudo systemctl status centronws`
Start the c-entron Web-Service using `sudo systemctl start centronws.service`
If you want the c-entron Web-Service to start when the machine starts, you can run `sudo systemctl enable centronws.service`
# Differences between Linux and Windows as a c-entron User
There are a couple of limitations when running on Linux, in comparison to running on Windows.
Most of them can be fixed in the future, but right now these limitations apply.
### Sub-Web-Services
The Sub-Web-Services don't work on Linux. All of the code regarding them is contained in the `c-entron Web-Service Connection Manager`, which is not available on Linux.
A refactoring is needed here.
**Our latest stance on this topic is:**
If a customer is running his web-service on linux, then he is a `poweruser`.
And as a `poweruser` he can copy the web-service, configure it for another database, and create another `systemd service` all by himself.
# What is left to do to make the Linux UX good?
### Web-Service configuration tool
On windows we have the `Web-Service Connection Manager`, but we currently don't have a similar tool for linux.
Right now you're out of luck and have to hand-edit the `WebServiceConfig.xml` file, which is a very bad user experience.
What I would like to have is some kind of CLI that guides the user through web-service configuration.
It would be extra nice if you could call the `Centron.Host.Console.exe` with a `configure` command, and then it steps you through the configuration (web-service url, certificate, path, database connection, etc.).
### Certificate password encryption
Right now the certificate-password is written in `plaintext` in the `WebServiceConfig.xml` file.
Once we have the `Web-Service configuration tool` from above, we should `encrypt` the certificate-password instead.
Just as we already encrypt the `database connection-string` and `proxy password`.
### Tool to read your Hardware-ID
On windows it's very easy, because the `Web-Service Connection Manager` shows you your `Hardware-ID`.
On linux this is currently not available, and you as the user can't really do anything by yourself.
Right now the workaround is: Send your linux `/etc/machine-id` to a developer, and he magically creates the `Hardware-ID` from it.
A better experience would be, to have some kind of CLI that prints you your own `Hardware-ID`.
Would be nice if it also was on `Centron.Host.Console.exe`, with a `hardware-id` command, and all it does is print out your `Hardware-ID`.
@@ -0,0 +1,3 @@
# How to create a dialog window
I don't know, but I would like to - please tell me.
@@ -0,0 +1,116 @@
# How to create a module in c-entron.NET
A module in the c-entron<span>.NET is just a view that can be displayed in the main window. It can show up in the module list on the left and can be docked and undocked from the main window.
We'll be using a module to manage our `Thingies` again. If you want to know how to create webservice methods for our thingies check out [this](How-to-add-webservice-methods.md).
## ICentronAppModuleController
The first class to create is a `..AppModuleController`. For us this will be named `ThingiesAppModuleController`.
This class needs to implement the `ICentronAppModuleController` interface. If you go to the `ICentronAppModuleController.cs` file there are summaries for each property and how to fill them properly.
``` csharp
public class TicketListAppModuleController : ICentronAppModuleController
{
public string ModuleName => "Dinge-Liste";
public BitmapImage ImageSmall => new BitmapImage(new Uri("pack://application:,,,/c-entron 2.0;component/Images/Icons/16x16/Thingies_16x16.png"));
public BitmapImage ImageLarge => new BitmapImage(new Uri("pack://application:,,,/c-entron 2.0;component/Images/Icons/32x32/Thingies_32x32_32x32.png"));
public string Description => "Alle Ihre Dinge auf einen Blick";
public CentronModuleCategory MainCategory => CentronModuleCategory.MyCentron;
public string ID => "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}";
public string ReportGroupGuid => null;
public CentronConnectionType[] SupportsConnectionTypes => new[] { ronConnectionType.CentronWebServices, entronConnectionType.SqlServer }
public BaseModule CreateModuleInstance(params object[] param)
{
var viewModel = new ThingiesViewModel();
return new ThingiesView
{
DataContext = viewModel
};
}
public IList<ICentronAppModuleSettingController> GetSettings() => new <ICentronAppModuleSettingController>
{
new ThingiesSettingsController()
};
public void Dispose()
{
}
}
```
## BaseModule (View)
Next we'll create the view and as we've already seen in our `AppModuleController.CreateModuleInstance(..)` it will be called `ThingiesView`. The easiest way to create it, is just to let VisualStudio create a `User Control(WPF)` then change it's type to `BaseModule`. To do this go to the `ThingiesView.xaml.cs` and delete the inheritance of `UserControl`. Next go to `ThingiesView.xaml` and replace `UserControl` with ` controls:BaseModule`, then add this namespace: `xmlns:controls="clr-namespace:CentronSoftware.Centron.WPF.UI.Extension.Controls;assembly=Centron.WPF.UI.Extension"`
*Pro tip: add `d:DataContext="{d:DesignInstance module:TicketListModuleViewModel}"` for intelisense in xaml.*
As all our modules need a ribbon you'll need to implement one of 3 interfaces in your code-behind (`Thingies.xaml.cs`):
interface|function
--|--
IRibbonControlModule|Adds the GetRibonControl() method where your ribbon gets returned
IRibbonControlModuleWithDefaultButtons|Same as above, but also adds our default buttons (Save layout, reset layout, settings, videoportal, reports)
IRibbonControlModuleWithDefaultButtonsAndPreferredSendType|Same as above, but also adds buttons for send type for reports.
``` csharp
public partial class ThingiesView : IRibbonControlModuleWithDefaultButtons
{
public ThingiesView()
{
this.InitializeComponent();
}
public RibbonControl GetRibbonControl()
{
return this.Ribbon;
}
}
```
``` xml
<controls:BaseModule x:Class="CentronSoftware.Centron.WPF.UI.Modules.Helpdesk.TicketDetails.TicketDetailView"
xmlns:controls="clr-namespace:CentronSoftware.Centron.WPF.UI.Extension.Controls;assembly=Centron.WPF.UI.Extension"
[..]
>
[..]
<dxb:RibbonControl x:Name="Ribbon">
[..]
</dxb:RibbonControl>
[..]
</controls:BaseModule>
```
If you want to display a message box on closing or cancel closing, you can override `public override void FormClosing(out bool cancel)` and cancel the close by settings the boolean to **true**.
## ViewModel
At last we will create our viewmodel aptly named `ThingiesViewModel`. It doesnt really need anything, just implement `BindableBase` like every other viewmodel and set it as datacontext for the view in your appmodulecontroller.
## ModuleRegistration
If your module is supposed to be available via the module list on the left, then we also need to register it in `ModuleRegistration.cs`. Simply go to the constructor and add your module to the _module list like this:
``` csharp
ModuleRegistrationItem.For<ThingiesAppModuleController>(
() => Helper.NoRightCheck(),
() => ModuleFeatures.IsThingiesAvailable),
```
The first parameter allows you to check if the user has the specified rights.
The second allows you to hide your module until it is ready to be released. Simply create an entry in `ModuleFeatures.cs`.
@@ -0,0 +1,52 @@
# How to create a settings page
Naming convention for your settingspage is simple:
[YourSettingsName]Settings[Controller/View/ViewModel]
## Create your Controller
First you have to create a class that implements `ICentronAppModuleSettingController`.
`CreateSettingControlInstance()` needs to handle the creation of the view and the viewmodel and **set the DataContext for the view**. This should all be done in this method and not in the ctor of the view.
**You MUST also add an ImageLarge.**
## Create your View
Next create a view of the type `BaseSettingControl`.
You have to implement `DoInitalize()` and simply route it through to your viewmodel. It should handle all the loading of settings.
Then implement `DoSave()` and again simply route it to your viewmodel. If you want to restrict saving you can also implement `CanSave()`. `DoAfterSave()` allows you to execute something after saving of all pages is complete.
The view should not implement a WaitIndicator. One will be shown on initialization and on saving by the container.
## Create your ViewModel
The viewmodel does not need to implement anything specific aside from `BindableBase`.
It should handle initialization and loading of settings in a asnyc method called from the view. Nothing should be loaded via the ctor.
Saving should be done with another async method that is called from the view.
If there is an error during loading or saving you need to throw an exception. The container will handle it and show a error dialog. **Do NOT display a messagebox yourself**.
Of course all other c-entron<span>.NET conventions apply ;)
## Register your Controller
If your settings applies to a specific module it needs to be registered in that modules `ICentronAppModuleController.GetSettings()`.
If it does not belong to a specific module it needs to be registered in `ModuleRegistration.GetSettingsWithoutModule()`.
If your settings page is a personal settings it needs to be registered in `ModuleRegistration.GetPersonalSettings()`.
**If the specific module is not registered in the moduleslist you need to register your settings in both `ICentronAppModuleController.GetSettings()` in your module and `ModuleRegistration.GetSettingsWithoutModule()`.**
## Examples
```
Centron.WPF.UI/Modules/Administration/CentronConfigDb/CentronConfigDbSettingsController.cs
Centron.WPF.UI/Modules/Helpdesk/Settings/TypesHelpdeskTypeSettingsControllers.cs
Centron.WPF.UI/Modules/MyCentron/PersonalSettings/UI/PersonalUISettingsController.cs
```
@@ -0,0 +1,280 @@
# Localization
## General structure
We use the .NET default approach for the localization. Each DLL which needs localized strings, adds the necessary resource files.
The used language is determined by CultureInfo.CurrentUICulture.
## Resource Files Structure
The application has separate resource files for different layers and supports multiple languages:
**WPF UI Layer:**
- **German (default)**: `src/centron/Centron.WPF.UI/Resources/LocalizedStrings.resx`
- **English**: `src/centron/Centron.WPF.UI/Resources/LocalizedStrings.en.resx`
**Business Logic Layer:**
- **German (default)**: `src/backend/Centron.BL/Resources/LocalizedStrings.resx`
- **English**: `src/backend/Centron.BL/Resources/LocalizedStrings.en.resx`
### Language Support
- **Default Language**: German - stored in the base resource files (`LocalizedStrings.resx`)
- **Additional Languages**: English - stored in language-specific resource files (`LocalizedStrings.en.resx`)
- **Language Selection**: Determined by `CultureInfo.CurrentUICulture`
Each resource file contains the same keys but with text in the respective language. When creating new localized strings, you must provide translations for both German and English.
**Example of the same key in different languages:**
```
German (LocalizedStrings.resx):
LoginDialogView_Benutzername = "Benutzername"
English (LocalizedStrings.en.resx):
LoginDialogView_Benutzername = "Username"
```
## Usage Examples
### XAML Usage
In XAML files, localized strings are accessed using static property syntax with the appropriate namespace declaration.
**Namespace Declaration:**
Add this to your XAML file's root element:
```xml
xmlns:properties="clr-namespace:CentronSoftware.Centron.WPF.UI.Resources"
```
**Usage in XAML:**
```xml
<!-- For text properties -->
<Label Content="{x:Static properties:LocalizedStrings.LoginDialogView_Benutzername}" />
<!-- For placeholder text -->
<TextEdit NullText="{x:Static properties:LocalizedStrings.LoginDialogView_Benutzername}" />
<!-- For tooltip text -->
<Button ToolTip="{x:Static properties:LocalizedStrings.SomeTooltipKey}" />
```
**Real Example from LoginDialogView.xaml:**
```xml
<dxe:TextEdit Name="UsernamTextEdit"
ShowNullTextForEmptyValue="True"
NullText="{x:Static properties:LocalizedStrings.LoginDialogView_Benutzername}"
Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" />
```
### Code-Behind Usage (WPF UI)
In C# code-behind files in the WPF UI project, add the using statement and access strings directly.
**Using Statement:**
```csharp
using CentronSoftware.Centron.WPF.UI.Resources;
```
**Usage in Code:**
```csharp
// For status messages
using (CentronApplication.Instance.AppStatusBar.PushMessage(
LocalizedStrings.LoginDialogViewModel_DoLogin_Anmeldung))
{
// Your code here
}
// For exception messages
throw new Exception(LocalizedStrings.SomeErrorMessageKey);
// For dialog messages
var message = LocalizedStrings.ConfirmationMessageKey;
```
**Real Example from LoginDialogViewModel.cs:**
```csharp
using (CentronApplication.Instance.AppStatusBar.PushMessage(
LocalizedStrings.LoginDialogViewModel_DoLogin_Anmeldung))
{
var preResult = await PreDoLoginToCentron();
// ... rest of login logic
}
```
### Business Logic Usage
In business logic classes, localized strings are accessed similarly but using the business logic resource namespace.
**Using Statement:**
```csharp
using Centron.BusinessLogic.Resources;
```
**Usage in Business Logic:**
```csharp
// For exception messages
throw new ResultException(
LocalizedStrings.AppSettingsGroupBL_UpdateUpdateAvailableNotificationSettings_WählenSieMitarbeiterAusDieBeiVerfügbarenUpdatesEineBenachrichtigungAngezeigtBekommenSollen,
DefaultMessageCodes.BadRequest);
// For validation messages
var errorMessage = LocalizedStrings.ValidationErrorKey;
```
**Real Example from AppSettingsGroupBL.cs:**
```csharp
throw new ResultException(
LocalizedStrings.AppSettingsGroupBL_UpdateUpdateAvailableNotificationSettings_WählenSieMitarbeiterAusDieBeiVerfügbarenUpdatesEineBenachrichtigungAngezeigtBekommenSollen,
DefaultMessageCodes.BadRequest);
```
## Common WPF Properties Requiring Localization
When auditing XAML files for hardcoded text that needs localization, pay special attention to these classic WPF properties that commonly contain user-facing text:
### Primary Text Properties
- **`Text`** - Direct text content (TextBlock, Run, etc.)
- **`Content`** - Button content, CheckBox content, etc.
- **`Label`** - LayoutItem labels, form field labels
- **`Header`** - Headers in controls like TabItem, TreeViewItem, **GridColumn headers**
- **`Title`** - Window titles, ToolTip titles
- **`Caption`** - Ribbon page captions, group captions
### Input Control Properties
- **`NullText`** - Placeholder text in DevExpress editors
- **`Watermark`** - Placeholder text in standard controls
- **`ToolTip`** - Tooltip text for controls **and images in DataTemplates**
### Converter and Template Values
- **Converter Target Values** - Hardcoded strings in ObjectToObjectConverter mappings
- **DataTemplate Content** - Text within data templates, **including ToolTip properties on images and controls**
- **Style Setter Values** - Hardcoded text in style setters
### Grid Controls and Data Tables
- **`Header`** - GridColumn header text that displays column titles
- **Runtime Localization Strings** - Grid control runtime strings like "NoRecords" messages
- **DataTemplate ToolTips** - Tooltip text on images, buttons, and other controls within cell templates
- **Custom Grid Messages** - Any hardcoded text in grid customization areas
### Example Search Patterns
When auditing XAML files, search for these patterns:
```xml
Title="German Text"
Content="German Text"
Label="German Text"
Text="German Text"
Caption="German Text"
Header="German Text"
NullText="German Text"
Target="German Text"
ToolTip="German Text"
Value="German Text"
```
### Grid-Specific Examples
Pay special attention to grid controls:
```xml
<!-- GridColumn headers -->
<dxg:GridColumn Header="Bezeichnung" />
<dxg:GridColumn Header="Standard" />
<!-- DataTemplate tooltips -->
<Image ToolTip="Änderungen vorhanden" />
<!-- Runtime localization strings -->
<dxg:RuntimeStringIdInfo Value="Keine Verbindungen vorhanden." />
```
## Key Naming Conventions
Localization keys follow a structured naming pattern to ensure uniqueness and maintainability:
- **Format**: `[ClassName]_[MethodName]_[Description]` or `[ViewName]_[PropertyName]_[Description]`
- **Examples**:
- `LoginDialogView_Benutzername` - Username field in LoginDialogView
- `LoginDialogViewModel_DoLogin_Anmeldung` - Login process message
- `AppSettingsGroupBL_UpdateUpdateAvailableNotificationSettings_WählenSieMitarbeiterAusDieBeiVerfügbarenUpdatesEineBenachrichtigungAngezeigtBekommenSollen` - Business logic validation message
## Tools
#### ResXManager
Great tool for managing the language resource files. It is a visual studio extension. It provides a dialog too manage the resource and translate the resources.
It also adds a right click menu item "Move to Resource" for quick resource entry creation.
## Working
If we encounter strings which are localized through the old workflow (CentronLocalization.Instance.GetLocalizedString()) we replace it with the new workflow.
### Translate strings in code
1. Click into the string. (Complete string selection is not nessassry. The VS-Extension extends the selection automaticly. Be careful if your string contains ". In this case check if the complete text got selected. If not, select manualy.)
2. Right Click.
3. Select "Move to Resource".
4. The popup "Move to Resource" appears.
- For better read ability select at "Code" the format which adds the class and method name too the generated code.
- If the string contains variables {0}, {1} add a "Comment" for them. So we have a info in the resource file what values can be expected in the variables.
- After a klick on "OK" the extension creates a new entry in the resource file and replaces the string with the generated code property.
#### Namespaces
.xaml: `xmlns:properties="clr-namespace:CentronSoftware.Centron.WPF.UI.Resources"`
.cs: `using CentronSoftware.Centron.WPF.UI.Resources;`
#### Useful searches
These can be used by using the inbuilt search function: Open 'Find and Replace', disable 'Match case' and 'Match whole word', enable 'Use regular expressions' then select 'Currrent document' and 'Entire document'. Now you can just go through the file using 'Find Next' and you should catch most strings.
.cs:
`"[^"]*?"` - finds all strings
`(?=(^((?!///).)*$)).*((".+?")|('.+?')).*` - finds all strings excluding comments
`(LocalizationManager|CentronLocalization)\.Instance\.GetLocalizedString\("[0-9]*", "[^"]*?"\);` - finds texts already translated by the old system
.xaml:
`(Text|Label|Caption|ToolTip|Content|Header)="[^{]*?"` - finds most common string literals
`{lex:Loc[^"]*?"` - finds texts already translated by the old system
#### Keybindings
1. Open `Tools`>`Options`>`Keyboard`
2. Search for `EditorContextMenus.CodeWindow.Movetoresource`
3. Bind to the keys you like (don't foregt to press Assign)
4. Profit
### Translate entries in the resource file
1. To translate a resource entry, navigate to the Resource directory in your assembly.
2. Locate the file "LocalizedStrings.resx".
3. Right-Click it and select "Open in ResX Manager".
If the Resource Manager shows no files, click on "Refresh view".
- The manager highlights the not translated entries with a red background color.
- Additionaly this Tool contains a own tab for the translation. It shows the entries which are not translated. There is a auto translate function too.
## Setup localization for a assembly
1. Create a "Resources" directory, if not existing.
2. Add "New Item"
3. Select "Resources File"
4. Use as Name: LocalizedStrings.resx
5. Press "Add"-Button
6. If visual studio does open the resource file automaticly, close it.
7. Select the new file in the Solution Explorer.
8. Check The properties.
- Build Action: Embedded resource
- Custom Tool: ResXFileCodeGenerator. Enter PublicResXFileCodeGenerator if the assembly contains XAML UI.
9. Open "ResX Manager" through Tools (Visual Studio menu)
10. Click on "Refresh View", the new resource file should be visibly now.
11. Close the manager.
12. Translate a string in the source code.
13. Open the ResX Manager again.
14. If not selected, select the new resource file.
15. Because we have no resource files for the other languages yet, double click into the edit box and add a translation for the language. The tool will create a new resource file for the language.
16. Done. We can close the manager.
Its possible that the Resource Manager does not show the new created langugage files from the start. In this case press Refresh. This should fix the issue.
## Get localized strings through Web-Service call
To get the message in a specific langugage, it is necessary too add a new Attribut too the header "Accept-Language" (Value example: de-DE).
Example:
httpRequest.Headers.Add("Accept-Language", CultureInfo.CurrentUICulture.Name);
@@ -0,0 +1,208 @@
# High level overview
There are a couple of different pieces involved in creating a build.
The most central piece is the **Centron.Scripts** project in this repository, it does most of the heavy work.
There is also the **azure-pipelines.yml** that specifies how Azure DevOps works with the Centron.Scripts project, and what to do with the resulting files.
The builds will be placed in our [software builds sharepoint](https://centrongmbh.sharepoint.com/sites/SoftwareBuilds/Shared%20Documents/Forms/AllItems.aspx).
If the build references one or multiple **c-entron Tickets**, these Tickets will automatically get the version number of that build, and the ticket will be forwarded to the quality-control department.
There is also a **downloader** that automatically downloads the latest versions of c-entron.NET and Web-Service from the sharepoint to our own internal network shares. This makes it easier for everyone working at NEXOWARE to access the latest versions.
We have two build servers:
**CS-UL-DEVOPS01** (hosted by celos) that has currently 3 build agents, and
**CS-UL-DEVOPS02** (hosted at celos) that has currently 3 build agents.
All of the users, passwords and other credentials that I talk about in this document are also available [in our password server]( https://83.169.12.81:10001/Account/SignIn).
# Centron.Scripts
The **Centron.Scripts project** knows how to build the c-entron.NET, the c-entron Web-Service, and also the installers for all of them.
It does a handfull of things:
- It creates the **version number**
- It **builds** the projects using dotnet.exe
- It **deletes unneeded files** from the build output
- It **signs** the applications and installers
- It **creates archives** of everything and moves them into the *[git repository root]/artifacts* directory
- It **runs the tests** and places the test result in the *[git repository root]/artifacts* directory
So when you run this project, after a couple of minutes you will end up with perfect installers for c-entron.NET and c-entron Web-Services without any more input required from you.
Also, you will get a result file of all the tests.
## Code signing
Code signing is optional - you can create a build without having our code signing certificate and password - it is of course still be recommended to do it.
You can activate code signing by setting two environment variables:
**CENTRON_BUILD_CODE_SIGNING_CERTIFICATE** and
**CENTRON_BUILD_CODE_SIGNING_CERTIFICATE_PASSWORD**
**CENTRON_BUILD_CODE_SIGNING_CERTIFICATE** should contain the path to the code signing certificate file.
**CENTRON_BUILD_CODE_SIGNING_CERTIFICATE_PASSWORD** should contain the password for that code signing certificate.
Right now, all of the following files for c-entron.NET are signed:
- c-entron 2.0.exe
- c-entron.NET Installer.msi
And these for the c-entron Web-Service:
- Centron.Host.WindowsService.exe
- c-entron Connection Manager.exe
- Centron.Interfaces.dll
- Centron.WebServices.Core.dll
- Centron.Core.dll
- c-entron Web-Service Installer.msi
If one of these environment variables is empty or not set, the applications and installers will not be signed at all.
You can read in the console output whether the files were signed or not.
## Automatic versioning
There is a **version.json** file in the root directory of the git repository.
This version specifies the first 3 parts of the version number, for example **2.0.2601**.
The last part will be automatically generated by using [Nerdbank.GitVersioning](https://github.com/AArnott/Nerdbank.GitVersioning).
Nerdbank.GitVersioning is a very feature rich library to automatically generate version numbers for your git repository, but we only use it in a very simplistic simple way.
It basically counts how many commits there have been in the repository, since the **version.json** file was last changed.
That count is the last part of the version number.
**For example:**
We change the version number in **version.txt** to **2.0.1911**, and then have 2 commits, the version number will be **2.0.1911.3**.
**Note:** It will be *3* and not *2* because it also counts the commit where the version.txt was changed.
This means that the version number is always increasing automatically.
# azure-pipelines.yml
The azure-pipelines.yml file contains a couple of tasks, some for **building** the project, others for **deploying** it.
## Tasks
So the tasks work roughly like this:
- **Authenticate** with the Azure DevOps **NuGet server for c-entron Office**
- It **downloads** our **code-signing certificate** from Azure DevOps (it's stored in `Azure DevOps` -> `c-entron.NET` -> `Pipelines` -> `Library` -> `Secure files` -> `c-entron code signing certificate.pfx`)
- It **updates** the **Azure DevOps version number** to show the version number of the build
- It then **runs** the **Centron.Scripts** and also sets the environment variables for code signing to work (by using the certificate file downloaded earlier, and the password that is added as a secure variable to the build pipeline)
- After the build succeeded, it will **upload all the created installer archives** (c-entron.NET Installer and c-entron Web-Service Installer) to our [Software Builds sharepoint documents](https://centrongmbh.sharepoint.com/sites/SoftwareBuilds/Shared%20Documents/Forms/AllItems.aspx).
They will be uploaded to "/`c-entron.NET` or `c-entron Web-Service`/v`first three parts of the version number`/v`complete version number`/`artifact name`".
So for example:
**/c-entron.NET/v2.0.1908/v2.0.1908.3/c-entron.NET Installer.zip** and
**/c-entron Web-Service/v2.0.1908/v2.0.1908.3/c-entron Web-Service Installer.zip**
**Note:** The upload to the [Software Builds sharepoint documents](https://centrongmbh.sharepoint.com/sites/SoftwareBuilds/Shared%20Documents/Forms/AllItems.aspx) is **only done for real builds**, it is **not done for pull request builds**.
# c-entron Tickets
If a **real build** was created, then c-entron Tickets that have been fixed or resolved by that **real build** will get the version number of that build, and also will be forwarded to our quality-control department.
This works by using so called [Web Hooks from Azure DevOps](https://docs.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops). In our case: When a build finishes, a HTTP API will be triggered.
This HTTP API will then figure out which pull request was completed for that build, and whether this pull request references any c-entron Tickets.
To figure out which pull request was completed by the build, it calls the Azure DevOps API using an **API key** generated by the **build@c-entron.de** user.
It then searches for ticket numbers in the title and description of the pull request - the following formats are currently supported:
* Ticket 12345
* Ticket#12345
* Ticket #12345
* Ticket# 12345
* Ticket: 12345
When one or more ticket numbers were found, it will call our own c-entron API (https://c-suite.c-entron.de/CentronServiceProductive) to insert the build version into the ticket, and then forward it to the quality-control department (Personal I3D 372).
To call our own c-entron API the **sw_centron** AD user is used - that matches to the **TFS** user in our own c-entron database.
This user also has the email address **build@c-entron.de** to send out ticket forwarding emails.
It is possible to skip the forwarding by adding one of the following tags to the description of the PR:
* [skip-forwarding]
* [skip-fwd]
* [skip-forward]
If this is done it will only insert the build version into the ticket, but it does not forward the ticket. This allows you to link several PRs without the ticket getting send to QS before it is finished.
The HTTP API is currently running on our own web-server **CS-UL-ERPWEB01**, available at **https://erp.c-entron.de/DevOpsCentronTicketBridge** - you can enter this URL in the browser and should see a short message that the bridge is running.
It is running as the windows service **DevOpsCentronTicketBridge**, located at *C:\Program Files (x86)\DevOpsCentronTicketBridge*.
Basically everything about it can be configured in the **appsettings.json** file:
* The URL where it is available
* The Personal Access Token used to communicate with the Azure DevOps API
* The regular expressions that are used to find the ticket numbers in the title and description of the pull requests
These regular expressions should have **exactly one group** *(the round brackets)* where the number will be located.
For example: **Ticket ([0-9]{5,6})** or **Ticket#([0-9]{5,6})**
* Where the c-entron Web-Service is running at, and which user to login with
* To which user the tickets should be forwarded
The whole source-code of this web hook is available on our [Azure DevOps server too](https://dev.azure.com/CentronSoftware/DevOpsCentronTicketBridge).
# Azure DevOps setup
There are a couple of things configured in Azure DevOps to make this all work:
- The **azure-pipelines.yml** build pipelines is configured as a **branch policy** for the **master branch** and all release-branches (**release/vXYZ**) with trigger **automatic**.
This means the pipeline will be automatically executed for pull requests.
- There is a **secure variable** configured for the build pipeline called **codeSigningCertificatePassword**
- There is the code signing certificate added as a **secure file** called **c-entron code signing certificate.pfx**
- In `Project settings` -> `Service connections` is a connection for **Software Builds - Sharepoint** - it uses the **build@c-entron.de** user to authenticate
- In `Project settings` -> `Service hooks` is a **web hook** created to make the c-entron Ticket workflow possible.
It has a trigger of type **Build completed** and a couple filters:
Build pipeline = **c-entron.NET CI**
Build Status = **Succeeded**
It performs a **HTTP POST** to **https://erp.c-entron.de/DevOpsCentronTicketBridge** with:
Resource details to send = **All**
Resource version = **1.0**
# Azure DevOps Build Agents
We have two build-servers called **CS-UL-DEVOPS01** and **CS-UL-DEVOPS02**.
**CS-UL-DEVOPS01** is accessible with its full name **CS-UL-DEVOPS01.group.celos.de**
I have my own local admin account there **LocalAdminDH**, but _Celos Computer GmbH_ can add real accounts based on our windows accounts.
Stanislaus Lieb also can access this server.
There are currently **3 build agents installed** on that machine:
- E:/AzureDevOpsAgent1 called **CS-UL-DEVOPS01.1** and
- E:/AzureDevOpsAgent2 called **CS-UL-DEVOPS01.2** and
- E:/AzureDevOpsAgent3 called **CS-UL-DEVOPS01.3**
**CS-UL-DEVOPS02** is accessibly with its full name **CS-UL-DEVOPS02.group.celos.de**
Login same as **CS-UL-DEVOPS01**
There are currently **3 build agents installed** on that machine:
- E:/AzureDevOpsAgent1 called **CS-UL-DEVOPS02.1** and
- E:/AzureDevOpsAgent2 called **CS-UL-DEVOPS02.2** and
- E:/AzureDevOpsAgent3 called **CS-UL-DEVOPS03.3**
These build agents **connect** to Azure DevOps using **API keys** that were generated by the **build@c-entron.de** user.
All of them are configured to run as a **windows service** using the local user **AzureDevOpsAgentUser**.
This user is also an administrator on the machine (it has to be, otherwise the **deploy tasks** will fail).
We have a couple of **software requirements** for the **build server**:
- Visual Studio 2022 (authenticated with the **build@c-entron.de** user)
- Make sure to install all .NET Framework targeting packs, so building c-entron.NET will work correctly
- Install these workloads:
- ASP.NET and web development
- .NET Desktop development
- WIX 3 Toolset build tools (see [here](https://wixtoolset.org/docs/wix3/))
- WIX 3 Toolset Visual Studio 2022 Extension (also see [here](https://wixtoolset.org/docs/wix3/))
- .NET Core SDK 8.0.x (the latest one, see global.json) (get it [here](https://dotnet.microsoft.com/download))
- .NET Core SDK 7.0.3x (for c-entron Office) (get it [here](https://dotnet.microsoft.com/download))
- .NET Core SDK 6.0.1x (for Outlook Add-In) (get it [here](https://dotnet.microsoft.com/download))
- An SQL-Server for our end-to-end tests (MSSQL 2017 prefered right now)
- Make sure to set the environment-variables so the end-to-end tests can actually find the database. You can find the environment-variable names in the Database.cs file.
- As we are using the Azure DevOps task `razorspoint.rp-build-release-pnptasks.RP-PnPPowerShell.PnPPowerShell@3` to upload the finished versions to sharepoint, we have to make sure the SharePoint upload works correctly too.
Try running `Find-Module -Name "SharePointPnPPowerShellOnline" -RequiredVersion "3.23.2007.1"` on the build server.
If it works, you should be good to go. If it doesn't we might have to add NuGet as a PowerShell PackageProvider.
To do that, execute this command `Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force`
# Automatic downloader
We have an automatic downloader that downloads the latest versions of c-entron.NET and c-entron Web-Service every day at around 6 AM.
This downloader is running on the server **CS-UL-CENTRON** as the windows service **SharePointVersionDownloader**.
It is running as the user **svc_SoftwareCentron@group.celos.de** because the process needs write permissions to the network shares where the downloaded files will be placed.
On that server the downloader is located at _C:\Program Files (x86)\SharePointVersionDownloader_.
The downloader can be configured completely through the **appsettings.json** file.
If you want to download, for example, the latest 2.0.1908 version of c-entron.NET, then you have to add a job to the **appsettings.json** for that.
Another job will be required for the latest 2.0.1909 version, or for c-entron Web-Service versions.
For examples on how to configure these jobs, take a look at the current **appsettings.json** configuration, or even better, the source-code.
The source-code for this downloader is available on our [Azure DevOps server](https://dev.azure.com/CentronSoftware/SharePointVersionDownloader).
@@ -0,0 +1,47 @@
# How to release stop
## Bump the version.txt
Creating a release stop is pretty simple.
First, you have to bump the version number in the **version.txt** file.
For example, it's **currently at 2.0.1908**, and we would like to have a **release stop** for this **2.0.1908** release.
So what you do is you go ahead and **change it to 2.0.1909**.
Of course still use the usual workflow of separate branch, pull request, and merge.
After the change is merged into the `master` branch, every next version created, will have a **2.0.1909** version number.
## Create the release branch
Now you go ahead and **create a `release` branch based on the commit before the version bump.**
For example, the `master` branch after merging the version bump looks like this:
```
* <- Merged PR ABC: Version Bump [Current state of master, will create build 2.0.1909.2]
|\
| \
| * <- Version Bump to 2.0.1909 in the pull request branch [Commit in the PR]
| /
|/
* <- Merged PR XYZ: Some other thing [Commit before the version bump]
|\
| \
other commits...
```
Create a new branch named `release/v2.0.1908` based on `Merged PR XYZ: Some other thing`.
Push this branch to the Azure DevOps server, and then we have to configure some **branch policies**.
## Branch policies
The branch policies are setup automatically for all branches starting with `release/`, so there is nothing you have to do for the branch policies.
It is already taken care of.
## Automatic downloader
Now that the new version **2.0.1909** will be created, consider adding it to the **automatic downloader**.
This will allow everyone working at NEXOWARE to more easily get the latest builds of that version.
How to do that, and many more information on the automatic downloader are available in the [our build server and automated builds](./Our-build-server-and-automated-builds.md#automatic-downloader) page.
@@ -0,0 +1,136 @@
# How to update DevExpress in c-entron<span>.NET
Before your start, **please** talk to everyone who manages one of the other projects (Outlook Add-In, Service-Board, Virtual Mail Assistant, c-entron Agent, etc.) to make sure they can all update as well.
We have documentation for the other projects (even if they have their own wikis) here to consolidate the update process.
Projects that do not have documentation here are usually updated by their respective developers (ServiceBoard etc.) or have documentation in their git repo.
This'll take you a few hours to complete and you cannot just do it halfway (leaving projects unable to update their c-entron dlls), so make sure to leave enough time and preferably start in the morning.
*There __will__ be issues along the way, so be prepared!*
**Please note: this is a very delicate process and needs to be done very thoroughly. You cannot resonably test every project/ui to ensure it's working, so you'll have to make sure you do thing correctly.**
**Errors or missing updates will probably only be noticed once people install (even worse if its only noticed once released to customers) and will usually result in bad/cryptic error messages.**
**Always check all the changed files in your PRs, make sure everything that needs to be updated is updated and everything that doesnt need to be updated is not updated!**
## Branches & PRs
For consistency reasons it is advisable to use the same pattern of branchname, commitmessage and PR name everywhere.
where | pattern | example (for Version 22.1.3)
--|--|--
Branch | \<shortsign>/update_devexpress_to_\<versionnumber> | hay/update_devexpress_to_2213
Commit | Update devexpress to \<versionnumber> | Update devexpress to 22.1.3
PR | same as the commit message
# [c-entron.Help](https://dev.azure.com/CentronSoftware/c-entron%20Help)
`Solution` > `ContextMenu` > `Manage NuGet Packages for Solution...` to update the NuGet packages.
Package|Projects
--|--
`DevExpress.*` | `Centron.Help`
Update the version number for `Centron.Help` in the `.csproj` file. Put your changes into a PR and self approve and complete it.
Copy the nuget-package to `/nugets` in c-entron<span>.NET and c-entron Outlook AddIn solution.
[Reference PR](https://dev.azure.com/CentronSoftware/c-entron%20Help/_git/c-entron%20Help/pullrequest/12178)
# [c-entron<span>.NET](https://dev.azure.com/CentronSoftware/c-entron.NET)
There are two ways for you to do this, the offical way via nugget packages and the fast way.
Package|Projects
--|--
`DevExpress.*` | WPF, backend, webservice, shared **and Nexus (Blazor)** — all driven by the `NugetVersionDevExpress` variable (see below), you don't need to touch the individual `.csproj` files
`Centron.Help` | `Centron.WPF.UI`
## Updating DevExpress dlls (single source)
The DevExpress version lives in **one** file: **`DevExpress.Version.props`** (repo root), which holds `<NugetVersionDevExpress>`. It is imported by both the root `Directory.Build.props` (WPF/backend/webservice/shared) and `src/nexus/Directory.Build.props` (Nexus/Blazor). Just change the value there — every `DevExpress.*` `PackageReference` uses `Version="$(NugetVersionDevExpress)"`.
> History: Nexus used to hard-code its 6 DevExpress versions in `src/nexus/CentronNexus/CentronNexus.csproj`. Those were switched to `$(NugetVersionDevExpress)` and Nexus's own `Directory.Build.props` imports the shared file (it does not inherit the root props), so a version change is now a single edit.
## Obtaining & updating c-entron.Help (do this FIRST — it is a hard blocker)
`Centron.WPF.UI` references the `Centron.Help` NuGet, and that package is compiled against a specific DevExpress version. If it lags behind, the WPF **markup compiler fails** with `MC1000 … Could not find assembly 'DevExpress.Data.v<OLD>'` — because the new DevExpress ships `DevExpress.Data.v<NEW>` and the old versioned assembly no longer exists.
So **ask the c-entron.Help maintainer for a new `Centron.Help.1.0.xxxx.x.nupkg` built against the new DevExpress version** (see the c-entron.Help section above / its own repo). Then:
1. Copy the new `Centron.Help.1.0.xxxx.x.nupkg` into `/nugets` and delete the old one.
2. Bump the `Centron.Help` `PackageReference` `Version` in `src/centron/Centron.WPF.UI/Centron.WPF.UI.csproj`.
3. Clear the old extracted package from the NuGet cache if the version number was reused.
## Running tests
Some EndToEnd expected files contain RTF text with the DevExpress version that generated it, so they must be updated. This is **not just 2 tests** — there are roughly **20+** `*.expected.txt` files under `tests/Centron.Tests.EndToEnd/Tests/Settings/` and `tests/Centron.Tests.EndToEnd/Tests/Helpdesk/`.
You can either run the tests (`HelpdeskSettingsTests` / `SettingsTests`) so they regenerate, or bulk-replace the generator string across all files:
`..{\*\generator DevExpress Office File API/<OLD>.0}..`
to
`..{\*\generator DevExpress Office File API/<NEW>.0}..`
(e.g. old `25.2.6.0` → new `26.1.3.0`.) These files are UTF-8 **without BOM** and use **CRLF** — preserve both (use `perl -i -pe`, not an editor that rewrites line endings). Ignore the unrelated `9.2.x` strings (that is the c-entron ApplicationVersion) and any `*.actual.txt` (generated, gitignored).
## Breaking changes on major upgrades (e.g. 25.x → 26.x)
A minor bump is usually just the version + heatmaps. A **major** bump additionally tends to need:
- **Clear stale WPF build artifacts.** Delete `**/*_wpftmp.csproj` and the `obj`/`bin` folders of the WPF projects (`Centron.WPF.UI`, `Centron.WPF.UI.Extension`, `Centron.Controls`, `c-entron.misc.ConnectionManager`). They cache the old `DevExpress.*.v<OLD>` reference paths and keep reproducing the `MC1000` error even after the version bump.
- **Removed/changed API.** Fix compile errors from removed members. Example (26.1): `services.AddDevExpressBlazor(o => o.BootstrapVersion = BootstrapVersion.v5)` — `BootstrapVersion` was removed (Bootstrap 4 dropped, v5 is default) → use plain `services.AddDevExpressBlazor();` in `src/nexus/CentronNexus.Host/Program.cs`.
- **Version-hashed static assets.** DevExpress 26.1 no longer ships the internal `_content/DevExpress.Blazor/dx-blazor-<hash>.svg` sprite. Any custom `<use href="…#dx-editor-remove-tag">` (e.g. in `EmployeeTreeSelectionDropDown.razor`) breaks — replace with a self-contained inline SVG.
- Re-verify `wwwroot/css/devexpress-mods.css` overrides and any DevExpress `.Internal` namespace usages.
- **Restore first** (`dotnet restore Centron.sln`): a failed restore means the DevExpress feed/subscription does not cover the new (major) version — stop and clear that before anything else.
## Updating the heatmaps
The build might fail now, due to the installers not knowing about the new DevExpress DLLs.
To fix this, execute the following command line in the root of the git directory:
`dotnet run --project .\scripts\Centron.Scripts\Centron.Scripts.csproj -- update-installer-product-heat`
This will update the `CentronProductHeat.wxs` and the `WebServiceProductHeat.wxs` (once for c-entron WS) to include the new missing files.
*If only the minor version changes the heatmaps might not need an update.*
## Cleanup & PR
Updating the heatmaps re-touches the `Directory.Build.props` file (the `setup-versioning` step rewrites `<Version>` and `<GitCommitId>`). **Revert those two back** (`<Version>1.0.0.0</Version>`, empty `<GitCommitId>`). The DevExpress version itself now lives in `DevExpress.Version.props`, not here, so it is unaffected.
Now you can create the PR and wait for it to merge. Due to the changes to `.sln` and `.csproj` files, it'll need someone from the code-reviewer group.
[Reference PR](https://dev.azure.com/CentronSoftware/c-entron.NET/_git/c-entron.NET/pullrequest/12182)
# [c-entron Outlook AddIn](https://dev.azure.com/CentronSoftware/c-entron%20Outlook%20Add-In)
This only needs to be done in coordination with the maintainer of the Addin.
First you need to update the centron.Help nuget package, then update the packages themselves.
Package|Projects
--|--
`DevExpress.*` | `Centron.OutlookAddin` & `Centron.Tickets`
`Centron.Help` | `Centron.OutlookAddin` & `Centron.Tickets`
`Centron.Controls` | `Centron.OutlookAddin` & `Centron.Tickets`
As before you also need to update the product heat. (reference c-entron<span>.NET)
**Make sure to run the terminal as administrator, otherwise the build _will_ fail!**
`dotnet run --project .\scripts\Centron.OutlookAddin.Scripts\Centron.OutlookAddin.Scripts.csproj -- update-installer-product-heat`
[Reference PR](https://dev.azure.com/CentronSoftware/c-entron%20Outlook%20Add-In/_git/c-entron%20Outlook%20Add-In/pullrequest/9193?path=/src/Centron.Tickets/Centron.Tickets.csproj)
# Finishing touches
After you're done you should send a notification e-mail to all developers so they are aware of the update and can (if needed) update their own projects.
*If you notice any changes to this process, then please update this documentation.*
# Further reading
## Error uploading nuget packages
### Error
`##[error]The nuget command failed with exit code(1) and error(System.AggregateException: One or more errors occurred. ---> System.Net.Http.HttpRequestException: Response status code does not indicate success: 403 (Forbidden - User 'ed8980f1-8a8a-4b5f-a522-5ca37b713bec' lacks permission to complete this action. You need to have 'AddPackage'. (DevOps Activity ID: 36B90B94-83BC-46CE-9349-5A4F110417E5)).`
### Fix
Ensure the nuget version in the build pipeline via the `NuGetToolInstaller@1` command. [Microsoft documentation](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/tool/nuget?view=azure-devops)
If you get this error and have no idea what it means go talk to the grandmasters of the build pipelines and show them this
@@ -0,0 +1,154 @@
# On DTOs and Entities
In our c-entron<span>.NET solution we use three different types of objects that hold actual data:
type | layer | usage
---|---|---
Entity | BL/WebServiceBL | interaction with the database via NHibernate
DTO | WebService/Logics |transfer from entity to viewmodel via webservice
ViewModels | ViewModel/UI|display data to the user and allow user to edit it
*Because ViewModels are pretty straight forward and quite boring we're just going to ignore them here.*
We're going to use the `Thingy` class for examples here again. For a real implementation check out MyDayWorkItem and associated classes.
## Entities
On the most basic level entities are just a row in a database table managed by our ORM [NHibernate](https://nhibernate.info/). If we have a `Thingy` there is a corresponding row in the `Thingies` table, where the **primary key equals** the **I3D**. Entities can never leave the BL-Layer as they cannot be used in the webservice and need a connection to NHibernate and the DB itself. CRUD-Operations are done via `Session.GetGenericDAO\<T>().SaveOrUpdate/Get/Delete` and can only be executed with a valid entity.`
As an entity is simply for holding data it should never include any logic, no overrides and not even a ctor. To avoid confusion all properties present in an entity should also be mapped to a column in the database.
An entitiy needs 2 classes:
The actual entity that holds all data and is used for all operations. It must inherit the `BaseEntity` abstract base class that adds the unique identifier (**I3D**) for us. The correct project for entities is `Centron.Entities` and then the `Entities` directory.
All properties must be declared **virtual** and have to have both a **setter** and a **getter**. Both things are required for the interaction with NHibernate.
``` csharp
public class Thingy : BaseEntity
{
public virtual string SomeProperty { get; set; }
public virtual int SomeOtherProperty { get; set; }
}
```
Aside from the actual entity we also need a mapping class from [Fluent NHibernate](https://github.com/FluentNHibernate/fluent-nhibernate). This class maps the properties to the database column and must inherit `ClassMap<T>`. Even tough NHibernate can figure a lot of the properties out itself, you should always set the table, the id and ALL properties. Mapped properties need to be described with `.Not`, `.Nullable()`, `.Lenght()` etc. as closely as possible. Check out ['Fluent NHibernate in a Nutshell'](https://github.com/FluentNHibernate/fluent-nhibernate/wiki/Getting-started) for more infos. The correct project for entities is `Centron.DAO` and then the `Mappings` directory.
``` csharp
public class ThingyMaps : ClassMap<Thingy>
{
public ThingyMaps
{
this.Table("Thingies");
this.Id(f => f.I3D);
this.Map(f => f.SomeProperty).Nullable();
this.Map(f => f.SomeOtherProperty).Not.Nullable();
}
}
```
## DTOs
>In the field of programming a data transfer object (DTO) is an object that carries data between processes. (https://en.wikipedia.org/wiki/Data_transfer_object)
For us DTOs do exactly that. They transfer the data from the entity on the bl-layer to the viewmodel in the UI over WebService or direct database connection. Our webservice exclusivly uses DTOs, so all other applications and the c-entron<span>.NET ILogics use them. The project for DTOs is `Centron.WebServices.Core` and then the wrongly named `Entities` directory.
Like `entities DTOs` should never hold any kind of logic, no overrides and no ctors. It can include properties not present in the entity but this should be only be done in very specific circumstances.
Constructing DTOs is very easy: just create a class `<yourclass>DTO` (`ThingyDTO` for us), inherit `BaseDTO` and copy all properties from the entity. Then add the `[DataContract]` attribute to your class and the `[DataMember]` attribute to each property.
There are certain things that need to be avoided.
Properties of type DateTime can be dangerous. The default value of DateTime in .NET is 01.01.0001 which cannot be parsed to json and *will* throw an exception. Especially if the DateTime property gets filled by the database, is used in multiple locations or by other applications. Using a nullable `DateTime?` is the easiest way around that.
List properties **must** use `List<T>`. Using `IList<T>` or other types of list (`IEnumerable<T>`, `ICollection<T>`) can lead to problems with serializing it into json. As this is not widely done in our DTOs please consider updating existing DTOs.
### KnownTypes
If your DTO inherits from a different class (NOT `BaseDTO`) you must add the type to `GetKnownTypes()` in `KnownTypes.cs`. Additionally each webservice method that uses that DTO also needs the `[ServiceKnownType(nameof(KnownTypes.GetKnownTypes), typeof(KnownTypes))]` attribute in `ICentronRestService`. Otherwise the webservice *will* throw an exception while parsing your DTO to json.
## Converting DTOs to Entities and vice versa
### DTO -> Entity
Converting from a DTO to an entity can be somewhat annoying and complicated.
**NEVER use the `ObjectMapper` for this.** *(Yes, I know, there are a lot of places that do)*
What we need to do is as follows:
1. check if the I3D of the `ThingyDTO` is 0.
\> yes: `new Thingy()`;
\> no: load the entity from the database (via the appopriate BL method)
2. take over all properties from the DTO to the entity by hand.
This is done to ensure that all entities that are already saved to the DB (I3D != 0) are unique and properly managed by NHibernate.
``` csharp
public Thingy ConvertThingyDTOToEntity(ThingyDTO dto)
{
Guard.NotNull(dto, nameof(dto));
Thingy entity;
if(dto.I3D != 0)
entity = new ThingyBL(this.Session).LoadThingy(dto.I3D);
else
entity = new Thingy();
entity.I3D = dto.I3D;
entity.SomeProperty = dto.SomeProperty;
[..]
return entity;
}
```
### Entity -> DTO
Converting an entity to a DTO is very simple as we can just use the `ObjectMapper.Map<TEntity, TDTO>()`.
To use the Objectmapper you should create a configuration file.
These files go under `Centron.Bl` -> `Webservice` -> `ObjectMapperConfiguration`.
If there is a fitting class already just add `this.CreateMap<Thingy, ThingyDTO>();`, else we need to create a new file.
``` csharp
public class ThingyConfiguration : Profile
{
protected ovveride void Configure()
{
this.CreateMap<Thingy, ThingyDTO>();
}
}
```
### Generating mapping and entities
The following SQL statements generate your mappings and entities for you.
**There could be errors inside, you need to manually verify them.**
``` SQL
SELECT 'Map(m => m.' + c.name + ').Column("' + c.name + '")' +
CASE WHEN ty.name = 'text' THEN '.Length(int.MaxValue)' ELSE '' END +
CASE WHEN ty.name = 'varchar' THEN '.Length(' + CONVERT(varchar(100), c.max_length) + ')' ELSE '' END +
CASE WHEN ty.name = 'nvarchar' THEN '.Length(' + CONVERT(varchar(100),c.max_length / 2) + ')' ELSE '' END +
CASE WHEN c.is_nullable = 1 THEN '.Nullable()' ELSE '' END + ';', c.max_length
FROM sys.all_columns c
INNER JOIN sys.tables t ON t.object_id = c.object_id
INNER JOIN sys.types ty ON ty.system_type_id = c.system_type_id
WHERE t.name = 'TableName' AND ty.name <> 'sysname' AND c.Name <> 'I3D'
SELECT 'public virutal ' +
CASE WHEN ty.name IN ('text', 'nvarchar', 'varchar') THEN 'string' ELSE '' END +
CASE WHEN ty.name = 'bit' THEN 'bool' ELSE '' END +
CASE WHEN ty.name = 'datetime' THEN 'DateTime' ELSE '' END +
CASE WHEN c.is_nullable = 1 AND ty.name = 'datetime' THEN 'DateTime' ELSE '' END +
CASE WHEN ty.name = 'float' THEN 'double' ELSE '' END +
CASE WHEN ty.name = 'int' THEN 'int' ELSE '' END +
CASE WHEN ty.name = 'uniqueidentifier' THEN 'Guid' ELSE '' END +
CASE WHEN ty.name = 'char' THEN 'char' ELSE '' END +
CASE WHEN c.is_nullable = 1 AND ty.name NOT IN ('text', 'nvarchar', 'varchar', 'datetime') THEN '?' ELSE '' END +
' ' + c.name + ' { get; set; }'
FROM sys.all_columns c
INNER JOIN sys.tables t ON t.object_id = c.object_id
INNER JOIN sys.types ty ON ty.system_type_id = c.system_type_id
WHERE t.name = 'TableName' AND ty.name <> 'sysname' AND c.Name <> 'I3D'
```
**In the mapping Length(0) should be .Length(int.MaxValue)**
**For the entities ? should be DateTime?**
@@ -0,0 +1,3 @@
# MVVM in centron
I don't know, but I would like to - please tell me.
@@ -0,0 +1,224 @@
# Results and Responses in c-entron.NET
This document explains the `Result` and `Response` classes in c-entron.NET, their purpose, and how they interact across the system layers. These classes form a critical part of the error handling and communication pattern throughout the application.
## Overview
In c-entron.NET, we use a standardized approach for operation results and API responses:
1. **`Result`** - Internal class used within the business logic layer to indicate operation success/failure
2. **`Response`** - Web service API class that translates `Result` objects to client-friendly responses
This pattern provides:
- Consistent error handling across all application layers
- Type-safe return values with status information
- Clean separation between internal logic and API responses
- Standardized way to include error messages and codes
## Result Class
### Location
`src/backend/Centron.Interfaces/Results/Result.cs`
### Purpose
The `Result` class represents the outcome of an operation in the business logic layer. It includes not just the success/failure status, but also contextual information like error messages and exception details.
### Structure
```csharp
public class Result
{
public string Message { get; protected set; }
public int? MessageCode { get; protected set; }
public Exception Error { get; protected set; }
public ResultStatus Status { get; protected set; }
// Factory methods and constructors...
}
```
### Status Values
The `Result` object can have one of the following statuses:
- **Success** - The operation completed successfully
- **Error** - The operation failed due to an error
- **Warning** - The operation completed but with warnings
### Factory Methods
The `Result` class uses factory methods (instead of constructors) for creating result objects:
```csharp
// Success results
Result.AsSuccess();
Result.AsSuccess("Operation completed successfully");
// Error results
Result.AsError("The operation failed", messageCode: 100);
Result.AsError("Invalid input", error: exception);
// Warning results
Result.AsWarning("Some fields could not be processed");
// From exceptions
Result.FromException("Failed to process request", exception);
Result.FromException(exception);
```
### Generic Version
There is also a generic version `Result<T>` that carries data along with the status:
```csharp
// Success with data
Result<Customer> customerResult = Result<Customer>.AsSuccess(customer);
// Error with no data
Result<Customer> errorResult = Result<Customer>.AsError("Customer not found");
```
## Response Class
### Location
`src/webservice/Centron.WebServices.Core/Messages/Response.cs`
### Purpose
The `Response` class serves as the API response format returned by web services to clients. It translates the internal `Result` objects into standardized API responses.
### Structure
```csharp
[DataContract]
public class Response
{
[DataMember]
public StatusCode Status { get; set; }
[DataMember]
public string Message { get; set; }
[DataMember]
public int MessageCode { get; set; }
// Factory methods...
}
[DataContract]
public class Response<T> : Response
{
[DataMember]
public List<T> Result { get; set; }
// Additional factory methods...
}
```
### Status Values
The `Response` object can have one of the following statuses:
- **Success** - The operation completed successfully
- **Failed** - The operation failed due to an error
Note that unlike `Result`, `Response` maps both `ResultStatus.Success` and `ResultStatus.Warning` to `StatusCode.Success`.
## Interaction Between Result and Response
The key interaction occurs through the `FromBLResult` methods in the `Response` class:
```csharp
// Converting a Result to Response
Response response = Response.FromResult(result);
// Converting a Result<T> to Response<T>
Response<Customer> response = Response<Customer>.FromBLResult(customerResult);
```
### Mapping Logic
1. `Result.Status` is mapped to `Response.Status`:
- `ResultStatus.Success` → `StatusCode.Success`
- `ResultStatus.Warning` → `StatusCode.Success` (warnings are treated as success at API level)
- `ResultStatus.Error` → `StatusCode.Failed`
2. `Result.Message` is directly copied to `Response.Message`
3. `Result.MessageCode` is copied to `Response.MessageCode` (with a default if not provided)
4. For `Result<T>`, the data is added to the `Response<T>.Result` collection
## Usage Examples
### Business Logic Layer (BL)
```csharp
public Result<CustomerDTO> GetCustomerById(int customerId)
{
try
{
var customer = this._repository.GetCustomerById(customerId);
if (customer == null)
return Result<CustomerDTO>.AsError("Customer not found");
var dto = this.ConvertToDTO(customer);
return Result<CustomerDTO>.AsSuccess(dto);
}
catch (Exception ex)
{
return Result<CustomerDTO>.FromException(ex);
}
}
```
### Web Service Layer
```csharp
public Response<CustomerDTO> GetCustomerById(int customerId)
{
var result = this._customerBL.GetCustomerById(customerId);
return Response<CustomerDTO>.FromBLResult(result);
}
```
## Best Practices
1. **Always use factory methods** instead of constructors for creating `Result` objects
```csharp
// Good
return Result.AsSuccess("Operation succeeded");
// Avoid
return new Result(ResultStatus.Success, "Operation succeeded");
```
2. **Include meaningful error messages** to help diagnose issues
```csharp
// Good
return Result.AsError($"Customer with ID {id} not found");
// Avoid
return Result.AsError("Not found");
```
3. **Propagate exceptions appropriately** using `FromException`
```csharp
try
{
// Operation code
}
catch (Exception ex)
{
return Result.FromException("Failed to complete operation", ex);
}
```
4. **Use `ThrowIfError` extension method** when chaining operations
```csharp
var result = await someOperation().ThrowIfError();
```
5. **Convert between `Result` and `Response` only at service boundaries** to maintain clean separation of concerns
## Conclusion
The `Result` and `Response` classes provide a robust pattern for error handling and operation results throughout the c-entron.NET application. Understanding how they work together is essential for maintaining consistency in API responses and proper error handling across all application layers.
@@ -0,0 +1,10 @@
# Where can i find Stani's secret API-Documentation
<details>
<summary>Here</summary>
<summary>
P:\Entwicklung C#\c-entron 2.0 Web-Services\Dokumentationen\c-entron Web-Service Dokumentation.pdf
</summary>
</details>
@@ -0,0 +1,36 @@
#We use TraySoft AddTAPI.NET component on all our products with TAPI intigration
https://www.traysoft.com/add-tapi-telephony-library
Currently c-entron.NET, c-entron Outlook Addin and ServiceBoard have TAPI-Modules.
The dlls can be found on our network drives under `P:\Entwicklung C#\Components\AddTapi.NET Professional 18.11.2021`
**OUR Traysoft.AddTAPI.dll has been modified to work with .NET 5/6**
If you update this .dll you need to check if this needs to be modified again.
##How to update
First you need a decompiled version of TraySoft.AddTapi.dll. The easiest way to do this is to use dotPeek, decompile the dll and then export as csproj.
Then navigate to the following location:
TapiLine.cs > ProcessIncomingCall
In this method the BeginInvoke call is no longer supported in our new .NET runtimes.
We can simply replace this call with the following code that *should* result in the same behavior.
```
Task.Factory.StartNew(() =>
{
tapiCallHandler.Invoke(call);
});
```
##How to debug
There are a few ways to debug tapi related things:
1. a simple TAPI-Test app under `P:\Entwicklung C#\Components\tapi_test_app` which allows a very low level testing.
2. the product itself of course, there should be vast amounts of logging everywhere (check PhoneManager.cs & TapiPhoneConnectionManager.cs in c-entron.NET)
3. if your product uses the UI in centron.Controls there's a testtool in Centron.Controls.Preview
If you need very low level logging of the AddTapi.dll itself, you can use VS and check the output tab when debugging as the dll writes its log there.
@@ -0,0 +1,159 @@
# Centron Database Script Rules
These conventions **must** be followed for all database scripts in the `Centron.BusinessLogic.Administration.Scripts.ScriptMethods.Scripts` namespace. Scripts are used to update the SQL Server database by creating/altering tables, creating indexes, functions, triggers, and updating data when necessary.
---
## 1. Script Organization & Naming
1. **Placement**
- Place all scripts in the directory: `src/backend/Centron.BL/Administration/Scripts/ScriptMethods/Scripts/`
2. **Naming Convention**
- Name scripts using the pattern: `ScriptMethod{NUMBER}.cs`
- The script number is managed in an external Excel file accessible through Teams
- When creating a new script, use a placeholder for the number and update it with the next available number from the Excel file
3. **Class Structure**
- Each script class must inherit from `BaseScriptMethod`
- The class name must match the filename
```csharp
internal class ScriptMethod11699 : BaseScriptMethod
```
## 2. Script Implementation
1. **Required Method**
- Override the `GetSqlQueries()` method from the base class
- Return type should be `IEnumerable<string>`
- Use `yield return` statements to return SQL statements
```csharp
public override IEnumerable<string> GetSqlQueries()
{
yield return ScriptHelpers.AddColumnIfNotExists("dbo", "TableName", "ColumnName", "datatype");
}
```
2. **Using ScriptHelpers**
- Always use the `ScriptHelpers` class methods to generate SQL statements
- This ensures consistency and safety in database operations
- Common helper methods:
- `AddColumnIfNotExists`
- `DropColumnIfExists`
- `ChangeColumnTypeIfExists`
- `AddTableIfNotExists`
- `AddIndexIfNotExists`
- `CreateSpecialObjectAlterIfExists`
- And many more in `ScriptHelpers.cs`
### Script Helper Note
When using `ScriptHelpers.AddTableIfNotExists()` method in database scripts, the primary key column `I3D` is automatically created and does not need to be specified in the column list. The method internally handles the creation of:
- The `I3D` [int] IDENTITY(1,1) NOT NULL column
- The primary key constraint with appropriate clustering
### Standard Audit Columns
- Add `CreatedByI3D`, `CreatedDate`, `IsDeleted`, `DeletedByI3D`, and `DeletedDate` to new NHibernate-managed domain tables according to the standard entity conventions.
- Add `ChangedByI3D` and `ChangedDate` only when rows are modified after creation.
- Write-once/read-only history tables do not need `Changed*` columns. Examples: chat messages, tool call history, immutable communication logs.
## 3. Common Script Scenarios
1. **Adding or Changing Columns**
```csharp
// Adding a column
yield return ScriptHelpers.AddColumnIfNotExists("dbo", "TableName", "ColumnName", "datatype", nullable: true/false);
// Changing a column type
yield return ScriptHelpers.ChangeColumnTypeIfExists("dbo", "TableName", "ColumnName", "new_datatype");
```
2. **Creating Tables**
```csharp
// Basic usage with dbo schema implied
yield return ScriptHelpers.AddTableIfNotExists("TableName",
("Column1", "int", false, null),
("Column2", "nvarchar(100)", false, null),
("Column3", "datetime2(0)", true, null));
// Explicit schema usage
yield return ScriptHelpers.AddTableIfNotExists(
schema: "dbo",
table: "TableName",
("Column1", "int", false, null),
("Column2", "nvarchar(100)", false, null),
("IsActive", "bit", false, null));
```
**Parameter explanation:**
- Column format: (name, datatype, nullable, defaultValue)
- For new NHibernate-managed tables, use `null` or an empty value for `defaultValue`
- NHibernate writes all mapped columns on `INSERT`, so SQL defaults on new tables do not apply in normal application writes and only create unused SQL objects
- Only consider SQL defaults when extending existing tables with new `NOT NULL` columns, where existing rows or non-NHibernate writers need a valid value
- If `defaultValue` is empty, no default constraint will be created
- The primary key column `I3D` is automatically created
- Examples of justified default values:
- For integers: "0", "1", etc.
- For strings: "''" (empty string with single quotes)
- For dates: "GETUTCDATE()"
- For bits (boolean): "1" (true) or "0" (false)
3. **Creating Indexes**
```csharp
yield return ScriptHelpers.AddIndexIfNotExists(
table: "TableName",
indexName: "IX_TableName_Column1_Column2",
columns: new List<(string column, OrderDirection? orderDirection)>
{
("Column1", OrderDirection.ASC),
("Column2", OrderDirection.ASC),
});
```
4. **Executing SQL Statements Directly**
```csharp
yield return @"
UPDATE TableName
SET Column1 = 'value'
WHERE Condition = 1;";
```
5. **Creating/Altering Views, Functions, Triggers**
```csharp
yield return ScriptHelpers.CreateSpecialObjectAlterIfExists("ViewName", "VIEW", @"
CREATE VIEW dbo.ViewName
AS
SELECT * FROM TableName
WHERE Condition = 1;");
```
## 4. Best Practices
1. **Script Independence**
- Each script should be independent and idempotent
- Use conditional checks like `IF EXISTS` and `IF NOT EXISTS`
2. **Script Safety**
- Always use `ScriptHelpers` methods when available
- When direct SQL is needed, ensure proper schema references and SQL injection protection
3. **Performance Considerations**
- For large data operations, consider transaction management and batching
- When modifying indexed columns, use `AlterColumnTypeIndexSafe` to preserve indexes
4. **Documentation**
- Add comments to clarify complex operations
- For significant schema changes, document the purpose in a comment
5. **Testing**
- Test scripts in a development environment before applying to production
- Verify the script achieves the intended changes without side effects
## 5. Examples
Refer to these existing scripts for common patterns:
- `ScriptMethod11699.cs` - Adding columns and changing column types
- `ScriptMethod11698.cs` - Executing direct SQL update statements
- `ScriptMethod11696.cs` - Creating new tables
- `ScriptMethod11677.cs` - Creating indexes
- `ScriptMethod11670.cs` - Creating and altering views (same approach for triggers, functions)
@@ -0,0 +1,351 @@
# EDI Architecture Documentation
This document provides a comprehensive overview of the Electronic Data Interchange (EDI) architecture in the c-entron.NET system, focusing on the supplier EDI integration patterns and implementation.
## Table of Contents
- [Overview](#overview)
- [Core Architecture](#core-architecture)
- [EDI Data Flow](#edi-data-flow)
- [Supplier Integration Patterns](#supplier-integration-patterns)
- [Partial Class Architecture](#partial-class-architecture)
- [EDI Data Types](#edi-data-types)
- [Document Processing Workflow](#document-processing-workflow)
- [Error Handling and Logging](#error-handling-and-logging)
- [Configuration Management](#configuration-management)
- [Extension Points](#extension-points)
## Overview
The EDI system in c-entron.NET facilitates automated business document exchange between c-entron and various suppliers. The architecture supports multiple EDI standards and supplier-specific formats, processing orders, order responses, deliveries, and invoices through a unified interface.
### Key Components
- **SupplierEdiBL**: Main business logic class handling EDI operations
- **EDICommonBL**: Shared utilities for data parsing and formatting
- **EDILogBL**: Logging and audit trail management
- **ClientConnectBL**: Connection management for external EDI services
- **Partial Classes**: Supplier-specific implementations for different EDI formats
## Core Architecture
### Class Hierarchy
```
SupplierEdiBL (main class)
├── SupplierEdiBL.AlsoCH.cs - ALSO Switzerland specific implementation
├── SupplierEdiBL.Also.cs - ALSO generic implementation
├── SupplierEdiBL.Alltron.cs - Alltron supplier integration
├── SupplierEdiBL.Herweck.cs - Herweck supplier integration
├── SupplierEdiBL.Komsa.cs - Komsa supplier integration
└── SupplierEdiBL.Opentrans.cs - OpenTrans 2.1 standard implementation
```
### Dependencies
The EDI system relies on several core components:
- **Gateway Libraries**: Supplier-specific EDI parsing libraries
- `Centron.Gateway.EDI_Also`
- `Centron.Gateway.EDI_AlsoCH`
- `Centron.Gateway.EDI_Alltron`
- `Centron.Gateway.EDI_Herweck`
- `Centron.Gateway.OpenTrans`
- **External APIs**:
- ITScope API for product data synchronization
- EGIS integration for electronic invoicing
- ZUGFeRD for structured invoice data
## EDI Data Flow
### High-Level Process Flow
```
1. Configuration Setup
├── Supplier EDI configurations defined
├── Connection parameters configured
└── Data type mappings established
2. Document Download
├── FTP/SFTP file retrieval
├── File decompression (ZIP support)
└── Document validation
3. Data Processing
├── Format detection (EdiDataType)
├── Supplier-specific parsing
└── c-entron object creation
4. Integration
├── Order matching and validation
├── Business rule application
└── Database persistence
5. Logging and Audit
├── Processing status tracking
├── Error logging
└── User notification
```
### Core Processing Method: `ApplyDistriToCentron`
The `ApplyDistriToCentron` method serves as the central dispatch mechanism for processing EDI files:
```csharp
public async Task<bool> ApplyDistriToCentron(
List<EDIDistriFile> xmlData,
SupplierEdiConfigurations config,
OrderInfo deal)
```
**Key Responsibilities:**
- Routes processing based on `EdiDataType` and `ObjectKind`
- Delegates to supplier-specific parsing methods
- Handles file cleanup after successful processing
- Returns success/failure status for downstream processing
## Supplier Integration Patterns
### Document Types Supported
Each supplier integration supports different combinations of document types:
| Supplier | Order Response | Delivery | Invoice | Notes |
|----------|---------------|----------|---------|--------|
| OpenTrans 2.1 | ✓ | ✓ | ✓ | Industry standard |
| ALSO | ✓ | ✓ | ✓ | Generic ALSO format |
| ALSO CH | ✓ | ✓ | ✓ | Switzerland-specific |
| Herweck | ✓ | ✓ | ✓ | Dual structure support |
| Komsa | ✓ | ✓ | ✗ | No invoice integration |
| Alltron | ✓ | ✓ | ✓ | Full document support |
| ZUGFeRD | ✗ | ✗ | ✓ | Invoice-only standard |
## Partial Class Architecture
### Design Pattern
The EDI system uses partial classes to organize supplier-specific logic while maintaining a unified interface. Each partial class handles:
1. **Document Parsing**: XML deserialization using supplier-specific schemas
2. **Data Mapping**: Conversion from supplier format to c-entron entities
3. **Business Logic**: Supplier-specific validation and processing rules
4. **Error Handling**: Format-specific error recovery and logging
### Example: ALSO CH Implementation
```csharp
public partial class SupplierEdiBL
{
public bool ReadAlsoCHResponse(List<EDIDistriFile> distriFiles, SupplierEdiConfigurations config)
{
// Parse ALSO CH specific XML format
var serializer = new XmlSerializer(typeof(orderresponse));
var response = (orderresponse)serializer.Deserialize(distriFile.XmlDatei);
// Map to c-entron entities
EDIOrderResponseHead head = new EDIOrderResponseHead();
// ... mapping logic
// Process and validate
return ApplyEDIReceiptToCentronOrder(lstHead, lstItems, lstData, objectKind);
}
}
```
### Supplier-Specific Features
#### ALSO CH (AlsoCH.cs)
- **Special Handling**: Swiss banking integration (ESR codes)
- **Additional Costs**: Handling of various Swiss fees (G1-G8 codes)
- **Localization**: Swiss address format and currency handling
#### Herweck (Herweck.cs)
- **Dual Structure**: Supports both legacy and new XML structures
- **Fallback Logic**: Automatic retry with alternative parsing method
- **Advanced Mapping**: Complex article code resolution
#### Alltron (Alltron.cs)
- **Serial Number Tracking**: Enhanced barcode and serial number management
- **Delivery Integration**: Detailed delivery note processing
- **Swiss Market**: Optimized for Swiss IT distribution
## EDI Data Types
### Supported Formats
```csharp
public enum EdiDataType
{
OpenTrans21 = 1, // Industry standard OpenTrans 2.1
Also = 2, // ALSO generic format
AlsoCH = 3, // ALSO Switzerland
Herweck = 4, // Herweck proprietary format
Komsa = 5, // Komsa format
Alltron = 6, // Alltron format
Zugferd = 7 // ZUGFeRD standard
}
```
### Object Kinds
```csharp
public enum EDIConnectionObjectKind
{
Order = 1, // Purchase orders (outbound)
OrderResponse = 2, // Order confirmations (inbound)
Delivery = 3, // Delivery notifications (inbound)
Invoice = 4 // Electronic invoices (inbound)
}
```
## Document Processing Workflow
### Order Response Processing
1. **Document Reception**: Download from supplier FTP/SFTP
2. **Format Detection**: Identify EDI format and supplier
3. **Header Processing**:
- Extract order reference information
- Validate buyer/supplier party IDs
- Map delivery addresses
4. **Line Item Processing**:
- Parse product codes (supplier, manufacturer, EAN)
- Extract quantities and pricing
- Handle delivery dates and availability
5. **Integration**:
- Match with existing c-entron orders
- Update order status and quantities
- Generate user notifications for discrepancies
### Delivery Note Processing
1. **Shipment Information**: Extract tracking and delivery details
2. **Serial Number Handling**: Process individual item serial numbers
3. **Quantity Validation**: Verify shipped quantities against orders
4. **Barcode Processing**: Handle product identification codes
5. **Receipt Generation**: Create delivery receipts in c-entron
### Invoice Processing
1. **Financial Data**: Extract pricing, VAT, and currency information
2. **Reference Matching**: Link invoices to deliveries and orders
3. **Tax Calculation**: Validate VAT amounts and rates
4. **Payment Terms**: Process payment conditions and bank details
5. **Accounting Integration**: Create accounting entries
## Error Handling and Logging
### Logging Framework
The EDI system uses structured logging through `EDILogBL`:
```csharp
public enum EDILogState
{
DownloadOK, // Successful processing
DownloadError, // Processing errors
DownloadTest, // Test mode operations
Exception, // System exceptions
TestException // Test mode exceptions
}
```
### Error Recovery Strategies
1. **Format Fallbacks**: Multiple parsing attempts for flexible formats
2. **Partial Processing**: Continue processing valid records despite individual failures
3. **User Notifications**: Alert users to validation issues requiring manual intervention
4. **Retry Logic**: Automatic retry for transient connection issues
### Audit Trail
- Complete processing history for each document
- User actions and validations tracked
- Error details and resolution steps logged
- Performance metrics and processing times recorded
## Configuration Management
### Supplier Configuration
Each supplier integration requires configuration through `SupplierEdiConfigurations`:
```csharp
public class SupplierEdiConfigurations
{
public int SupplierI3D { get; set; } // Supplier identifier
public string SupplierCustomerNumber { get; set; } // Customer number at supplier
public int EdiDataType { get; set; } // Format type
public int ObjectKind { get; set; } // Document type
public string ConnectionString { get; set; } // FTP/API connection details
// ... additional configuration properties
}
```
### Branch-Specific Handling
- Multi-branch deployments supported
- Branch-specific supplier configurations
- Centralized vs. distributed processing options
## Extension Points
### Adding New Suppliers
To integrate a new supplier:
1. **Create Partial Class**: `SupplierEdiBL.NewSupplier.cs`
2. **Implement Reading Methods**:
```csharp
private bool ReadNewSupplierResponse(List<EDIDistriFile> files, SupplierEdiConfigurations config)
private bool ReadNewSupplierDelivery(List<EDIDistriFile> files, SupplierEdiConfigurations config)
private bool ReadNewSupplierInvoice(List<EDIDistriFile> files, SupplierEdiConfigurations config)
```
3. **Update ApplyDistriToCentron**: Add new case for supplier format
4. **Add Gateway Library**: Create parsing library if needed
5. **Configure Mappings**: Set up data type and configuration entries
### Custom Business Logic
- Override validation rules in supplier-specific partials
- Implement custom data transformations
- Add supplier-specific error handling
- Extend logging and audit capabilities
### API Integration
- ITScope API integration for real-time product data
- EGIS electronic invoicing support
- ZUGFeRD structured invoice processing
- Custom API endpoints for supplier-specific requirements
## Best Practices
### Performance Considerations
- Batch processing for large document volumes
- Async processing for I/O operations
- Memory-efficient XML parsing for large files
- Connection pooling for FTP/API operations
### Security
- Secure credential storage for supplier connections
- Encrypted data transmission (SFTP/HTTPS)
- Audit logging for compliance requirements
- Access control for EDI operations
### Maintainability
- Consistent error handling patterns across suppliers
- Comprehensive unit testing for each supplier integration
- Clear separation of concerns between parsing and business logic
- Documentation of supplier-specific requirements and limitations
---
**Related Documentation:**
- [Database Schema Reference](../database/README.md)
- [Security Architecture](../security/README.md)
- [Receipt Processing Guide](../receipts/README.md)
@@ -0,0 +1,244 @@
# EDI-Service Import Process
This document outlines the technical specifications and process flow for how the c-entron.NET EDI-Service downloads EDI documents from suppliers and imports them into the database.
## 1. System Architecture
### 1.1 Components
- **EdiDownloadService**: ASP.NET Core BackgroundService for scheduled downloads
- **SupplierEdiWebServiceBL**: Business logic layer for EDI web services
- **SupplierEdiBL**: Core business logic for supplier EDI operations
- **EDIConnectBL**: Connection handling for FTP/SFTP/FTPS
### 1.2 Execution Frequency
- Runs every 30 minutes (configurable)
- Initial 1 minute delay after system startup
- Log cleanup for entries older than 185 days occurs between 00:00-02:00
## 2. Document Processing Flow
### 2.1 Initialization & Configuration
```
EdiDownloadService.ExecuteAsync
└── SupplierEdiWebServiceBL.EDIDownloadStartAsync
└── SupplierEdiBL.DownloadStartAsync
├── GetSupplierEdiConfigurations
└── ProcessIndividualConfigurations
```
### 2.2 Download Process
For each supplier configuration:
1. **Connection Selection**:
- FTP/FTPS: Uses `Ftp_DownloadAsync()`
- SFTP: Uses `sFtp_DownloadAsync()`
2. **File Filtering**:
```csharp
// Get list of already processed files
var usedFiles = UsedFiles(config);
// Filter available files
foreach (FtpListItem file in serverFiles.Data.Where(f => f.Type == FtpObjectType.File))
{
// Skip if specific file expected but doesn't match
if (!string.IsNullOrEmpty(expectedFile) && expectedFile != file.Name) continue;
// Skip blacklisted files (those that failed multiple times before)
if (badFiles.Any(f => f.Value.Contains(file.Name))) continue;
// Skip already processed files
if (usedFiles.IndexOf(file.Name) > -1) continue;
// Skip files not matching mask pattern
if (!FitMask(file.Name, config.Mask)) continue;
// Process file
var distriFiles = await DownloadFtpFile(config, file.Name);
if (distriFiles != null)
if (await ApplyDistriToCentron(distriFiles, config, null)) ++nSaved;
}
```
### 2.3 ZIP Handling
Files are processed differently based on extension:
```csharp
if (Path.GetExtension(fileName).ToLower() == ".zip")
{
// Add ZIP file to list
distriFiles.Add(new EDIDistriFile() { DistriName = fileName });
// Extract contents
ZipExtract(result.Data, distriFiles);
}
else
{
// Handle non-ZIP file
MemoryStream dataStream = new MemoryStream();
result.Data.CopyTo(dataStream);
distriFiles.Add(new EDIDistriFile() {
UnpackName = fileName,
DistriName = fileName,
XmlDatei = dataStream
});
}
```
### 2.4 Database Import
Import logic varies by document and supplier type:
```csharp
switch (config.EdiDataType)
{
case (int)EdiDataType.OpenTrans21:
if (config.ObjectKind == (int)EDIConnectionObjectKind.OrderResponse)
isOk = await ReadOT21Response(xmlData, config, deal);
if (config.ObjectKind == (int)EDIConnectionObjectKind.Delivery)
isOk = await ReadOT21Delivery(xmlData, config, deal);
if (config.ObjectKind == (int)EDIConnectionObjectKind.Invoice)
isOk = await ReadOT21InvoiceAsync(xmlData, config, deal);
break;
// Additional formats (Also, AlsoCH, Herweck, etc.)
...
}
```
## 3. Database Schema
### 3.1 Primary EDI Tables
| Table | Description | Key Columns |
|-------|-------------|------------|
| `[dbo].[EDIInvoiceHead]` | Stores EDI invoice headers | `I3D`, `SupplierI3D`, `OrigFileName` |
| `[dbo].[EDIInvoicePositions]` | Stores EDI invoice line items | `I3D`, `InvoiceHeadI3D` |
| `[dbo].[EDIDeliveryHead]` | Stores EDI delivery headers | `I3D`, `SupplierI3D`, `OrigFileName` |
| `[dbo].[EDIDeliveryPositions]` | Stores EDI delivery line items | `I3D`, `DeliveryHeadI3D` |
### 3.2 File Tracking
The system prevents duplicate imports by checking the `OrigFileName` column:
```csharp
// For invoices
if (config.ObjectKind == (int)EDIConnectionObjectKind.Invoice)
{
var used = this.Session.GetGenericDAO<EDIInvoiceHead>().GetEntityList(f => f.SupplierI3D == config.SupplierI3D);
return used.Select(f => f.OrigFileName).ToList();
}
// For delivery notes
if (config.ObjectKind == (int)EDIConnectionObjectKind.Delivery)
{
var used = this.Session.GetGenericDAO<EDIDeliveryHead>().GetEntityList(f => f.SupplierI3D == config.SupplierI3D);
return used.Select(f => f.OrigFileName).ToList();
}
```
## 4. Special Case Handling
### 4.1 Distributor-Specific Processing
- **ITScope**: Uses `LoadITScopeReceiptAsync()` for specialized receipt handling
- **EGIS**: Uses `CheckEgisAsync()` for EGIS-specific downloads
- **Supplier-Specific Format Handlers**:
- OpenTrans 2.1 (`ReadOT21*` methods)
- Also (`ReadAlso*` methods)
- AlsoCH (`ReadAlsoCH*` methods)
- Herweck (`ReadHerweck*` methods)
- Komsa (`ReadKomsa*` methods)
- Alltron (`ReadAlltron*` methods)
- Zugferd (`ReadZugferd*` methods)
### 4.2 Error Handling & File Blacklist
- Failed downloads are tracked in a separate list
- The system can be configured to retry previously failed downloads
- Errors are logged with detailed exception information
- A file blacklist mechanism prevents repeated processing of problematic files
#### 4.2.1 File Blacklist Implementation
The system maintains a blacklist of files that have repeatedly failed processing:
```csharp
// In DownloadStartAsync method:
badFiles = GetDownloadWithError(config.SupplierI3D, config.ObjectKind);
// Files with more than 3 recorded exceptions are blacklisted
await Ftp_DownloadAsync(config, badFiles.Where(f => f.ID > 3).ToList(), expectedFile);
```
The blacklist is populated from the `EDIManagementLog` table using SQL:
```csharp
private List<IntStringList> GetDownloadWithError(int distributorI3D, int objectKind)
{
ReceiptLogKind logKind;
// Map objectKind to appropriate log kind...
string sSql = $@"select COUNT(*) ID, l.FileName Value from EDIManagementLog l
Where l.State = {(int)EDILogState.Exception}
and l.EDIReceiptLogKind = {(int)logKind}
and l.DistributorI3D = {distributorI3D.ToString()}
Group By l.FileName ";
return Session.Advanced.RawSqlAccess.ExecuteQuery<IntStringList>(sSql, null).ToList();
}
```
During download processing, blacklisted files are skipped:
```csharp
// In both Ftp_DownloadAsync and sFtp_DownloadAsync methods
if (badFiles.Any(f => f.Value.Contains(file.Name))) continue;
```
This prevents the system from repeatedly trying to process files that have caused multiple exceptions, reducing system load and avoiding potential endless error loops.
## 5. Logging System
The EDI process uses NLog for comprehensive logging:
```csharp
// Log start of EDI process
Logger.Info($"EDI Download starts.");
// Log errors with full exception details
Logger.Error(exception, "EDI Download ERROR");
// Detailed operation logs via _eDILogBL
_eDILogBL.WriteEdiDownloadLog(config, EDILogState.DownloadTest, fileName: expectedFile, comment: $"File: {expectedFile} has already been exported.");
```
## 6. Testing & Debugging
### 6.1 Test Mode
A test mode is available (`isTest` parameter in `DownloadStartAsync`):
- Files are not deleted from remote server
- More detailed logs are generated
- Can target specific files via `expectedFile` parameter
### 6.2 System User
The system uses a designated system user account:
```csharp
var user = this.Session.GetGenericDAO<AppUser>().GetById(
new AppSettingsBL(Session).GetSettings(ApplicationSettingID.CentronSystemUser)
.GetInt(ApplicationSettingID.CentronSystemUser, null));
```
## 7. Security Considerations
- Connection credentials are securely stored in supplier EDI configuration
- Supports secure protocols: FTPS (FTP with SSL/TLS) and SFTP
- Files are processed in memory to minimize disk exposure
- System user permissions control database operations
@@ -0,0 +1,123 @@
# RMM-Article Logic in Contract Billing
## Overview
The Remote Monitoring Management (RMM) Article functionality in c-entron.NET allows for automatic billing of usage-based services that are measured by an external RMM system. This document outlines the rules, workflow, and technical implementation of the RMM Article billing process.
## Key Concepts
### RMM System Integration
- The c-entron.NET application integrates with external RMM systems (e.g., "Riverbird") to retrieve usage statistics
- Usage data is collected for specified periods and used to calculate billing amounts
- Communication happens via the `RiverConnectionBL` class which connects to the RMM service
### Contract Article References
- Each billable RMM item is defined as a `ContractArticleReferenzes` entity
- These references link articles in c-entron.NET to specific metrics in the RMM system
- Article references contain configuration for billing calculation rules
## Workflow
### 1. Contract Configuration
1. A contract is configured to use RMM billing (`WhetherRMM` returns true)
2. Contract article references are configured, specifying:
- Article type (e.g., server, workstation)
- Article reference (linking to the inventory item)
- Pricing rules
### 2. Invoice Generation Process
1. During the `CreateInvoiceToContractComplete` process, `CheckRMMArticle` is called
2. The system checks if the contract has RMM enabled
3. The system looks for a placeholder tag `@@RMMArtikel@@` in the invoice template
4. Contract article references are retrieved for the specific contract
5. The system queries the external RMM service for usage data in the billing period
6. For each article reference with available usage data:
- Usage amount is calculated using `CalculateContractBillingAmount`
- An invoice line item is created with the calculated amount
- Descriptive text is added explaining the service type
- The item is inserted at the position marked by `@@RMMArtikel@@` or near the end of the invoice
### 3. Placeholder Handling
- If the invoice template contains a text element with `@@RMMArtikel@@`, it serves as a position marker
- This placeholder is removed and replaced with the actual RMM article items
- If no placeholder is found, RMM items are inserted near the end of the invoice (count - 2 position)
## Error Handling
### External Service Unavailability
- If the RMM service is unavailable during invoice generation, and the contract requires RMM data:
- An `RMMServiceUnavailableException` is thrown
- The invoice creation process is aborted
- An error message is logged with details about the failure
- This prevents invoices from being created with incomplete usage data, ensuring customers are billed correctly
### Data Integrity Rules
- When a parent entity is deleted (State = 0), related child entities should also be marked as deleted
- This ensures data consistency when RMM configurations change
## Technical Implementation Details
### RMM Article Detection
```csharp
var rmmItem = invoice.Items.FirstOrDefault(f =>
(f.RichText != null && f.RichText.IndexOf("@@RMMArtikel@@", StringComparison.InvariantCulture) > -1) ||
(f.Text != null && f.Text.IndexOf("@@RMMArtikel@@", StringComparison.InvariantCulture) > -1));
```
### Usage Data Retrieval
```csharp
var riverbirdStatisticsResult = new RiverConnectionBL(this.Session).GetContractBillingAmounts(
billingParam.InvoiceFrom.Value,
billingParam.InvoiceTo.Value.AddDays(1),
invoice.CustomerI3D,
rmmArticleReferences);
```
### Error Handling for Service Unavailability
```csharp
if (riverbirdStatisticsResult.Status is ResultStatus.Error)
{
// Only throw exception when RMM articles are expected
if (rmmItem != null || rmmArticleReferences.Any())
{
string errorMsg = $"Die Rechnung kann nicht erstellt werden, da der RMM-Service nicht erreichbar ist. " +
$"Fehlermeldung: {riverbirdStatisticsResult.Message}";
_logger.Error(errorMsg);
throw new RMMServiceUnavailableException(errorMsg);
}
return;
}
```
## Best Practices
1. **Service Configuration**
- Ensure the RMM service URL is properly configured in application settings
- Verify authentication tickets are valid for the RMM service
2. **Contract Setup**
- Associate correct article references with appropriate RMM metrics
- Set proper calculation rules for each article type
3. **Invoice Templates**
- Include the `@@RMMArtikel@@` placeholder in invoice templates where RMM items should appear
- Ensure proper formatting and positioning for RMM article items
4. **Monitoring**
- Monitor logs for RMM service connectivity issues
- Periodically verify that usage data is being correctly retrieved and calculated
## Troubleshooting
| Problem | Possible Cause | Solution |
|---------|---------------|----------|
| No RMM items in invoice | RMM not enabled for contract | Check contract configuration |
| No RMM items in invoice | No usage data in RMM system | Verify usage data in RMM system |
| Invoice creation fails | RMM service unavailable | Check network connectivity and service status |
| Incorrect billing amounts | Calculation rules misconfigured | Review article reference configuration |
## Related Components
- `AutomaticFacturaWebServiceBL` - Main billing logic
- `RiverConnectionBL` - Handles communication with RMM service
- `ContractArticleReferenzes` - Defines article references for RMM billing
@@ -0,0 +1,398 @@
# ActionPrice System Documentation
## Overview
The ActionPrice (Aktionspreis) system in c-entron manages time-limited promotional pricing from distributors and manufacturers. It integrates seamlessly with the price matrix (Preismatrix) to provide users with current action prices alongside other pricing sources.
## Table of Contents
- [Database Structure](#database-structure)
- [Architecture & Components](#architecture--components)
- [Data Flow](#data-flow)
- [Data Sources](#data-sources)
- [UI Access](#ui-access)
- [Integration with Price Matrix](#integration-with-price-matrix)
- [API Reference](#api-reference)
- [Business Rules](#business-rules)
## Database Structure
### Table: `HerstellerArtikAktionspreis`
**Location**: SQL Server database
**Mapped by**: `ActionPriceMaps.cs`
| Column | Data Type | Description |
|--------|-----------|-------------|
| `I3D` | int IDENTITY(1,1) | Primary key |
| `ArtikelI3D` | int | Foreign key to Article table |
| `Artikelcode` | nvarchar(60) | Article code |
| `Preis` | decimal | Action price |
| `Distributor` | nvarchar(100) | Distributor name |
| `GueltigAb` | datetime2(2) | Effective from date |
| `GueltigBis` | datetime2(2) | Effective until date |
| `Text` | nvarchar(500) | Description/notes |
| `Hersteller` | nvarchar(60) | Manufacturer |
| `BearbeiterI3D` | int | Editor user ID |
| `EDI_I3D` | int | EDI integration ID (reserved) |
| `Verfuegbarkeit` | nvarchar(50) | Availability |
| `VK` | decimal | Selling price |
| `Kreditorcode` | nvarchar(50) | Creditor code |
| `Status` | int | Status flag |
| `DistID` | nvarchar(50) | Distributor product ID |
## Architecture & Components
### Core Components
#### 1. Entity Layer
- **File**: `Centron.Entities/Warehousing/ActionPrice.cs`
- **Purpose**: Domain entity representing action price data
- **Properties**: Maps 1:1 with database columns
#### 2. Data Access Layer (DAO)
- **File**: `Centron.DAO/Mappings/Warehousing/ActionPriceMaps.cs`
- **Purpose**: NHibernate mapping for ActionPrice entity
- **Technology**: FluentNHibernate
#### 3. Business Logic Layer (BL)
- **File**: `Centron.BL/Warehousing/ActionPriceBL.cs`
- **Methods**:
- `GetActionPrice(int actionPriceI3D)`
- `GetActionPricesByArticleI3D(int articleI3D)`
- `SaveOrUpdateActionPrice(ActionPrice actionPrice)`
- `DeleteActionPrice(ActionPrice actionPrice)`
#### 4. Web Service Layer
- **File**: `Centron.BL/WebServices/Warehousing/ActionPriceWebServiceBL.cs`
- **Purpose**: DTO conversion and web service operations
- **Features**: Entity ↔ DTO mapping using ObjectMapper
#### 5. REST API
- **File**: `CentronRestService.cs`
- **Endpoints**:
- `POST /GetActionPrice`
- `POST /GetActionPricesByArticleI3D`
- `POST /SaveOrUpdateActionPrice`
- `POST /DeleteActionPrice`
### Dual Implementation Pattern
Following c-entron's standard pattern, ActionPrice supports both connection types:
#### BL Logic (Direct Database)
- **File**: `BLActionPriceLogic.cs`
- **Connection**: `CentronConnectionType.SqlServer`
- **Access**: Direct database via NHibernate
#### WS Logic (Web Service)
- **File**: `WSActionPriceLogic.cs`
- **Connection**: `CentronConnectionType.CentronWebServices`
- **Access**: REST API calls
## Data Flow
### Reading ActionPrices
```
1. Price Matrix Request
↓
2. Article Lookup (by ManufacturerCode or EAN)
↓
3. IActionPriceLogic.GetActionPricesByArticleI3D()
↓
4. Filter by Date Range (current valid prices only)
↓
5. Convert to PriceItemViewModel
↓
6. Display in Price Matrix Grid
```
### Creating ActionPrices
```
1. User Right-clicks Price Matrix Grid
↓
2. Select "Aktionspreis hinzufügen"
↓
3. AddActionPriceViewModel Dialog Opens
↓
4. User Enters Data (Distributor, Price, Dates)
↓
5. Validation (Distributor required, valid date range)
↓
6. IActionPriceLogic.SaveOrUpdateActionPrice()
↓
7. Data Saved to Database
↓
8. Price Matrix Refreshed
```
## Data Sources
### Current Active Sources
#### 1. Manual Entry (Primary)
- **Location**: Article Management → Additional Info → Preisspiegel Tab
- **Method**: Right-click context menu → "Aktionspreis hinzufügen"
- **Validation**:
- Distributor name required
- EffectiveFrom ≤ EffectiveUntil
- **User Tracking**: EditorI3D field tracks creator
### Potential Sources (Infrastructure Exists)
#### 1. EDI Integration
- **Evidence**: `EDI_I3D` field in database
- **Status**: Infrastructure exists but no active implementation found
- **Purpose**: Automated import from supplier EDI systems
#### 2. Bulk Import
- **Evidence**: Standard c-entron import patterns
- **Status**: No specific ActionPrice import modules identified
- **Potential**: Could be implemented for supplier data feeds
## UI Access
### Step-by-Step Navigation
1. **Open Article Management**
- Navigate: Warehousing → Article Management
2. **Select Article**
- Search for and open an existing article
3. **Access Additional Info**
- Navigate to "Zusatzinfo" (Additional Info) section
4. **Open Preisspiegel Tab**
- Click on "Preisspiegel" tab
- This displays the price matrix grid
5. **Access ActionPrice Functions**
- **Right-click** on the price matrix grid
- Context menu appears with options:
- "Preisspiegel aktualisieren" (Refresh)
- "Aktionspreis hinzufügen" (Add ActionPrice)
- "Aktionspreis bearbeiten" (Edit ActionPrice)
- "Aktionspreis löschen" (Delete ActionPrice)
### UI Components
- **View**: `ArticleAdditionalInfoView.xaml`
- **ViewModel**: `ArticleAdditionalInfoViewModel.cs`
- **Grid**: `PriceWatchGridControl` (line 68)
- **Tab**: "Preisspiegel" (line 64)
- **Context Menu**: Lines 123-141
## Integration with Price Matrix
### Price Matrix Sources
ActionPrice is one of 7 parallel price sources in the matrix:
1. **ITscope** - External API
2. **Article Import** - Imported price data
3. **COP** - External API
4. **NEOS** - External API
5. **TradersGuide** - External API
6. **EGIS** - External API
7. **Aktionspreise** - Internal action prices ←
### Display Logic
- **File**: `PriceMatrixViewModel.cs`
- **Method**: `GetPriceItemsFromArticleActionPrices()` (lines 416-459)
- **Filtering**: Only shows prices where current date is within EffectiveFrom/EffectiveUntil range
- **Service Label**: Displays as "Aktionspreise" in Service column
- **Description Format**: "Aktionspreis vom {EffectiveFrom:d} bis {EffectiveUntil:d}. {Text}"
### Price Item Properties
```csharp
// ActionPrice in Price Matrix
Service = "Aktionspreise"
Supplier = actionPrice.Distributor
PurchasePrice = actionPrice.Price
RawPurchasePrice = actionPrice.Price
Date = actionPrice.EffectiveFrom
Stock = null // Always visible
ArticleDescription = "Aktionspreis vom ... bis ... {Text}"
```
## API Reference
### REST Endpoints
#### Get Single ActionPrice
```http
POST /GetActionPrice
Content-Type: application/json
{
"Data": 123 // ActionPrice I3D
}
```
#### Get ActionPrices by Article
```http
POST /GetActionPricesByArticleI3D
Content-Type: application/json
{
"Data": 456 // Article I3D
}
```
#### Save or Update ActionPrice
```http
POST /SaveOrUpdateActionPrice
Content-Type: application/json
{
"Data": {
"I3D": 0, // 0 for new, >0 for update
"ArticleI3D": 456,
"ArticleCode": "ART001",
"Price": 99.99,
"Distributor": "Supplier Name",
"EffectiveFrom": "2024-01-01T00:00:00",
"EffectiveUntil": "2024-12-31T23:59:59",
"Text": "Special promotion",
"Manufacturer": "Brand Name"
}
}
```
#### Delete ActionPrice
```http
POST /DeleteActionPrice
Content-Type: application/json
{
"Data": {
"I3D": 123,
// ... other properties
}
}
```
### Code Usage
#### Get ActionPrices for Article
```csharp
var actionPrices = await ClassContainer.Instance
.WithInstance((IActionPriceLogic logic) =>
logic.GetActionPricesByArticleI3D(articleI3D))
.ThrowIfError();
```
#### Save New ActionPrice
```csharp
var actionPriceDTO = new ActionPriceDTO
{
ArticleI3D = articleI3D,
Distributor = "Supplier Name",
Price = 99.99,
EffectiveFrom = DateTime.Now,
EffectiveUntil = DateTime.Now.AddMonths(3),
Text = "Special promotion"
};
await ClassContainer.Instance
.WithInstance((IActionPriceLogic logic) =>
logic.SaveOrUpdateActionPrice(actionPriceDTO))
.ThrowIfError();
```
## Business Rules
### Validation Rules
1. **Required Fields**
- `Distributor` - Must not be empty or whitespace
2. **Date Validation**
- `EffectiveFrom` must be ≤ `EffectiveUntil`
- Both dates are required
3. **Display Rules**
- Only ActionPrices with current date within effective range show in Price Matrix
- Filter: `EffectiveFrom.StartOfDay() <= DateTime.Now && EffectiveUntil >= DateTime.Now`
### Data Integrity
1. **Article Linking**
- ActionPrices are linked to articles via `ArticleI3D`
- Article must exist in system
2. **User Tracking**
- `EditorI3D` tracks who created/modified the record
- Automatically set during save operations
3. **Status Management**
- `Status` field available for workflow management
- Currently not actively used in UI
### Price Matrix Integration
1. **Loading Priority**
- ActionPrices loaded in parallel with other price sources
- No specific priority ordering
2. **Cache Behavior**
- Price matrix results are cached by ManufacturerCode + EANCode
- Cache invalidated when ActionPrices are modified
3. **Display Formatting**
- ActionPrices always show Stock as null (always visible)
- Service column shows "Aktionspreise"
- Description includes date range and text
## Troubleshooting
### Common Issues
1. **ActionPrice Not Visible in Price Matrix**
- Check if current date is within EffectiveFrom/EffectiveUntil range
- Verify article linking via ArticleI3D
- Ensure Price Matrix cache is refreshed
2. **Context Menu Not Appearing**
- Ensure right-clicking directly on the Price Matrix grid
- Check if article is properly selected
- Verify user is in "Preisspiegel" tab
3. **Save Validation Errors**
- Verify Distributor field is not empty
- Check date range: EffectiveFrom ≤ EffectiveUntil
- Ensure all required fields are populated
### Debug Information
- **Price Matrix Loading**: Check `PriceMatrixViewModel.GetPriceItemsFromArticleActionPrices()`
- **Article Lookup**: Verify article found by ManufacturerCode or EANCode
- **Date Filtering**: Current ActionPrice validation logic
- **UI Binding**: Check `ArticleAdditionalInfoViewModel.ActionPrices` collection
## Development Notes
### Future Enhancements
1. **EDI Integration**
- `EDI_I3D` field suggests planned EDI integration
- Could automate ActionPrice imports from suppliers
2. **Bulk Import**
- Standard c-entron import patterns could be applied
- Excel/CSV import functionality possible
3. **Workflow Management**
- `Status` field could support approval workflows
- Multi-step ActionPrice approval process
4. **Advanced Filtering**
- Additional filter options in Price Matrix
- ActionPrice-specific search capabilities
### Code Maintenance
- **Entity Changes**: Update both `ActionPrice` entity and `ActionPriceDTO`
- **Database Changes**: Update `ActionPriceMaps` NHibernate mapping
- **API Changes**: Update both BL and WS logic implementations
- **UI Changes**: Update both View and ViewModel files
---
*This documentation covers the complete ActionPrice system as implemented in c-entron. For questions or updates, refer to the source code files referenced throughout this document.*
@@ -0,0 +1,435 @@
# Contracts Backend Architecture
This document describes the specific backend implementation for contracts within the c-entron.NET receipts system. Contracts extend the generic receipt architecture with specialized functionality for recurring billing, service agreements, and customer asset management.
## Overview
Contracts in c-entron.NET are specialized receipts that handle ongoing service agreements, maintenance contracts, and recurring billing scenarios. They extend the base receipt functionality with contract-specific features like billing intervals, contingent management, device tracking, and automated invoice generation.
## Entity Architecture
### ReceiptContract Entity
**Location:** `src/backend/Centron.Entities/Entities/Sales/Receipts/ContractLists/ReceiptContract.cs`
The `ReceiptContract` class extends `ReceiptBase` and implements `IReceiptContract`. It represents the main contract entity with comprehensive contract-specific properties.
#### Core Contract Properties
**Customer and Project Information:**
- `CustomerI3D`: Primary customer reference
- `ProjectNumber`: Project identifier for the contract
- `PurchaseOrderNumber`: Customer's purchase order reference
- `AdditionalText`: Supplementary contract description
**Personnel Assignment:**
- `SalesRepresentativeI3D`: Assigned sales representative
- `OfficeStaffI3D`: Internal staff responsible for contract management
**Delivery and Billing Addresses:**
- `DeliveryAddress`, `DeliveryAddressCustomerI3D`: Service delivery location
- `InvoiceAddress`, `InvoiceAddressCustomerI3D`: Billing address information
- `LicenseeAddress`, `LicenseeAddressCustomerI3D`: Software licensing address
**Contract Lifecycle:**
- `DeliveryDate`: Contract start or service delivery date
- `ContractEnd`: Contract termination date
- `ContractTermination`: Actual termination date
- `FirstPaidDate`: Date of first payment received
- `ReminderDate`: Follow-up reminder date
- `PreparationDate`: Contract preparation date
- `FinishDate`: Contract completion date
#### Billing Configuration
**Billing Intervals:**
- `BillingIntervalKind`: Type of billing cycle (Daily, Monthly, Quarterly, Yearly)
- `BillingIntervalDuration`: Number of intervals (e.g., 3 for quarterly when kind is Monthly)
- `BillingKind`: Billing methodology (enum BillingKinds)
- `AutomatedBilling`: Boolean flag for automatic invoice generation
**Contract Calculation:**
- `CalculationKind`: How contract values are calculated (enum ContractCalculationKind)
- `CalcNeedKind`: Calculation requirements (enum ContractNeedCalcKind)
- `IsNormalize`: Whether to normalize billing amounts
- `IsFullNormalizeAmount`: Full normalization flag
**Payment and Collection:**
- `PaymentConditionI3D`: Reference to payment terms
- `PaymentConditionText`: Custom payment terms description
- `CollectInvoice`: Collection settings
- `MandatI3D`: SEPA mandate reference
#### Advanced Contract Features
**Contingent Management:**
- `ContingentUsedHours`: Hours consumed from contract contingent
- `ContingentUsedAmount`: Monetary amount consumed
- `ContingentBalanceUsedHours`: Balance hours utilized
- `ContingentBalanceUsedAmount`: Balance amount utilized
- `ContingentBalanceArticleI3D`: Article used for contingent balancing
- `UseContingentBalanceArticle`: Flag to enable balance article usage
- `ContingentResidualValueStart`: Starting residual value
- `ContingentResidualValueStartDate`: Start date for residual calculation
**Contingent Limits and Monitoring:**
- `IsContingentLimitBilling`: Enable contingent limit billing
- `ContingentLimitValue`: Limit threshold value
- `ContingentLimitKind`: Type of limit (enum ContingentLimitKinds)
- `IsMonitoring`: Enable contract monitoring
- `MonitoringValue`: Monitoring threshold
**Device and Asset Management:**
- Contract-specific device relationships through master data lists
- Serial number tracking and device lifecycle management
- Click counter management for printer/copier contracts
#### Contract Automation
**Prolongation and Renewals:**
- `AutomatedProlongation`: Automatic contract renewal flag
- `LastSubsequentBillingDate`: Date of last follow-up billing
**Web Integration:**
- `IsDisplayedOnWeb`: Web portal visibility flag
- `WebReportI3D`: Associated web report
### Database Schema
#### Contract Database Architecture
Contracts follow the dual-layer database architecture used throughout the receipts system, consisting of legacy German-named tables and modern English-named views.
#### Legacy Contract Tables
##### VertragKopf Table (Contract Headers)
**Physical Table:** `dbo.VertragKopf`
**Entity Class:** `Centron.DAO.TemporaryEntities.VertragKopf`
**Mapping:** `Centron.DAO.Mappings.TemporaryEntities.VertragKopfMaps`
The `VertragKopf` table inherits from `ReceiptTable` and contains contract header information.
**Key Columns:**
- `I3D` - Primary key (identity)
- `Nummer` - Contract number
- `KundenI3D` - Customer reference
- `Datum` - Contract date
- `Status` - Contract state
- `Berechnungsart` - Calculation method
- `AutoVerlaengerung` - Auto-renewal flag
- `AbrechnungsIntervallArt` - Billing interval type
- `AbrechnungsIntervallDauer` - Billing interval duration
- `VertragsBeginn` - Contract start date
- `VertragsEnde` - Contract end date
- `KuendigungsDatum` - Termination date
- `ErsteBezahlung` - First payment date
- `KontingentWert` - Contingent value
- `KontingentArt` - Contingent type
- `Automatische Abrechnung` - Automated billing flag
##### VertragPos Table (Contract Items)
**Physical Table:** `dbo.VertragPos`
**Entity Class:** `Centron.DAO.TemporaryEntities.VertragPos`
**Mapping:** `Centron.DAO.Mappings.TemporaryEntities.VertragPosMaps`
The `VertragPos` table contains contract line items and positions.
**Key Columns:**
- `I3D` - Primary key (identity)
- `VertragKopfI3D` - Foreign key to contract header
- `Pos` - Position number for ordering
- `ArtikelI3D` - Article/product reference
- `Text` - Item description
- `Stk` - Quantity
- `VKKalk` - Sales price calculation
- `EK` - Purchase price
- `MwstI3D` - VAT rate reference
- `VertragI3D` - Contract reference for recurring items
- `Lieferdatum` - Delivery date
- `Benachrichtigungsdatum` - Notification date
##### Contract Version Tables
**Version History Tables:**
- `VertragKopfVersions` - Contract header version history
- `VertragPosVersions` - Contract items version history
**Critical Architecture Detail:** Version tables are **exact 1:1 copies** of their corresponding original tables (`VertragKopf` and `VertragPos`). This means:
- Every column that exists in `VertragKopf` must also exist in `VertragKopfVersions` with identical data types
- Every column that exists in `VertragPos` must also exist in `VertragPosVersions` with identical data types
- The only exceptions are system columns (`I3D`, `OriginalI3D`) which are handled specially
- Additional versioning columns are added: `OriginalI3D` (references original record) and `KopfVersionsI3D` (for position tables)
**Contract Version Creation Process:**
When a contract version is saved (using `AssetHeadDAO.SaveAssetVersion` mechanism):
```sql
-- Save contract header version
INSERT INTO VertragKopfVersions (all_columns_except_I3D, OriginalI3D)
SELECT all_columns_except_I3D, I3D AS OriginalI3D
FROM VertragKopf
WHERE I3D = @contractId
-- Save contract items version
INSERT INTO VertragPosVersions (all_columns_except_I3D, OriginalI3D, KopfVersionsI3D)
SELECT all_columns_except_I3D, I3D AS OriginalI3D, @headerVersionId AS KopfVersionsI3D
FROM VertragPos
WHERE VertragKopfI3D = @contractId
```
This enables complete audit trails of all contract modifications, rollback capability, and change tracking functionality.
**⚠️ Schema Maintenance Warning:** When adding new columns to `VertragKopf` or `VertragPos`, the identical columns must also be added to their corresponding version tables (`VertragKopfVersions` and `VertragPosVersions`). Failure to maintain this 1:1 correspondence will cause runtime errors during version saving operations.
#### Modern Contract Views
##### Contracts View (Contract Headers)
**Database View:** `dbo.Contracts`
**Purpose:** Clean, English-named view of VertragKopf for C# application use
**Benefits:**
- Consistent English naming convention
- Cleaner column structure
- Type safety improvements
- Better integration with ORM mapping
##### ContractItems View (Contract Items)
**Database View:** `dbo.ContractItems`
**Purpose:** Clean, English-named view of VertragPos for C# application use
##### Contract Version Views
**Version Views:**
- `ContractVersions` - Clean view of VertragKopfVersions
- `ContractItemVersions` - Clean view of VertragPosVersions
#### Contract Logging Integration
##### AnlageLog Integration
Contracts integrate with the centralized `AnlageLog` table for audit logging:
**Contract Log Entries:**
- `AnlageArt = 22` (Contract identifier)
- `AnlageI3D` - References contract I3D from VertragKopf
- Log entries for contract creation, modifications, billing events, renewals, and terminations
**Typical Log Events:**
- Contract creation and approval
- Billing interval changes
- Contingent modifications
- Device associations/removals
- Automated billing execution
- Contract renewals and terminations
## Business Logic Architecture
### ReceiptContractBL
**Location:** `src/backend/Centron.BL/Sales/Receipts/ContractLists/ReceiptContractBL.cs`
The `ReceiptContractBL` class provides contract-specific business logic extending the base receipt functionality.
#### Key Responsibilities
**Contract Invoice Management:**
- `DeactivateContractInvoice()`: Deactivate invoices generated from contracts
- `GetContractInfosFromInvoices()`: Extract contract information from invoices
- `ExistsInvoiceForContract()`: Check if invoices exist for a contract
**Device and Counter Management:**
- `ResetDeviceClickCounter()`: Reset click counters for printer/copier contracts
- `UpdateDeviceToContract()`: Associate devices with contracts
- `CheckCounterHistory()`: Validate counter readings history
**Contingent and Billing:**
- `ContractContingentBalanceCalculation()`: Calculate contingent balances
- `CalculateContingentWithRecalculationArticle()`: Handle contingent recalculations
- `UpdateContractContingentBalanceCalculationForReceiptChange()`: Adjust balances when receipts change
- `UpdateTakeRestAndOverBooking()`: Handle remainder and overbooking scenarios
**Master Data List Management:**
- `AddMasteDateListsToContract()`: Associate master data lists with contracts
- `CreateMasterDataListsForNewMspArticles()`: Create lists for new MSP articles
- `RemoveMasterDataList()`: Remove master data list associations
- `CheckRemovedMasterDataList()`: Validate removed associations
**Contract-Specific Item Processing:**
- `GetContractRelevantItems()`: Retrieve items relevant for contract billing
- `UpdatePriceForContractRelevantItem()`: Update pricing for contract items
- `SaveContractPositionCounter()`: Persist counter readings
- `SaveContractFreeCopies()`: Handle free copy allowances
### ContractSpecificLogic
**Location:** `src/backend/Centron.BL/Sales/Receipts/ContractLists/ContractSpecificLogic.cs`
This class implements contract-specific operations that are called by the main `ReceiptBL` through the `SpecificLogics` pattern.
### Related Business Logic Classes
#### ContractBL
**Location:** `src/backend/Centron.BL/Sales/CustomerAssets/Contracts/ContractBL.cs`
Handles broader contract asset management beyond the receipt functionality:
- Contract lifecycle management
- Device associations and management
- Contract analysis and reporting
- Integration with customer asset management
#### AutomaticFacturaBL.Contracts
**Location:** `src/backend/Centron.BL/Sales/CustomerAssets/AutomaticFactura/AutomaticFacturaBL.Contracts.cs`
Manages automated billing for contracts:
- Automatic invoice generation based on billing intervals
- RMM (Remote Monitoring and Management) integration
- Billing parameter calculation
- Multi-interval billing support
## Contract Workflows
### Contract Creation Process
1. **Initialize Contract**
```csharp
var contract = new ReceiptContract
{
CustomerI3D = customerId,
Date = DateTime.Now,
BillingIntervalKind = BillingIntervalKinds.Monthly,
BillingIntervalDuration = 1,
AutomatedBilling = true
};
```
2. **Configure Billing Parameters**
- Set billing intervals and calculation methods
- Define payment conditions and terms
- Configure contingent limits if applicable
3. **Add Contract Items**
- Products and services to be provided
- Pricing and quantity information
- Device associations for maintenance contracts
4. **Set Up Automation**
- Enable automated billing if required
- Configure renewal settings
- Set up monitoring and alerts
### Automated Billing Process
The automated billing system (`AutomaticFacturaBL`) processes contracts based on their billing intervals:
1. **Contract Evaluation**
- Check contracts due for billing
- Validate billing parameters and dates
- Verify customer and contract status
2. **Invoice Generation**
- Create invoice based on contract items
- Apply pricing rules and calculations
- Handle contingent deductions
3. **Post-Processing**
- Update contract billing dates
- Generate documents and notifications
- Update contingent balances
### Device Management Integration
For maintenance and service contracts:
1. **Device Association**
- Link devices to contracts through master data lists
- Track serial numbers and device information
- Monitor device status and warranty information
2. **Counter Reading Management**
- Collect meter readings for copiers/printers
- Calculate usage-based billing amounts
- Handle free copy allowances and overages
3. **Service Integration**
- Connect with RMM systems for automated data collection
- Process device monitoring data
- Generate alerts for maintenance requirements
## Configuration Options
### Billing Interval Configuration
**BillingIntervalKind Options:**
- `Daily`: Daily billing cycles
- `Monthly`: Monthly billing cycles
- `Quarterly`: Quarterly billing cycles
- `Yearly`: Annual billing cycles
**BillingIntervalDuration:**
- Number of intervals (e.g., 3 months for quarterly when kind is Monthly)
- Supports flexible billing periods
### Calculation Methods
**ContractCalculationKind Options:**
- Standard calculation methods for different contract types
- Custom calculation logic for specialized scenarios
**ContractNeedCalcKind:**
- Defines calculation requirements and triggers
- Controls when recalculations are needed
## Integration Points
### Customer Asset Management
- Integration with device and asset tracking systems
- Warranty and maintenance schedule management
- Service history and documentation
### Accounting System
- Automated journal entry generation for contract billing
- Revenue recognition for service contracts
- Contingent liability tracking
### External Systems
- **RMM Integration**: Remote monitoring and management systems
- **Device APIs**: Direct device communication for counter readings
- **Customer Portals**: Web-based contract management interfaces
## Performance Considerations
### Billing Performance
- **Batch Processing**: Large contract sets processed in batches
- **Parallel Processing**: Multiple contracts processed simultaneously where possible
- **Caching**: Frequently accessed contract data cached for performance
### Database Optimization
- **Indexed Relationships**: Foreign keys properly indexed
- **Partitioning**: Large contract tables partitioned by date ranges
- **Archive Strategy**: Old contract data archived for performance
## Security and Compliance
### Access Control
- **Role-Based Permissions**: Different access levels for contract operations
- **Branch Isolation**: Contracts accessible only to authorized branches
- **Customer Isolation**: Cross-customer data protection
### Audit Requirements
- **Change Tracking**: Complete audit trail for all contract modifications
- **Billing History**: Detailed logging of all billing operations
- **Compliance Reporting**: Support for regulatory reporting requirements
## Best Practices
### Contract Design
- **Clear Billing Intervals**: Use consistent and predictable billing cycles
- **Contingent Management**: Monitor contingent usage to prevent overruns
- **Device Integration**: Properly associate devices for accurate billing
### Development Guidelines
- **Use ContractBL**: Leverage existing contract business logic
- **Handle Contingents**: Always consider contingent impacts in calculations
- **Validate Intervals**: Ensure billing interval consistency
- **Test Automation**: Thoroughly test automated billing scenarios
### Troubleshooting
- **Billing Issues**: Check interval configuration and calculation settings
- **Device Problems**: Verify master data list associations
- **Performance Issues**: Review indexing and query optimization
- **Integration Failures**: Validate external system connections and data formats
@@ -0,0 +1,356 @@
# Receipt Search Architecture
This document explains how the receipt search system works in the c-entron.NET backend, including filter application, shared logic across receipt types, and how to add new searchable properties.
## Overview
The receipt search system provides a unified search interface across all receipt types (offers, orders, delivery lists, invoices, contracts, credit vouchers, pickup lists, and supplier receipts). It uses a configurable, extensible architecture that allows different receipt types to implement their own search logic while sharing common filtering capabilities.
## Architecture Components
### 1. REST API Layer
**Entry Point:** `CentronRestService.SearchReceiptsThroughPaging`
- **Location:** `src/webservice/Centron.Host/Services/CentronRestServiceParts/CentronRestService.Receipts.cs`
- **Method:** `SearchReceiptsThroughPaging(Request<SearchReceiptsThroughPagingRequest> request)`
```csharp
public Response<ReceiptSearchItemPagingDTO> SearchReceiptsThroughPaging(Request<SearchReceiptsThroughPagingRequest> request)
{
var result = this.Session.GetBL<ReceiptSearchWebServiceBL>().SearchReceipts(
this.GetLoggedInUserByTicket(request.Ticket),
request.Data.Filter,
request.Data.Page,
request.Data.EntriesPerPage);
return Response<ReceiptSearchItemPagingDTO>.FromBLResult(result);
}
```
### 2. Business Logic Layer
**Primary Class:** `ReceiptSearchWebServiceBL`
- **Location:** `src/backend/Centron.BL/WebServices/Sales/Receipts/ReceiptSearch/ReceiptSearchWebServiceBL.cs`
- **Responsibility:** Coordinates search operations, handles pagination, and manages user context
```csharp
public Result<ReceiptSearchItemPagingDTO> SearchReceipts(LoggedInUser user,
ReceiptSearchFilter filter, int page, int entriesPerPage)
{
var receipts = new ReceiptSearcher(this.Session).SearchReceipts(filter, user);
// Apply pagination and return results
ReceiptSearchItemPagingDTO pagingDTO = new ReceiptSearchItemPagingDTO()
{
Count = receipts.Count,
CurrentPage = page,
PageCount = (int)Math.Ceiling(receipts.Count/(decimal) entriesPerPage),
Result = receipts.OrderByDescending(o => o.Date).Skip((page - 1) * entriesPerPage).Take(entriesPerPage).ToList()
};
return Result<ReceiptSearchItemPagingDTO>.AsSuccess(pagingDTO);
}
```
### 3. Core Search Engine
**Primary Class:** `ReceiptSearcher`
- **Location:** `src/backend/Centron.BL/WebServices/Sales/Receipts/ReceiptSearch/ReceiptSearcher.cs`
- **Responsibility:** Executes searches across all receipt types using configuration-driven SQL generation
#### Key Features:
- **Multi-Receipt Type Support:** Searches across multiple receipt types simultaneously
- **Configuration-Driven:** Uses `ReceiptSearchConfiguration` classes for each receipt type
- **Raw SQL Execution:** Generates and executes optimized SQL queries for performance
- **User Context Handling:** Applies user-specific filters (web accounts, branches, permissions)
#### Search Process:
1. **Filter Preparation:** Adjusts filter based on user context (web accounts, permissions)
2. **Configuration Iteration:** Loops through all receipt type configurations
3. **SQL Generation:** Creates receipt-type-specific SQL queries with parameters
4. **Query Execution:** Executes raw SQL with 5-minute timeout
5. **Result Aggregation:** Combines results from all receipt types
6. **Result Sorting:** Orders by ObjectKind, then by Number descending
```csharp
public IList<ReceiptSearchItemDTO> SearchReceipts(ReceiptSearchFilter filter, LoggedInUser user)
{
var result = new List<ReceiptSearchItemDTO>();
this.PrepareFilterForWebAccounts(filter, user);
foreach (var configuration in this._receiptSearchConfigurations)
{
if (filter.ReceiptKinds == null || filter.ReceiptKinds.Count == 0 || filter.ReceiptKinds.Contains(configuration.ReceiptKind))
{
var query = this.CreateSqlStatementAndParameters(configuration, filter, user);
if (query == null) continue; // Receipt type doesn't support this filter
var sqlStatement = query.Item1;
var parameters = query.Item2;
var receipts = this._rawSqlAccessDAO.ExecuteQuery<ReceiptSearchItemDTO>(sqlStatement, parameters, timeout: TimeSpan.FromMinutes(5));
result.AddRange(receipts);
}
}
return result.OrderBy(f => f.ObjectKind).ThenByDescending(f => f.Number).ToList();
}
```
### 4. Filter Definition
**Class:** `ReceiptSearchFilter`
- **Location:** `src/backend/Centron.Interfaces/Sales/Receipts/ReceiptSearch/ReceiptSearchFilter.cs`
- **Responsibility:** Defines all available search criteria
#### Available Filter Properties:
- **Basic Search:** `SearchText`, `ReceiptNumber`, `ReceiptNumbers`
- **Date Range:** `DateFrom`, `DateTo`, `ChangedAfterDate`
- **Account/Customer:** `AccountI3D`, `AccountNumbers`, `AccountName`
- **Receipt Types:** `ReceiptKinds` (controls which receipt types to search)
- **Status:** `IncludeClosedReceipts`, `OnlyOwn`, `OnlyOwnBranch`
- **Financial:** `GrossPriceFrom`, `GrossPriceTo`, `PaymentConditionI3D`, `DeliveryConditionI3D`
- **Specialized:** `HourlySurchargeRateI3Ds`, `ContractKindI3Ds`, `ArticleI3Ds`, `CampaignI3D`
- **Items:** `SearchInReceiptItemText`, `ReceiptItemI3D`
- **Advanced:** `IsCart`, `OnlyNonCarts`, `IsDownPaymentInvoice`, `IsReceiptTemplate`
### 5. Configuration System
**Base Class:** `ReceiptSearchConfiguration`
- **Location:** `src/backend/Centron.BL/WebServices/Sales/Receipts/ReceiptSearch/ReceiptSearchConfiguration.cs`
- **Responsibility:** Defines the contract for receipt-type-specific search configurations
#### Configuration Properties:
- **Basic Properties:**
- `ReceiptKind`: Identifies the receipt type (CentronObjectKindNumeric)
- `GetBaseSelectStatement()`: Returns the base SELECT query for this receipt type
- `OnlyActiveWhereStatement`: SQL for filtering active receipts
- **Filter WHERE Statements:** Each filter property has a corresponding WHERE clause property:
- `AccountI3DWhereStatement`: SQL for filtering by account
- `ReceiptNumberWhereStatement`: SQL for filtering by receipt number
- `DateFromWhereStatement`, `DateToWhereStatement`: Date range filtering
- `SearchTextWhereStatement`: Full-text search implementation
- And many more...
- **Permission Integration:**
- `ShowRight`: Required right to view receipts of this type
- `OnlyOwnRight`: Right that restricts to user's own receipts
- `OnlyOwnBranchRight`: Right that restricts to user's branch
#### Receipt Type Configurations:
- `OfferReceiptSearchConfiguration` - Offers (AngKopf)
- `OrderReceiptSearchConfiguration` - Orders (AufKopf)
- `DeliveryListReceiptSearchConfiguration` - Delivery Lists (LiefKopf)
- `InvoiceReceiptSearchConfiguration` - Invoices (RechKopf)
- `ContractReceiptSearchConfiguration` - Contracts (VertragKopf)
- `CreditVoucherReceiptSearchConfiguration` - Credit Vouchers (GutKopf)
- `PickupListReceiptSearchConfiguration` - Pickup Lists (AbholKopf)
- Plus supplier variants for each type
## SQL Generation Process
The `ReceiptSearcher.CreateSqlStatementAndParameters` method builds SQL queries dynamically based on the provided filter and receipt type configuration:
### 1. Base Query Construction
```csharp
var baseSelect = configuration.GetBaseSelectStatement(filter);
var builder = new StringBuilder(baseSelect);
```
### 2. Filter Application
For each filter property that has a non-null/non-empty value:
```csharp
if (filter.AccountI3D != null && filter.AccountI3D > 0)
{
var accountWhereStatement = configuration.AccountI3DWhereStatement;
if (string.IsNullOrWhiteSpace(accountWhereStatement))
return null; // This receipt type doesn't support this filter
builder.AppendLine(accountWhereStatement);
parameters.Add(new NamedQueryParameter("AccountI3D", filter.AccountI3D, NHibernateUtil.Int32));
}
```
### 3. Permission Checks
```csharp
if (configuration.ShowRight.HasValue && !this._appRightsBL.HasRight(user.AppUser, configuration.ShowRight.Value))
{
return null; // User doesn't have permission to search this receipt type
}
```
### 4. Active Receipts Filter
```csharp
if (!filter.IncludeClosedReceipts)
{
builder.AppendLine(configuration.OnlyActiveWhereStatement);
}
```
## Adding New Filter Properties
To add a new searchable property (e.g., `IsHiddenInHelpdesk`), follow these steps:
### Step 1: Add to ReceiptSearchFilter
Add the new property to `ReceiptSearchFilter.cs`:
```csharp
[DataMember]
public bool? IsHiddenInHelpdesk { get; set; }
```
### Step 2: Add to Base Configuration
Add the corresponding WHERE statement property to `ReceiptSearchConfiguration.cs`:
```csharp
public virtual string IsHiddenInHelpdeskWhereStatement { get; } = null;
```
### Step 3: Update Receipt Type Configurations
For each receipt type that supports the new filter, implement the WHERE statement:
**Example for ContractReceiptSearchConfiguration:**
```csharp
public override string IsHiddenInHelpdeskWhereStatement => "AND AK.IsHiddenInHelpdesk = :IsHiddenInHelpdesk";
```
### Step 4: Update SQL Generation
Add the filter logic to `ReceiptSearcher.CreateSqlStatementAndParameters`:
```csharp
if (filter.IsHiddenInHelpdesk != null)
{
var isHiddenInHelpdeskWhere = configuration.IsHiddenInHelpdeskWhereStatement;
if (string.IsNullOrWhiteSpace(isHiddenInHelpdeskWhere))
return null; // This receipt type doesn't support this filter
builder.AppendLine(isHiddenInHelpdeskWhere);
parameters.Add(new NamedQueryParameter("IsHiddenInHelpdesk", filter.IsHiddenInHelpdesk.Value, NHibernateUtil.Boolean));
}
```
### Step 5: Database Schema Requirements
Ensure the underlying database tables and views include the new column:
- **Tables:** Add to base tables (e.g., `VertragKopf`) and version tables (e.g., `VertragKopfVersions`)
- **Views:** Update views (e.g., `Contracts`, `ContractVersions`) to include the new column
## Shared Logic Across Receipt Types
The system achieves code reuse through several mechanisms:
### 1. Configuration-Driven Architecture
- Common filter logic is implemented once in `ReceiptSearcher`
- Receipt-type-specific behavior is encapsulated in configuration classes
- New receipt types can be added by implementing a new configuration class
### 2. Base SELECT Queries
Each configuration provides a standardized SELECT query that returns `ReceiptSearchItemDTO` properties:
```csharp
public override string GetBaseSelectStatement(ReceiptSearchFilter filter)
{
return @"
SELECT
I3D = AK.I3D,
ObjectKind = 22,
Number = AK.Nummer,
Version = AK.Version,
Caption = AK.Zusatztext,
Date = AK.Datum,
Receiver = AK.Empfaenger,
AccountI3D = AK.KundenID,
-- ... more fields
FROM VertragKopf AK
-- ... joins
WHERE 1=1"; // Base WHERE clause for dynamic filter appending
}
```
### 3. Parameter Handling
- All configurations use named parameters (`:ParameterName`)
- Parameter types are consistently defined using NHibernate types
- Array parameters support IN clauses for multiple values
### 4. Permission Integration
- Rights checking is standardized across all receipt types
- Each configuration can define specific rights for viewing, own-only, and branch-only access
- Permission failures result in null queries (no results for that receipt type)
## Performance Considerations
### 1. Raw SQL Execution
- Uses raw SQL instead of LINQ/HQL for optimal performance
- Timeout set to 5 minutes for complex searches
- Transaction isolation levels configured for consistency
### 2. Pagination
- Results are paginated at the business logic level
- Sorting is applied after aggregation (may impact performance for large result sets)
- Consider implementing database-level pagination for very large datasets
### 3. Index Requirements
- Ensure all filterable columns are properly indexed
- Foreign key columns should have indexes
- Date range queries benefit from composite indexes
### 4. Query Optimization
- Each receipt type can optimize its base query independently
- Complex joins are handled in the base SELECT statement
- WHERE clauses are appended dynamically to avoid query plan issues
## Security and Permissions
### 1. User Context Handling
- Web account users are automatically filtered to their associated customer
- Employee users can be restricted by branch or ownership
- Permission checking prevents unauthorized access to receipt types
### 2. SQL Injection Prevention
- All user input is parameterized
- No dynamic SQL concatenation with user values
- Named parameters ensure type safety
### 3. Branch Isolation
- Branch-specific filtering can be enforced per receipt type
- User's branch context is automatically applied where configured
## Testing Strategies
### 1. Unit Testing
- Test individual configuration classes in isolation
- Mock filter scenarios for comprehensive coverage
- Verify SQL generation for all filter combinations
### 2. Integration Testing
- Test complete search workflows with real data
- Verify permission enforcement
- Test pagination and sorting behavior
### 3. Performance Testing
- Measure query execution times for large datasets
- Test timeout behavior under load
- Validate index effectiveness
## Future Enhancements
### 1. Elasticsearch Integration
- Consider moving to Elasticsearch for full-text search capabilities
- Maintain SQL for structured filtering
- Hybrid approach for optimal performance
### 2. Real-time Filtering
- Implement WebSocket-based real-time updates
- Consider caching frequently accessed search results
### 3. Advanced Search Features
- Saved search queries
- Search history
- Search result highlighting
## Conclusion
The receipt search system provides a robust, extensible architecture for searching across all receipt types in the c-entron.NET system. By following the established patterns for adding new filter properties, developers can easily extend search capabilities while maintaining consistency and performance across the entire system.
The configuration-driven approach ensures that new receipt types can be added with minimal impact on existing code, while the shared search logic provides consistency and maintainability across all receipt types.
@@ -0,0 +1,371 @@
# Receipts Backend Architecture
This document describes the generic architecture and components of the receipts system in the c-entron.NET backend, which provides a unified foundation for all receipt types including offers, orders, delivery lists, invoices, contracts, and credit vouchers.
## Overview
The receipts system follows a layered architecture pattern with a shared base implementation that is extended by specific receipt types. All receipt types inherit from common base classes and share fundamental operations while providing specialized functionality through their own business logic classes.
## Core Components
### Entity Layer
#### ReceiptBase Abstract Class
**Location:** `src/backend/Centron.Entities/Entities/Sales/Receipts/ReceiptBase.cs`
The `ReceiptBase` abstract class serves as the foundation for all receipt entities in the system. It inherits from `BaseEntity` and implements the `IReceiptBase` interface.
**Key Properties:**
- **Receipt Header Information:** Number, Date, Version, State, Editor
- **Branch Information:** BranchI3D, BranchOrigin
- **Currency Information:** CurrencyI3D, CurrencyFactor, CurrencyString, ExclusiveOfVAT
- **Contact Information:** Receiver, Phone, Fax, Email
- **Address Information:** AddressI3D, ContactPersonI3D, Street, PostOfficeBox, Zip, City, ContactName, CountryI3D
- **Audit Fields:** CreatedByI3D, CreatedAt, ChangedByI3D, ChangedAt, Application Version tracking
- **System Fields:** ConcurrencyControlGuid, CustomUpdateArticlePricesAndTexts
**Abstract Methods:**
- `ReceiptKind`: Returns the specific receipt type (CentronObjectKindNumeric)
- `GetReceiptItems()`: Returns all receipt items
- `SetReceiptItems()`: Sets receipt items collection
- `AddItem()`: Adds a new item to the receipt
- `RemoveItem()`: Removes an item from the receipt
#### Receipt Types Hierarchy
All receipt types extend `ReceiptBase` and follow a consistent pattern of entity classes, database tables, and views:
| Receipt Type | Entity Class | Database Table | Database View | Items Table | Items View |
|--------------|--------------|----------------|---------------|-------------|------------|
| **Offers** | `ReceiptOffer` | `AngKopf` | `Offers` | `AngPos` | `OfferItems` |
| **Orders** | `ReceiptOrder` | `AufKopf` | `Orders` | `AufPos` | `OrderItems` |
| **Delivery Lists** | `ReceiptDeliveryList` | `LiefKopf` | `DeliveryLists` | `LiefPos` | `DeliveryListItems` |
| **Invoices** | `ReceiptInvoice` | `RechKopf` | `Invoices` | `RechPos` | `InvoiceItems` |
| **Contracts** | `ReceiptContract` | `VertragKopf` | `Contracts` | `VertragPos` | `ContractItems` |
| **Credit Vouchers** | `ReceiptCreditVoucher` | `GutKopf` | `CreditVouchers` | `GutPos` | `CreditVoucherItems` |
| **Pickup Lists** | `ReceiptPickupList` | `AbholKopf` | `PickupLists` | `AbholPos` | `PickupListItems` |
**Entity Locations:**
- `src/backend/Centron.Entities/Entities/Sales/Receipts/Offers/ReceiptOffer.cs`
- `src/backend/Centron.Entities/Entities/Sales/Receipts/Orders/ReceiptOrder.cs`
- `src/backend/Centron.Entities/Entities/Sales/Receipts/DeliveryLists/ReceiptDeliveryList.cs`
- `src/backend/Centron.Entities/Entities/Sales/Receipts/Invoices/ReceiptInvoice.cs`
- `src/backend/Centron.Entities/Entities/Sales/Receipts/ContractLists/ReceiptContract.cs`
- `src/backend/Centron.Entities/Entities/Sales/Receipts/CreditVouchers/ReceiptCreditVoucher.cs`
- `src/backend/Centron.Entities/Entities/Sales/Receipts/PickupLists/ReceiptPickupList.cs`
## Database Schema
### Dual Layer Architecture: Tables and Views
The receipts system uses a dual-layer database architecture consisting of legacy tables and modern views. This design maintains backward compatibility while providing cleaner interfaces for the C# application.
#### Legacy Tables (German Names)
The original database tables use German naming conventions and contain historical structure:
**Header Tables (*Kopf):**
- `AngKopf` - Offer headers
- `AufKopf` - Order headers
- `LiefKopf` - Delivery list headers
- `RechKopf` - Invoice headers
- `VertragKopf` - Contract headers
- `GutKopf` - Credit voucher headers
- `AbholKopf` - Pickup list headers
**Position Tables (*Pos):**
- `AngPos` - Offer items
- `AufPos` - Order items
- `LiefPos` - Delivery list items
- `RechPos` - Invoice items
- `VertragPos` - Contract items
- `GutPos` - Credit voucher items
- `AbholPos` - Pickup list items
**Version Tables (*Versions):**
Each receipt type maintains version history through dedicated version tables:
- `AngKopfVersions` / `AngPosVersions` - Offer version history
- `AufKopfVersions` / `AufPosVersions` - Order version history
- `LiefKopfVersions` / `LiefPosVersions` - Delivery list version history
- `RechKopfVersions` / `RechPosVersions` - Invoice version history
- `VertragKopfVersions` / `VertragPosVersions` - Contract version history
- `GutKopfVersions` / `GutPosVersions` - Credit voucher version history
- `AbholKopfVersions` / `AbholPosVersions` - Pickup list version history
#### Modern Views (English Names)
For C# application compatibility, cleaner views with English names are used:
**Header Views:**
- `Offers` - Clean view of AngKopf
- `Orders` - Clean view of AufKopf
- `DeliveryLists` - Clean view of LiefKopf
- `Invoices` - Clean view of RechKopf
- `Contracts` - Clean view of VertragKopf
- `CreditVouchers` - Clean view of GutKopf
- `PickupLists` - Clean view of AbholKopf
**Item Views:**
- `OfferItems` - Clean view of AngPos
- `OrderItems` - Clean view of AufPos
- `DeliveryListItems` - Clean view of LiefPos
- `InvoiceItems` - Clean view of RechPos
- `ContractItems` - Clean view of VertragPos
- `CreditVoucherItems` - Clean view of GutPos
- `PickupListItems` - Clean view of AbholPos
**Version Views:**
- `OfferVersions` / `OfferItemVersions` - Offer version views
- `OrderVersions` / `OrderItemVersions` - Order version views
- `DeliveryListVersions` / `DeliveryListItemVersions` - Delivery list version views
- `InvoiceVersions` / `InvoiceItemVersions` - Invoice version views
- `ContractVersions` / `ContractItemVersions` - Contract version views
- `CreditVoucherVersions` / `CreditVoucherItemVersions` - Credit voucher version views
- `PickupListVersions` / `PickupListItemVersions` - Pickup list version views
### Shared Logging Infrastructure
#### AnlageLog Table
**Purpose:** Centralized logging for all receipt types
**Structure:** Shared table with receipt type differentiation
**Key Columns:**
- `AnlageI3D` - References the specific receipt's I3D
- `AnlageArt` - Receipt type identifier (corresponds to ObjectKind)
- Log entry details and timestamps
**AnlageArt Values:**
- `1` = Offer
- `2` = Order
- `3` = Delivery List
- `4` = Invoice
- `5` = Pickup List
- `6` = Credit Voucher
- `22` = Contract
This pattern (`ObjectI3D` + `ObjectKind` / `AnlageI3D` + `AnlageArt`) is used throughout the system for shared references across different entity types.
### Schema Maintenance
#### Version Tables: 1:1 Copies of Original Tables
**Critical Requirement:** Version tables (`*KopfVersions`, `*PosVersions`) are exact 1:1 copies of their corresponding original tables. This means **every column that exists in the base table must also exist in the version table** with identical structure and data types.
**Version Table Structure:**
- Contains all columns from the original table
- Excludes certain system columns (I3D, OriginalI3D)
- Adds `OriginalI3D` column to reference the original record
- Adds `KopfVersionsI3D` column (for *Pos version tables) to reference the header version
**Versioning Implementation Example:**
The versioning mechanism (as seen in `AssetHeadDAO.SaveAssetVersion`) works by:
```sql
-- Copy header record to version table
INSERT INTO AngKopfVersions (all_columns_except_I3D, OriginalI3D)
SELECT all_columns_except_I3D, I3D AS OriginalI3D
FROM AngKopf
WHERE I3D = @receiptId
-- Copy all item records to version table
INSERT INTO AngPosVersions (all_columns_except_I3D, OriginalI3D, KopfVersionsI3D)
SELECT all_columns_except_I3D, I3D AS OriginalI3D, @headerVersionId AS KopfVersionsI3D
FROM AngPos
WHERE AngKopfI3D = @receiptId
```
#### Adding New Columns - Complete Checklist
When adding new columns to receipts, **all** of the following must be updated:
1. **Base Table:** Add column to the legacy table (e.g., `AngKopf`)
2. **Version Table:** Add the **identical column** to the version table (e.g., `AngKopfVersions`)
3. **Update Views:** Modify both the main view (e.g., `Offers`) and version view (e.g., `OfferVersions`) to include the new column
4. **Entity Classes:** Add the property to the C# entity class (e.g., `ReceiptOffer`) and version entity if applicable
5. **Mapping Classes:** Update NHibernate mapping classes for ORM functionality
6. **Temporary Legacy Entities:** Add the property to the matching `*Kopf` / `*Pos` temporary entity under `src/backend/Centron.Entities/Entities/DbEntities/`
7. **Temporary Entity Mappings:** Add the mapping to the matching class under `src/backend/Centron.DAO/Mappings/TemporaryEntities/`
8. **SaveReceipt Repository:** Copy the value in the receipt-type-specific `SaveReceipt*Repository` (`SynchronizeReceiptData` for header fields, `SynchronizeReceiptItemData` for item fields)
9. **DTOs and Interfaces:** Add the property to webservice DTOs and relevant receipt interfaces when the value crosses BL/WebService/UI boundaries
10. **Version Views:** Ensure version views (`OfferVersions`, `OfferItemVersions`) include the new column
**⚠️ Critical Warning:** Forgetting to add a column to the version table will cause runtime errors when the versioning system attempts to copy records. The `DoGetFieldList()` method dynamically generates field lists, so missing columns in version tables will break the INSERT statements.
**Critical Save Warning:** The normal NHibernate receipt entity mapping is not the only persistence path. Receipt saves go through legacy `SaveReceipt*Repository` classes, which synchronize the modern receipt entities into temporary legacy table entities (`RechKopf`, `RechPos`, `LiefKopf`, `LiefPos`, etc.) before writing to the database. If a new field is only added to the modern entity/view mapping but not to the temporary entity, temporary mapping, and `SaveReceipt*Repository`, the value may load correctly from the view but will not be persisted on save.
#### Version Table Maintenance Process
**For Header Tables (*Kopf → *KopfVersions):**
1. Add column to base table: `ALTER TABLE AngKopf ADD NewColumn datatype`
2. Add identical column to version table: `ALTER TABLE AngKopfVersions ADD NewColumn datatype`
3. Update corresponding views to include the new column
**For Item Tables (*Pos → *PosVersions):**
1. Add column to base table: `ALTER TABLE AngPos ADD NewColumn datatype`
2. Add identical column to version table: `ALTER TABLE AngPosVersions ADD NewColumn datatype`
3. Update corresponding views to include the new column
This strict 1:1 correspondence ensures that the automatic versioning system (`AssetHeadDAO.SaveAssetVersion` and similar methods) can create complete snapshots of receipt states for audit trails and change tracking.
### Business Logic Layer
#### ReceiptBL - Core Business Logic
**Location:** `src/backend/Centron.BL/Sales/Receipts/ReceiptBL.cs`
The `ReceiptBL` class provides the central business logic for all receipt operations. It contains over 10,000 lines of code handling comprehensive receipt management functionality.
**Key Responsibilities:**
- **CRUD Operations:** Generic methods for loading, searching, and saving receipts
- **Receipt Processing:** State management, workflow processing, validation
- **Item Management:** Adding, updating, removing receipt items
- **Price Calculations:** Tax calculations, discounts, currency conversions
- **Document Generation:** PDF generation, printing, email sending
- **Integration:** Connection with accounting systems, warehousing, customer management
- **Workflow Management:** Approval processes, state transitions
- **Reporting:** Export functionality (Excel, PDF)
**Core Methods:**
- `GetReceiptByI3D<T>(int receiptI3D)`: Generic receipt retrieval
- `GetReceipts<T>(IReceiptFilter filter)`: Search receipts with filtering
- `SaveReceipt<T>(T receipt)`: Generic receipt saving
- `DeleteReceipt(int receiptI3D, CentronObjectKindNumeric receiptKind)`: Receipt deletion
- `ExportReceiptToExcel()`: Excel export functionality
#### SpecificLogics Pattern
The `ReceiptBL` utilizes a `SpecificLogics` helper class that delegates specialized operations to receipt-type-specific business logic classes:
- **ContractSpecificLogic** for contracts
- **InvoiceSpecificLogic** for invoices
- **OrderSpecificLogic** for orders
- And similar classes for other receipt types
### Data Access Layer
#### Repository Pattern
Each receipt type has its own repository for data persistence:
- **SaveReceiptContractRepository** → `VertragKopf` & `VertragPos` tables
- **SaveReceiptInvoiceRepository** → `RechKopf` & `RechPos` tables
- **SaveReceiptOfferRepository** → `AngKopf` & `AngPos` tables
- **SaveReceiptOrderRepository** → `AufKopf` & `AufPos` tables
- **SaveReceiptDeliveryListRepository** → `LiefKopf` & `LiefPos` tables
- **SaveReceiptCreditVoucherRepository** → `GutKopf` & `GutPos` tables
- **SaveReceiptPickupListRepository** → `AbholKopf` & `AbholPos` tables
- And similar repositories for supplier receipt types
These repositories are a legacy persistence layer between the normal NHibernate receipt entities and the database. They create or update temporary table entities and explicitly assign many properties. When adding a persisted receipt header or item field, update the repository method that synchronizes that level:
- Header field: `SynchronizeReceiptData(...)`
- Item field: `SynchronizeReceiptItemData(...)`
Do not rely on AutoMapper or the modern NHibernate entity mapping for this save path. End-to-end tests are the preferred safety net for new receipt fields because they execute the database script, save through `ReceiptWebServiceBL.SaveReceipt(...)`, reload the receipt, and can assert the raw legacy table values.
#### Database Table Structure
All receipt types follow a consistent two-table pattern:
**Header Tables (*Kopf):**
- Contains receipt-level information (customer, dates, totals, etc.)
- Inherits from `ReceiptTable` base structure
- Primary key: `I3D` (identity column)
- Common audit fields: CreatedAt, ChangedAt, CreatedByI3D, ChangedByI3D
**Position Tables (*Pos):**
- Contains individual line items/positions
- Foreign key reference to header table (*KopfI3D)
- Article information, quantities, prices, and item-specific data
- Primary key: `I3D` (identity column)
- Position number: `Pos` (for ordering)
## Common Workflows
### Receipt Creation Process
1. **Initialize Receipt Entity** - Create new receipt instance with default values
2. **Set Header Information** - Customer, addresses, dates, currency
3. **Add Receipt Items** - Products/services with quantities and prices
4. **Calculate Totals** - Tax calculations, discounts, final amounts
5. **Validate Business Rules** - Check inventory, credit limits, etc.
6. **Save to Database** - Persist header and position records
7. **Generate Document** - Create PDF, send emails if required
### Receipt State Management
Receipts progress through defined states:
- **Draft** - Initial creation, can be freely modified
- **Released** - Approved for processing, limited modifications
- **Processed** - Finalized, minimal changes allowed
- **Cancelled** - Marked as cancelled, read-only
### Item Management
- **Dynamic Item Addition** - Items can be added at any time during draft state
- **Price Calculation** - Automatic recalculation of totals when items change
- **Inventory Integration** - Real-time stock checking and updates
- **Article Linking** - Connection to master article data
## Integration Points
### Customer Management
- Customer data integration for addresses and contact information
- Credit limit checking and payment term assignment
- Customer-specific pricing and discount structures
### Inventory System
- Real-time stock level checking
- Automatic inventory updates on receipt processing
- Serial number and barcode tracking
### Accounting System
- Automatic journal entry generation
- Tax calculation and reporting
- Integration with financial reporting systems
### Document Management
- PDF generation for all receipt types
- Email delivery capabilities
- Document archiving and retrieval
## Extensibility
### Adding New Receipt Types
To add a new receipt type:
1. **Create Entity Classes** - Header and position entities extending base classes
2. **Implement Business Logic** - Specific BL class with type-specific operations
3. **Create Database Tables** - Following the *Kopf/*Pos naming convention
4. **Add Repository Classes** - For data persistence operations
5. **Register with SpecificLogics** - Enable integration with core ReceiptBL
### Customization Points
- **Custom Fields** - Additional properties on receipt entities
- **Business Rules** - Custom validation and processing logic
- **Workflow Extensions** - Additional states and transitions
- **Integration Hooks** - Custom external system connections
## Performance Considerations
### Database Optimization
- **Indexed Foreign Keys** - All *KopfI3D references are indexed
- **Pagination Support** - Large result sets handled via paging
- **Query Optimization** - Efficient queries for common operations
### Memory Management
- **Lazy Loading** - Receipt items loaded on demand
- **Batch Operations** - Bulk processing for multiple receipts
- **Caching Strategy** - Frequently accessed data cached appropriately
## Security
### Access Control
- **User-based Permissions** - Role-based access to receipt functions
- **Branch Isolation** - Users can only access receipts from their branches
- **Audit Trail** - Complete tracking of all receipt changes
### Data Protection
- **Concurrency Control** - GUID-based optimistic locking
- **Data Validation** - Input validation and sanitization
- **Transaction Management** - ACID compliance for all operations
## Best Practices
### Development Guidelines
- **Use Generic Methods** - Leverage ReceiptBL generic operations where possible
- **Follow Inheritance Patterns** - Extend base classes rather than duplicating code
- **Implement Proper Error Handling** - Use try-catch blocks and meaningful error messages
- **Maintain Audit Trails** - Always populate CreatedBy/ChangedBy fields
### Testing Considerations
- **Unit Tests** - Test business logic methods in isolation
- **Integration Tests** - Test complete receipt workflows
- **Database Tests** - Verify data persistence and retrieval
- **Performance Tests** - Ensure acceptable response times under load
@@ -0,0 +1,403 @@
# Anmelden mit Microsoft — Technische Anleitung
## Überblick
Die Funktion "Anmelden mit Microsoft" nutzt **Microsoft Entra ID (Azure AD)** über **OpenID Connect** mit der **MSAL-Bibliothek**. Der Client holt ein ID-Token von Microsoft, schickt es an die c-entron API, die es validiert, den User per Entra Object ID (`oid`-Claim) nachschlägt und ein c-entron Session-Ticket zurückgibt.
---
## Kompletter Flow
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Client App │ │ c-entron API │ │ Microsoft │
│ │ │ (Web Service) │ │ Entra ID │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
1. GET /config/jwt ─────────>│ │
│<── { Authority, │ │
│ Audience, │ │
│ Enabled } │ │
│ │ │
2. MSAL: Token von Microsoft holen ──────────────────> │
│<──────────────────────────── AuthenticationResult│
│ (enthält IdToken) │ │
│ │ │
3. POST /jwt/login ─────────>│ │
│ Authorization: │ │
│ Bearer {IdToken} │ 4. JWT Middleware │
│ Body: { Application, │ validiert Token │
│ AppVersion, │ (Signatur, Issuer, │
│ Device } │ Audience, Lifetime)│
│ │ │
│ │ 5. oid-Claim extrahieren│
│ │ → User in DB suchen │
│ │ (OpenIdConnect- │
│ │ SubjectIdentifier) │
│ │ │
│ │ 6. Ticket erstellen │
│<── "ticket-string" │ │
│ │ │
7. Ticket für alle │ │
weiteren API-Calls nutzen │ │
```
---
## Schritt 1: JWT-Konfiguration abrufen
Anonymer Endpoint — kein Auth nötig.
```http
GET {baseUrl}/config/jwt
```
**Response:**
```json
{
"Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"Audience": "{azure-ad-client-id}",
"Enabled": true
}
```
| Feld | Bedeutung |
|---|---|
| `Authority` | OpenID Connect Authority URL (Entra ID Tenant) |
| `Audience` | Azure AD Application (Client) ID |
| `Enabled` | `true` wenn beide Werte konfiguriert sind |
Wenn `Enabled == false` → OIDC ist nicht konfiguriert, Abbruch.
---
## Schritt 2: ID-Token von Microsoft Entra ID holen (MSAL)
Mit den Werten aus Schritt 1 wird ein MSAL Public Client konfiguriert:
- **Client ID** = `Audience` aus der JWT-Konfiguration
- **Authority** = `Authority` aus der JWT-Konfiguration
- **Scopes** = `["openid", "profile"]`
- **Broker** = Windows WAM (optional, für SSO mit Windows-Anmeldung)
**Ergebnis:** Ein `AuthenticationResult` mit einem `IdToken` (JWT).
> **Wichtig:** Es wird das **ID-Token** verwendet, nicht das Access-Token. Die Scopes `openid` und `profile` reichen aus.
---
## Schritt 3: ID-Token gegen c-entron Ticket tauschen
```http
POST {baseUrl}/jwt/login
Authorization: Bearer {microsoft-id-token}
Content-Type: application/json
{
"Application": "{verschlüsselte-lizenz-GUID}",
"AppVersion": "2.1.2605.636",
"Device": "MACHINE-NAME"
}
```
| Feld | Typ | Bedeutung |
|---|---|---|
| `Application` | string | Verschlüsselte Lizenz-GUID der Anwendung |
| `AppVersion` | string | Version der Client-Anwendung |
| `Device` | string | Gerätename (`Environment.MachineName`) |
**Response (Erfolg):** `200 OK`
```
ticket-hash-string
```
Der Response-Body enthält direkt den Ticket-String (plain text, kein JSON).
**Response (Fehler):** `400 Bad Request` oder `401 Unauthorized`
---
## Schritt 4: Ticket für weitere API-Calls verwenden
Das erhaltene Ticket wird für alle weiteren c-entron API-Aufrufe als Authentifizierung verwendet.
---
## Was auf dem Server passiert
### JWT-Validierung (Middleware)
Die ASP.NET Core JWT Bearer Middleware:
1. Lädt das OpenID Connect Discovery Document von `{Authority}/.well-known/openid-configuration`
2. Holt die Signing Keys vom JWKS-Endpoint
3. Validiert: Signatur, Issuer, Audience, Lifetime
4. Befüllt `HttpContext.User` mit den Claims
### User-Lookup
```csharp
// oid-Claim = Microsoft Entra Object ID
var oid = identity.Claims.FirstOrDefault(c => c.Type == "oid")?.Value;
// User in der Datenbank suchen
var user = dao.GetEntity(where => where.OpenIdConnectSubjectIdentifier == oid);
```
Die Spalte `OpenIdConnectSubjectIdentifier` in der Tabelle `Sichbenu` (AppUser) enthält die Microsoft Entra Object ID des verknüpften Benutzers.
### Ticket-Erstellung
```csharp
var salt = CryptoUtils.CreateSalt(32);
var ticketId = CryptoUtils.CreatePasswordHash(deviceId, salt); // SHA-basierter Hash
var expireDate = DateTime.Now.AddMinutes(30); // 30 Min Gültigkeit
// INSERT INTO Ticket (TicketId, ExpiryDate, ApplicationID, LicenseGUID, UserI3D, DeviceId)
```
---
## Voraussetzungen
### Azure AD App Registration
| Einstellung | Wert |
|---|---|
| Application (Client) ID | → wird als `JwtAudience` in c-entron gespeichert |
| Authority URL | `https://login.microsoftonline.com/{tenant-id}/v2.0` → `JwtAuthority` |
| Redirect URI | MSAL Default für Public Client Apps |
| Token-Typ | ID-Token (nicht Access-Token) |
### c-entron Konfiguration
| Was | Wo | Setting ID |
|---|---|---|
| `JwtAuthority` | ApplicationSettings | 10351 |
| `JwtAudience` | ApplicationSettings | 10352 |
| `SystemAuthenticationMethod` | ApplicationSettings | 10360 (0=Any, 3=OpenIdConnect) |
| OpenIDConnect-Lizenz | Lizenztabelle | `AB4181F6-EF3B-4763-B29B-F5D0603311F7` |
### User-Verknüpfung
Jeder c-entron User braucht seine **Microsoft Entra Object ID** in der Spalte `OpenIdConnectSubjectIdentifier` (Tabelle `Sichbenu`). Verknüpfung über:
- **Self-Service:** `POST /jwt/connect_accounts`
- **Admin-Zuweisung:** WPF-UI unter "Persönliche Einstellungen"
---
## API-Endpoints im Überblick
| Endpoint | Methode | Auth | Zweck |
|---|---|---|---|
| `/config/jwt` | GET | Keine | JWT-Konfiguration abrufen |
| `/config/jwt` | PATCH | c-entron Ticket | JWT-Konfiguration ändern |
| `/jwt/login` | POST | Bearer (ID-Token) | **ID-Token → c-entron Ticket** |
| `/jwt/connect_accounts` | POST | Bearer (ID-Token) | Microsoft-Konto mit c-entron verknüpfen |
---
## Implementierungsbeispiel: OAuth-Token gegen c-entron Ticket tauschen
Minimales Beispiel für eine externe Applikation, die bereits ein Microsoft ID-Token hat und dieses gegen ein c-entron Ticket tauschen möchte.
### C# (.NET)
```csharp
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.Identity.Client;
public class CentronOAuthClient
{
private readonly HttpClient _httpClient;
private readonly string _centronBaseUrl;
public CentronOAuthClient(string centronBaseUrl)
{
_centronBaseUrl = centronBaseUrl.TrimEnd('/');
_httpClient = new HttpClient { BaseAddress = new Uri(_centronBaseUrl) };
}
// ──────────────────────────────────────────────────────
// Schritt 1: JWT-Konfiguration vom c-entron Server holen
// ──────────────────────────────────────────────────────
public async Task<JwtConfiguration> GetJwtConfigurationAsync()
{
var response = await _httpClient.GetAsync("/config/jwt");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<JwtConfiguration>();
}
// ──────────────────────────────────────────────────────
// Schritt 2: Microsoft ID-Token über MSAL holen
// ──────────────────────────────────────────────────────
public async Task<string> AcquireMicrosoftIdTokenAsync(JwtConfiguration config)
{
var app = PublicClientApplicationBuilder
.Create(config.Audience) // Client ID aus c-entron Config
.WithAuthority(config.Authority) // Authority aus c-entron Config
.WithDefaultRedirectUri()
.Build();
string[] scopes = ["openid", "profile"];
AuthenticationResult result;
var accounts = await app.GetAccountsAsync();
var account = accounts.FirstOrDefault();
try
{
// Silent: aus Cache oder SSO
result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// Interaktiv: Microsoft Login-Dialog zeigen
result = await app.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync();
}
return result.IdToken; // WICHTIG: IdToken, nicht AccessToken!
}
// ──────────────────────────────────────────────────────
// Schritt 3: ID-Token gegen c-entron Ticket tauschen
// ──────────────────────────────────────────────────────
public async Task<string> ExchangeTokenForTicketAsync(
string microsoftIdToken,
string applicationGuid,
string appVersion)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/jwt/login")
{
Content = JsonContent.Create(new
{
Application = applicationGuid,
AppVersion = appVersion,
Device = Environment.MachineName
})
};
request.Headers.Add("Authorization", $"Bearer {microsoftIdToken}");
var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new Exception($"Login fehlgeschlagen: {response.StatusCode} — {error}");
}
// Response ist der Ticket-String (plain text)
return await response.Content.ReadAsStringAsync();
}
// ──────────────────────────────────────────────────────
// Kompletter Flow: Alles zusammen
// ──────────────────────────────────────────────────────
public async Task<string> LoginWithMicrosoftAsync(
string applicationGuid,
string appVersion)
{
// 1. JWT-Konfiguration abrufen
var config = await GetJwtConfigurationAsync();
if (!config.Enabled)
throw new Exception("OpenID Connect ist auf diesem Server nicht aktiviert.");
// 2. Microsoft ID-Token holen
var idToken = await AcquireMicrosoftIdTokenAsync(config);
// 3. Token gegen c-entron Ticket tauschen
var ticket = await ExchangeTokenForTicketAsync(idToken, applicationGuid, appVersion);
return ticket;
}
}
// ──────────────────────────────────────────────────────
// DTOs
// ──────────────────────────────────────────────────────
public class JwtConfiguration
{
public string Authority { get; set; }
public string Audience { get; set; }
public bool Enabled { get; set; }
}
```
### Verwendung
```csharp
var client = new CentronOAuthClient("https://mein-centron-server.example.com");
// Kompletter Flow
var ticket = await client.LoginWithMicrosoftAsync(
applicationGuid: "{verschlüsselte-lizenz-guid}",
appVersion: "1.0.0.0"
);
Console.WriteLine($"c-entron Ticket: {ticket}");
// → Ticket für alle weiteren API-Calls verwenden
```
### Minimales Beispiel: Nur Token-Tausch (wenn ID-Token bereits vorhanden)
```csharp
// Wenn du bereits ein Microsoft ID-Token hast (z.B. aus einer anderen Auth-Bibliothek):
var client = new CentronOAuthClient("https://mein-centron-server.example.com");
var ticket = await client.ExchangeTokenForTicketAsync(
microsoftIdToken: "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
applicationGuid: "{verschlüsselte-lizenz-guid}",
appVersion: "1.0.0.0"
);
```
### cURL-Beispiel
```bash
# 1. JWT-Konfiguration abrufen
curl -s https://mein-centron-server.example.com/config/jwt
# 2. ID-Token gegen Ticket tauschen (ID-Token aus MSAL o.ä.)
curl -X POST https://mein-centron-server.example.com/jwt/login \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOi..." \
-H "Content-Type: application/json" \
-d '{
"Application": "{verschlüsselte-lizenz-guid}",
"AppVersion": "1.0.0.0",
"Device": "MEIN-PC"
}'
# Response: ticket-hash-string (plain text)
```
### NuGet-Pakete
```xml
<PackageReference Include="Microsoft.Identity.Client" Version="4.*" />
<!-- Optional, für Token-Cache-Persistierung: -->
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.*" />
<!-- Optional, für Windows WAM Broker (SSO): -->
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.*" />
```
---
## Relevante Quellcode-Dateien
| Schicht | Datei | Rolle |
|---|---|---|
| WPF View | `src/centron/Centron.WPF.UI/Modules/Administration/Connections/LoginDialogView.xaml` | Button "Anmelden mit Microsoft" |
| WPF ViewModel | `src/centron/Centron.WPF.UI/Modules/Administration/Connections/LoginDialogViewModel.cs` | Command-Handler |
| Client MSAL | `src/centron/Centron.WPF.UI/Services/WebServices/CentronWebServiceConnection.cs` | MSAL-Konfiguration, Token-Akquise, Token-Tausch |
| HTTP Client | `src/webservice/Centron.WebServices.Core/HttpClients/JwtAuthClient.cs` | `POST /jwt/login` mit Bearer-Header |
| Server Middleware | `src/webservice/Centron.Host/CentronHost.cs` | JWT Bearer Validierung |
| Server Controller | `src/webservice/Centron.Controllers/Controllers/Unversioned/JwtAuthController.cs` | `/jwt/login` Endpoint |
| Auth Factory | `src/backend/Centron.BL/Administration/Logins/Auth/AuthenticatorFactory.cs` | Routing zum OIDC-Authenticator |
| OIDC Authenticator | `src/backend/Centron.BL/Administration/Logins/Auth/OpenIdConnectAuthenticator.cs` | User-Lookup per `oid`-Claim |
| Ticket-Erstellung | `src/backend/Centron.BL/Administration/Logins/Auth/Authenticator.cs` + `TicketBL.cs` | Ticket generieren & speichern |
| Account Linking | `src/backend/Centron.BL/Administration/Logins/Auth/OpenIdConnectAccountConnector.cs` | Microsoft ↔ c-entron verknüpfen |
| JWT Config Model | `src/webservice/Centron.WebServices.Core/RestRequests/JwtConfiguration.cs` | Authority + Audience DTO |
| Login Request Model | `src/webservice/Centron.WebServices.Core/RestRequests/JwtLoginRequest.cs` | Application + AppVersion + Device |
| Settings IDs | `src/backend/Centron.Interfaces/Administration/Settings/ApplicationSettingID.cs` | JwtAuthority=10351, JwtAudience=10352 |
| Lizenz-GUIDs | `src/backend/Centron.Interfaces/Administration/Logins/LicenseGuids.cs` | OpenIDConnectAuthentication |
@@ -0,0 +1,25 @@
# Developer security
We have some security in place, to protect developers from accidentally doing something bad, for example sending mails to real customers.
These safeguards can't protect you from all accidents, so you still have to be careful when you're sending mails or calling external APIs.
But in most cases, you should be protected and safe.
All of these safeguards are configured in the `DeveloperSecurity.cs` file.
Check out the code in that file to get a detailed understanding of how it works and what it does.
## Sending emails
To protect us from accidentally sending emails to actual customer email addresses, there are some safeguards in place.
> :exclamation: These safeguards are only active in DEBUG-builds of the c-entron.NET :exclamation:
If you manually create a RELEASE-build of the c-entron.NET and send emails with it, it's up to you to as the developer to make sure that you don't send any emails to actual customer email addresses.
In DEBUG-builds all **external email addresses** will get replaced by `test@nexoware.com`.
The **internal email addresses** will not get modified at all.
> A email address is considered **internal** when it ends with `nexoware.com`.
> Every other email address is considered **external**.
If you want to disable this behavior (for example when you're trying to test your email sending code), you can manually edit the `AllowSendingEmailToExternalAddresses` property in the `DeveloperSecurity.cs` file.
@@ -0,0 +1,102 @@
# How does our licensing work?
## What is a license?
Our licenses are just simple GUIDs.
There is a GUID for `c-entron.NET`, another one for `Service-Board`, and again a different one for `Outlook Add-In`, etc.
But also **single features** can have their own GUID.
For example the `branch functionality`, or the `report server`, etc.
Those are all licenses a customer can potentially **have** or **NOT have**.
Additionally, each license can have a `count`, a `valid until date` and a `valid until version`, with either a `real value` or `unlimited`.
The license also has a name for display purposes only, technically the name is not relevant at all.
So, to summarize it again, for each license we have the following possible values:
* Does he have the license (`GUID`)?
* How many of them (`count`)?
* Until when is this license valid (`valid until date`)?
* Until which version is the license valid (`valid until version`)?
## How does the c-entron.NET and c-entron Web-Service work with those?
The c-entron.NET and c-entron Web-Service (also Riverbird Web-Service) generally differentiate between `Applications` and `Only Licenses`.
`Only Licenses` are the **single features** like the `branch functionality` or the `report server`.
They are all listed in the `LicenseGuids.cs` file.
Actually, every single license that we have is listed in the `LicenseGuids.cs` file, no matter if it's just a single license that we check for, or a `Application`.
`Applications` on the other hand are all licenses that are allowed to `Login` at the web-service.
They are all listed in the `ApplicationKind.cs` file.
Every entry in that file is allowed to `Login` at the web-service.
For all of those the `count`, `valid until date` and `valid until version` values are automatically checked and validated.
## Which licenses do we have?
The single source of truth for all our available licenses is the license-server.
You can use the `c-entron Office` tool to look at all the licenses, but usually that is not required.
We try to keep the `LicenseGuids.cs` file in sync with the license-server, to make it easier to check for licenses.
## I need a new license, what do I do?
At first, make sure we really have a `NEW THING` that needs to be separately licensed?
When you're sure, go to your development leader of your choice, and ask him to create this new license for you.
He will give you the `GUID` that represents this license.
Remember: Our licenses are just simple GUIDs.
You should add this new GUID to the `LicenseGuids.cs` file. And if it's required to `Login` at the web-service with it (in case for a new product), also add it to the `ApplicationKind.cs` file.
## Great, I got the GUID, how do I check for the license now?
If your license is a `Application` like we talked about above, then you might not need to do anything.
Just adding it the the `ApplicationKind.cs` is enough to allow you to login at the web-service, and have the `count`, `valid until date` and `valid until version` validated for you.
If you only have a simple boring license that you want to check, to show or hide a module in the c-entron.NET (like the `password manager` for example), or show some UI to the user, or enable extra functionality in any other way, you can use the `LicenseManager` to do that.
Let me just show you some code examples.
### Check if the customer has a license
Again, you can use this to hide or show UI, a module, or enable some features for a customer only.
```csharp
bool hasPasswordManager = LicenseManager.Instance.HasLicense(LicenseGuids.PasswordManager); // This is the important line
if (hasPasswordManager)
this.ShowPasswordManagerUI();
```
### Check the `count` of the license
This can for example be used, when we license something on a HOW MANY base.
Right now we do it for example for the `MyDay Import`.
This module can be used to import from external tools into c-entron for the `MyDay` module.
And we sell every import separately.
That means, a customer could buy 3 imports, and then would be allowed to configure 3 different imports.
On a more crazy, made up example, we could use this functionality to license how many articles the customer is allowed to create in the c-entron.
```csharp
Result<int?> myDayImportCountResult = LicenseManager.Instance.GetLicenseCount(LicenseGuids.MyDayImports); // This is the important line
if (myDayImportCountResult.Status == ResultStatus.Error)
{
// The customer does NOT have a license for LicenseGuids.MyDayImports
// Consider checking with LicenseManager.Instance.HasLicense first if the customer even has the license
}
else if (myDayImportCountResult.Status == ResultStatus.Success)
{
// The customer does have a license for LicenseGuids.MyDayImports, that's great!
// Lets now check how MANY of them he does have
// Again, this checks the COUNT of the license
int? licenseCount = myDayImportCountResult.Data;
if (licenseCount == null)
{
// The COUNT is UNLIMITED
}
else
{
// The COUNT is the number that is in licenseCount right now
// If the customer is allowed to use 3 MyDayImports, then licenseCount would be 3 here
}
}
```
@@ -0,0 +1,582 @@
# ZUGFeRD / XRechnung Feldzuordnung
Diese Dokumentation erklärt, welche Felder aus c-entron in die ZUGFeRD/XRechnung XML-Datei übernommen werden.
## Übersicht
c-entron erstellt beim Export von Rechnungen und Gutschriften automatisch eine ZUGFeRD/XRechnung-konforme XML-Datei. Diese Dokumentation zeigt Ihnen, aus welchen c-entron Feldern die einzelnen XML-Informationen stammen.
### Unterstützte Versionen
- ZUGFeRD 1.0 (Altversion)
- ZUGFeRD 2.0 / XRechnung 1.2
- ZUGFeRD 2.1 / XRechnung 2.0, 2.2, 2.3.1
- ZUGFeRD 2.1 / XRechnung 3.0.1 (aktuell)
---
## Dokumentkopf
### Grundlegende Rechnungsinformationen
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Rechnungsnummer | `ram:ID` | Rechnungsnummer | Die Belegnummer der Rechnung/Gutschrift |
| Rechnungstyp | `ram:TypeCode` | Belegart | "380" = Rechnung, "381" = Gutschrift |
| Rechnungsdatum | `ram:IssueDateTime/udt:DateTimeString` | Rechnungsdatum | Datum der Rechnungserstellung |
| Zahlungsbedingungen (Notiz) | `ram:IncludedNote` | Zahlungskonditionen Text | Freitext zu den Zahlungsbedingungen |
| Verkäufer Information (Notiz) | `ram:IncludedNote[@SubjectCode="REG"]` | Automatisch generiert | Name, Adresse, Geschäftsführer, Handelsregisternummer |
| Verwendungszweck (Notiz) | `ram:IncludedNote[@SubjectCode="ABT"]` | Bankverbindung Einstellungen | Verwendungszweck für Überweisung |
---
## Verkäufer (Eigene Firma)
Der Verkäufer repräsentiert Ihre eigene Firma (Mandant) oder Filiale.
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| **Identifikation** | | | |
| Lieferantennummer (beim Kunden) | `ram:SellerTradeParty/ram:ID` | Eigene Lieferantennummer | Ihre Lieferantennummer beim Kunden |
| Handelsregisternummer | `ram:SellerTradeParty/ram:SpecifiedLegalOrganization/ram:ID` | Mandant → Handelsregisternummer | Handelsregisternummer (HRB) |
| **Name und Kontakt** | | | |
| Firmenname | `ram:SellerTradeParty/ram:Name` | Filiale → Name oder Mandant → Name | Name der Filiale (oder Mandant, abhängig von Einstellung) |
| Geschäftsführer | `ram:SellerTradeParty/ram:DefinedTradeContact/ram:PersonName` | Mandant → Geschäftsführer | Name des Geschäftsführers |
| Abteilung | `ram:SellerTradeParty/ram:DefinedTradeContact/ram:DepartmentName` | Kontaktperson → Abteilung | Abteilungsname (falls vorhanden) |
| Telefon | `ram:SellerTradeParty/ram:DefinedTradeContact/ram:TelephoneUniversalCommunication/ram:CompleteNumber` | Filiale → Telefon oder Mandant → Telefon | Telefonnummer |
| E-Mail | `ram:SellerTradeParty/ram:DefinedTradeContact/ram:EmailURIUniversalCommunication/ram:URIID` | Filiale → E-Mail oder Mandant → E-Mail | E-Mail-Adresse |
| **Adresse** | | | |
| Postleitzahl | `ram:SellerTradeParty/ram:PostalTradeAddress/ram:PostcodeCode` | Filiale → PLZ oder Mandant → PLZ | Postleitzahl |
| Straße | `ram:SellerTradeParty/ram:PostalTradeAddress/ram:LineOne` | Filiale → Straße oder Mandant → Straße | Straße und Hausnummer |
| Stadt | `ram:SellerTradeParty/ram:PostalTradeAddress/ram:CityName` | Filiale → Stadt oder Mandant → Stadt | Ort |
| Land | `ram:SellerTradeParty/ram:PostalTradeAddress/ram:CountryID` | Land → Ländercode | Zweistelliger Ländercode (z.B. "DE") |
| **Steuer** | | | |
| Steuernummer | `ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]` | Mandant → Steuernummer | Steueridentifikationsnummer |
### Datenquelle
- Wenn eine Filiale auf der Rechnung hinterlegt ist, werden deren Daten verwendet
- Andernfalls werden die Daten des Mandanten verwendet
- Das Land wird aus der Filiale, dem Mandanten oder dem Standardland ermittelt
---
## Käufer (Kunde)
Der Käufer repräsentiert Ihren Kunden.
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| **Identifikation** | | | |
| Kundennummer | `ram:BuyerTradeParty/ram:ID` | Rechnung → Kundennummer | Kundennummer in c-entron |
| **Name und Kontakt** | | | |
| Firmenname | `ram:BuyerTradeParty/ram:Name` | Rechnungsempfänger → Firmenname | Name des Rechnungsempfängers |
| Kontaktperson | `ram:BuyerTradeParty/ram:DefinedTradeContact/ram:PersonName` | Rechnungsempfänger → Kontaktname | Name der Kontaktperson |
| Abteilung (Kontakt) | `ram:BuyerTradeParty/ram:DefinedTradeContact/ram:DepartmentName` | Rechnungsempfänger → Kontakt-Abteilung | Abteilung der Kontaktperson |
| Telefon | `ram:BuyerTradeParty/ram:DefinedTradeContact/ram:TelephoneUniversalCommunication/ram:CompleteNumber` | Rechnung → Kontakt Telefon | Telefonnummer des Kontakts |
| E-Mail | `ram:BuyerTradeParty/ram:DefinedTradeContact/ram:EmailURIUniversalCommunication/ram:URIID` | Rechnung → Kontakt E-Mail | E-Mail-Adresse des Kontakts |
| **Adresse** | | | |
| Postleitzahl | `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:PostcodeCode` | Rechnungsempfänger → PLZ | Postleitzahl |
| Straße (Zeile 1) | `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:LineOne` | Rechnungsempfänger → Strukturiert aufgebaut | Erste Adresszeile (siehe unten) |
| Adresszeile 2 | `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:LineTwo` | Rechnungsempfänger → Strukturiert aufgebaut | Zweite Adresszeile (siehe unten) |
| Adresszeile 3 | `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:LineThree` | Rechnungsempfänger → Strukturiert aufgebaut | Dritte Adresszeile (siehe unten) |
| Stadt | `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:CityName` | Rechnungsempfänger → Stadt | Ort |
| Land | `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:CountryID` | Rechnungsempfänger → Land | Zweistelliger Ländercode |
| **Steuer** | | | |
| USt-IdNr. | `ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]` | Rechnung → USt-IdNr. | Umsatzsteuer-Identifikationsnummer |
### Strukturierter Adressaufbau
c-entron baut die Empfängeradresse intelligent aus den Rechnungsempfänger-Daten auf (maximal 3 Zeilen):
1. **Firmenname** - Der Firmenname (erste Priorität)
2. **Adresszusatz** - Zusätzlicher Adresszusatz
3. **Abteilung / Kontakt-Abteilung** - Abteilungsinformationen (Reihenfolge konfigurierbar über Einstellung "Kontakt-Abteilung zuerst")
4. **Kontaktname** - Name der Kontaktperson
5. **Straße/Hausnummer oder Postfach** - Entweder Straßenadresse oder Postfach
Die erste Information wird als `Name` verwendet, die weiteren Informationen füllen `AddressLine1`, `AddressLine2` und `AddressLine3` (maximal 3 Zeilen).
### Besonderheiten
- **Postfach**: Wenn ein Postfach angegeben ist, wird dieses anstelle der Straßenadresse verwendet
- **Straßenformatierung**: Straße und Hausnummer werden automatisch kombiniert
- **Abteilungsreihenfolge**: Die Reihenfolge von "Abteilung" und "Kontakt-Abteilung" kann über die Einstellung "Empfänger Kontakt-Abteilung zuerst" (ApplicationSettingID 10370) konfiguriert werden
- **Abweichende Rechnungsadresse**: Bei abweichender Rechnungsadresse wird der Name aus den Kundenstammdaten verwendet
---
## Bestellinformationen
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Leitweg-ID / Bestellnummer | `ram:BuyerReference` | Rechnung → Externe Bestellnummer | Bestellnummer des Kunden (bei XRechnung: Leitweg-ID) |
### Hinweis
Bei XRechnung-Exporten wird die Leitweg-ID als Pflichtfeld verwendet. Bei normalen ZUGFeRD-Exporten ist die Bestellnummer optional.
---
## Lieferinformationen
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Lieferdatum | `ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime` | Rechnung → Lieferdatum | Datum der Leistungserbringung |
### Wichtig
Das Lieferdatum ist in XRechnung ein Pflichtfeld. Wenn kein Lieferdatum angegeben ist, wird automatisch das Rechnungsdatum verwendet.
---
## Zahlungsinformationen
### Bankverbindung und SEPA
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| **Eigene Bankverbindung (Lastschrift)** | | | |
| SEPA Gläubiger-ID | `ram:CreditorReferenceID` | Mandant → SEPA-Identifikationsnummer | SEPA Creditor Identifier |
| **Währung** | | | |
| Währungscode | `ram:InvoiceCurrencyCode` | Rechnung → Währung | Währungscode (z.B. "EUR") |
| **Zahlungsart** | | | |
| Zahlungsart-Code | `ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode` | Rechnung → UNTDID 4461 Code | UN/EDIFACT Zahlungsart-Code (Standard: "ZZZ") |
| Zahlungsinformation | `ram:SpecifiedTradeSettlementPaymentMeans/ram:Information` | Rechnung → Zahlungskonditionen Text | Freitext zur Zahlungsart |
| **Lastschrift (nur bei SEPA)** | | | |
| Schuldner IBAN | `ram:PayerPartyDebtorFinancialAccount/ram:IBANID` | Bankverbindung → IBAN | IBAN des Kunden (bei Lastschrift) |
| **Überweisung (nur bei Banküberweisung)** | | | |
| Empfänger IBAN | `ram:PayeePartyCreditorFinancialAccount/ram:IBANID` | Mandant → Bankverbindung (Bank 1-4) | Ihre IBAN für Überweisungen |
| Empfänger BIC | `ram:PayeeSpecifiedCreditorFinancialInstitution/ram:BICID` | Mandant → Bankverbindung (Bank 1-4) | Ihre BIC |
| Kontoinhaber | `ram:PayeePartyCreditorFinancialAccount/ram:AccountName` | Mandant → Bankverbindung (Bank 1-4) | Name des Kontoinhabers |
### Bankauswahl
Die verwendete Bankverbindung wird wie folgt bestimmt:
1. Einstellung "Mandantenbank für Rechnung verwenden" (Bank 1-4)
2. Bei Kundenrechnungen: Kundenstammdaten können die Bankauswahl überschreiben
3. Die Bankdaten werden aus den Mandantenstammdaten (Bank 1-4) geladen
---
## Steuerinformationen
### Steuerbeträge und Steuersätze
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Steuerbetrag | `ram:ApplicableTradeTax/ram:CalculatedAmount` | Berechnet aus Positionen | Steuerbetrag je Steuersatz |
| Steuerart | `ram:ApplicableTradeTax/ram:TypeCode` | Fest "VAT" | Mehrwertsteuer |
| Steuerbefreiungsgrund | `ram:ApplicableTradeTax/ram:ExemptionReason` | Abhängig vom Szenario | Textlicher Grund für Steuerbefreiung |
| Steuerbemessungsgrundlage | `ram:ApplicableTradeTax/ram:BasisAmount` | Berechnet aus Positionen | Nettobetrag für die Steuerberechnung |
| Steuerkategorie | `ram:ApplicableTradeTax/ram:CategoryCode` | Abhängig vom Szenario | Steuerkategorie-Code |
| Steuersatz | `ram:ApplicableTradeTax/ram:RateApplicablePercent` | Positionen → Steuersatz | Mehrwertsteuersatz in Prozent |
### Steuerkategorien
Die Steuerkategorie wird automatisch ermittelt:
- **S (Standard)**: Normaler Steuersatz
- **E (Befreit)**: 0% MwSt. bei steuerfreien Inlandsgeschäften
- **K (Innergemeinschaftlich)**: 0% MwSt. bei innergemeinschaftlichen Lieferungen
- **G (Export)**: 0% MwSt. bei Exporten außerhalb der EU
- **AE (Reverse Charge)**: Umkehrung der Steuerschuldnerschaft
### Steuerbefreiungsgründe
Je nach Steuersituation wird automatisch der passende Text eingefügt:
- **Reverse Charge**: "Steuerschuldnerschaft des Leistungsempfängers gem. §13B Abs 2 Nr. 10 UStG."
- **Steuerfrei Inland**: "Steuerfrei"
- **Innergemeinschaftlich**: "Kein Ausweis der Umsatzsteuer bei innergemeinschaftlichen Lieferungen"
- **Export**: "Steuer nicht erhoben aufgrund von Export außerhalb der EU"
---
## Abrechnungszeitraum
Für Vertragsabrechnungen werden die Abrechnungszeiträume automatisch ermittelt:
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Abrechnungszeitraum Von | `ram:BillingSpecifiedPeriod/ram:StartDateTime` | Vertragsabrechnung → Frühestes Startdatum | Beginn des Abrechnungszeitraums |
| Abrechnungszeitraum Bis | `ram:BillingSpecifiedPeriod/ram:EndDateTime` | Vertragsabrechnung → Spätestes Enddatum | Ende des Abrechnungszeitraums |
### Hinweis
Die Abrechnungszeiträume werden nur exportiert, wenn sie sich unterscheiden (Start ≠ Ende).
---
## Zahlungsbedingungen
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Beschreibung | `ram:SpecifiedTradePaymentTerms/ram:Description` | Rechnung → Zahlungsbedingungen + Skonto-Info | Vollständiger Text der Zahlungsbedingungen mit Skonto-Informationen |
| Fälligkeitsdatum | `ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime` | Rechnung → Fälligkeitsdatum | Datum, bis wann die Zahlung erfolgen muss |
| SEPA Mandatsreferenz | `ram:SpecifiedTradePaymentTerms/ram:DirectDebitMandateID` | Bankverbindung → Mandatsreferenz | SEPA-Mandatsreferenznummer (nur bei Lastschrift) |
### Skonto-Information
Wenn Skonto-Konditionen hinterlegt sind, werden diese automatisch im BR-DE-18 Format an die Beschreibung angehängt.
---
## Betragsübersicht
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Nettobetrag (Summe) | `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:LineTotalAmount` | Rechnung → Nettobetrag gesamt | Summe aller Netto-Positionsbeträge |
| Zuschlagsbetrag | `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:ChargeTotalAmount` | 0 (fest) | Gesamtbetrag der Zuschläge |
| Abschlagsbetrag | `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:AllowanceTotalAmount` | 0 (fest) | Gesamtbetrag der Abschläge |
| Steuerbemessungsgrundlage | `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxBasisTotalAmount` | Rechnung → Nettobetrag gesamt | Grundlage für die Steuerberechnung |
| Steuerbetrag gesamt | `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxTotalAmount` | Rechnung → Steuerbetrag gesamt | Summe aller Steuerbeträge |
| Bruttobetrag gesamt | `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount` | Netto + Steuer | Gesamtbetrag der Rechnung |
| Bereits gezahlt | `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TotalPrepaidAmount` | 0 (fest) | Bereits gezahlter Betrag |
| Zu zahlender Betrag | `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:DuePayableAmount` | Netto + Steuer | Offener Zahlungsbetrag |
### Validierung
Das System prüft automatisch:
- Die Summe der Positions-Nettobeträge muss dem Gesamt-Nettobetrag entsprechen (Toleranz: ±3,00)
- Die Summe der Positions-Bruttobeträge muss dem Gesamt-Bruttobetrag entsprechen (Toleranz: ±3,00)
- Bei Abweichungen innerhalb der Toleranz wird eine Warnung ausgegeben
- Bei Abweichungen außerhalb der Toleranz schlägt der Export fehl
---
## Gutschriften: Verweis auf ursprüngliche Rechnung
Bei Gutschriften wird automatisch ein Verweis auf die ursprüngliche Rechnung erstellt:
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Original-Rechnungsnummer | `ram:InvoiceReferencedDocument/ram:IssuerAssignedID` | Ursprüngliche Rechnung → Nummer | Nummer der ursprünglichen Rechnung |
| Original-Rechnungsdatum | `ram:InvoiceReferencedDocument/ram:FormattedIssueDateTime` | Ursprüngliche Rechnung → Datum | Datum der ursprünglichen Rechnung |
### Voraussetzung
- Gilt nur für Gutschriften
- Es muss genau eine Ursprungsrechnung vorhanden sein
- Der Verweis wird automatisch aus den Positionsursprüngen ermittelt
---
## Rechnungspositionen
Jede Position der Rechnung oder Gutschrift wird in die XML übernommen.
### Positionsnummer
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Positionsnummer | `ram:AssociatedDocumentLineDocument/ram:LineID` | Automatisch fortlaufend | Fortlaufende Nummer (1, 2, 3, ...) |
---
### Artikelinformationen
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| EAN-Code (GTIN) | `ram:SpecifiedTradeProduct/ram:GlobalID[@schemeID="0160"]` | Position → EAN-Code | EAN/GTIN Barcode des Artikels |
| Artikelnummer | `ram:SpecifiedTradeProduct/ram:SellerAssignedID` | Position → Artikelcode | Ihre interne Artikelnummer |
| Positionstext | `ram:SpecifiedTradeProduct/ram:Name` | Position → Text | Bezeichnung / Beschreibungstext |
| Seriennummer (Beschreibung) | `ram:ApplicableProductCharacteristic/ram:Description` | Automatisch: "Seriennummer 1", "Seriennummer 2", ... | Bezeichnung der Seriennummer |
| Seriennummer (Wert) | `ram:ApplicableProductCharacteristic/ram:Value` | Position → Barcodes | Barcode-Wert / Seriennummer |
### Seriennummern (Barcodes)
- Wenn einer Position Barcodes/Seriennummern zugeordnet sind, werden diese automatisch exportiert
- Pro Position können **mehrere Seriennummern** exportiert werden
- Jede Seriennummer wird mit einer fortlaufenden Nummer versehen (Seriennummer 1, 2, 3, ...)
- **Bei Titelpositionen**: Seriennummern aller untergeordneten Positionen werden zur Titelposition zusammengefasst
- Die Seriennummern werden als Produktmerkmale (`ApplicableProductCharacteristic`) im XML abgelegt
### Einstellungen
- Der Export von EAN-Codes kann in den Rechnungseinstellungen deaktiviert werden
- Der Export von Artikelnummern kann in den Rechnungseinstellungen deaktiviert werden
---
### Preise und Mengen
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Einzelpreis (Netto) | `ram:NetPriceProductTradePrice/ram:ChargeAmount` | Position → Netto-Einzelpreis | Netto-Einzelpreis des Artikels |
| Menge | `ram:BilledQuantity` | Position → Menge | Abgerechnete Menge |
| Mengeneinheit | `ram:BilledQuantity[@unitCode]` | Artikel → Mengeneinheit → UN/ECE-Code | UN/ECE-Code der Mengeneinheit (z.B. "C62" = Stück) |
| Positionssumme (Netto) | `ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount` | Position → Netto-Positionssumme | Netto-Gesamtbetrag der Position |
### Besonderheiten bei negativen Preisen
ZUGFeRD unterstützt keine negativen Preise. Das System:
1. Macht den Einzelpreis positiv
2. Negiert stattdessen die Menge
3. Das Ergebnis bleibt rechnerisch gleich
### Mengeneinheiten (UN/ECE-Codes)
- Wird automatisch aus den Artikelstammdaten übernommen
- Standardwert: "C62" (Stück/Einheit)
- Nur bei Artikelpositionen; bei anderen Positionsarten wird "C62" verwendet
---
### Steuern auf Positionsebene
| ZUGFeRD Feld | XML-Pfad | c-entron Quelle | Beschreibung |
|--------------|----------|-----------------|--------------|
| Steuerart | `ram:ApplicableTradeTax/ram:TypeCode` | Fest "VAT" | Mehrwertsteuer |
| Steuerkategorie | `ram:ApplicableTradeTax/ram:CategoryCode` | Abhängig vom Szenario | Steuerkategorie-Code (siehe oben) |
| Steuersatz | `ram:ApplicableTradeTax/ram:RateApplicablePercent` | Position → Steuersatz | Mehrwertsteuersatz in Prozent |
---
### Titelpositionen (Gruppierungen)
Titelpositionen ermöglichen die Gruppierung von Artikelpositionen:
| Verhalten | Beschreibung |
|-----------|--------------|
| **Eingeklappte Titelpositionen** | Werden als einzelne Position mit der Summe der untergeordneten Positionen exportiert |
| **Menge** | Wird immer auf 1 gesetzt |
| **Einzelpreis** | Entspricht der Positionssumme |
| **Steuersatz** | Wird aus den untergeordneten Positionen ermittelt |
### Einschränkungen bei Titelpositionen
- **Ausgeklappte Titelpositionen** werden nicht unterstützt (Export schlägt fehl)
- **Gemischte Steuersätze** in einer Titelposition werden nicht unterstützt (Export schlägt fehl)
- **Standard-Steuersatz**: Wenn eine Titelposition keine untergeordneten Positionen hat, wird 19% verwendet
- **Seriennummern**: Alle Seriennummern der untergeordneten Positionen werden zur Titelposition aggregiert
### Welche Positionen werden exportiert?
- Artikel
- Kundenrabatte
- Eingeklappte Titelpositionen
- Sortierung nach interner Position
---
## Export-Einstellungen
Die folgenden Einstellungen beeinflussen den ZUGFeRD-Export:
### Rechnungseinstellungen
- **Aktive ZUGFeRD-Version**: Legt fest, welche ZUGFeRD/XRechnung-Version exportiert wird
- **Mandantenname bevorzugen**: Verwendet den Mandantennamen statt des Filialnamens
- **EAN-Code exportieren**: Aktiviert/deaktiviert den Export von EAN-Codes
- **Artikelnummer exportieren**: Aktiviert/deaktiviert den Export von Artikelnummern
### Kundeneinstellungen
- **ZUGFeRD exportieren**: Kann pro Kunde aktiviert/deaktiviert werden
- **Mandantenbank**: Kann pro Kunde überschrieben werden
### SEPA-Einstellungen
- **SEPA aktiv**: Bestimmt, ob Lastschrift (SEPA) oder Überweisung verwendet wird
- **Mandatsreferenz**: SEPA-Mandatsreferenznummer der Bankverbindung
---
## Datenquellen im Überblick
### Stammdaten
- **Mandant**: Firmendaten, Bankverbindungen, Steuernummern
- **Filiale**: Filial-spezifische Adress- und Kontaktdaten
- **Kunde**: Kundenstammdaten mit Adressen und Kontakten
- **Artikel**: Artikelstammdaten mit Mengeneinheiten
### Belege
- **Rechnung/Gutschrift**: Alle rechnungsspezifischen Daten
- **Rechnungspositionen**: Positionsdaten mit Preisen, Mengen und Steuern
- **Bankverbindung**: IBAN, BIC, Mandatsreferenz
### Verträge
- **Vertragsabrechnung**: Abrechnungszeiträume für Vertragspositionen
### Einstellungen
- **Rechnungseinstellungen**: ZUGFeRD-Version, Export-Optionen
- **Zahlungsbedingungen**: Skonto-Konditionen
---
## Häufig gestellte Fragen
### Welche Daten muss ich in c-entron pflegen, damit der ZUGFeRD-Export funktioniert?
**Pflichtfelder für XRechnung:**
1. **Eigene Firma**: Vollständige Adressdaten, Steuernummer, Handelsregisternummer
2. **Kunde**: Vollständige Adressdaten, USt-IdNr. (bei EU-Geschäften)
3. **Rechnung**: Rechnungsdatum, Lieferdatum (wird notfalls automatisch gesetzt)
4. **Bankverbindung**: IBAN und BIC (für Überweisungen)
5. **Leitweg-ID**: Bei XRechnung-Pflicht im Feld "Externe Bestellnummer"
**Empfohlene Felder:**
- Zahlungsbedingungen mit Skonto
- Kontaktdaten (Telefon, E-Mail)
- EAN-Codes und Artikelnummern
- SEPA-Mandatsreferenz (bei Lastschrift)
---
### Wie werden die Empfänger-Adresszeilen aufgebaut?
c-entron verwendet die strukturierten Rechnungsempfänger-Daten und baut daraus intelligent die Adresszeilen auf:
**Verfügbare Felder:**
- **Firmenname** - Wird als Name des Empfängers verwendet
- **Adresszusatz** - Zusätzliche Adressinformation
- **Abteilung** - Abteilungsbezeichnung
- **Kontakt-Abteilung** - Abteilung der Kontaktperson
- **Kontaktname** - Name der Kontaktperson
- **Straße und Hausnummer** - Straßenadresse
- **Postfach** - Postfach (wird anstelle der Straße verwendet, wenn aktiviert)
**Automatischer Aufbau:**
1. Das erste verfügbare Feld wird als **Name** verwendet
2. Weitere Felder füllen **Adresszeile 1, 2 und 3** (maximal 3 zusätzliche Zeilen)
3. Die Reihenfolge von "Abteilung" und "Kontakt-Abteilung" ist konfigurierbar
4. Wenn ein Postfach angegeben ist, ersetzt dieses die Straßenadresse
**Beispiel:**
- **Name**: "Musterfirma GmbH"
- **Adresszeile 1**: "IT-Abteilung"
- **Adresszeile 2**: "z.Hd. Max Mustermann"
- **Straße**: "Musterstraße 123"
- **PLZ/Ort**: "12345 Musterstadt"
---
### Wie wird die Bankverbindung ausgewählt?
1. In den Rechnungseinstellungen wird festgelegt, welche Mandantenbank verwendet wird (Bank 1-4)
2. Diese Einstellung kann pro Kunde in den Kundenstammdaten überschrieben werden
3. Die Bankdaten werden aus den Mandantenstammdaten geladen
---
### Was passiert bei negativen Preisen?
ZUGFeRD unterstützt keine negativen Einzelpreise. Daher:
- Der Einzelpreis wird positiv gemacht
- Die Menge wird negativ gemacht
- Das Ergebnis (Positionssumme) bleibt identisch
---
### Wie funktionieren Titelpositionen im Export?
**Eingeklappte Titelpositionen:**
- Werden als eine Position exportiert
- Enthalten die Summe aller untergeordneten Positionen
- Menge ist immer 1
- Einzelpreis = Positionssumme
**Wichtige Einschränkungen:**
- Ausgeklappte Titelpositionen werden nicht unterstützt
- Alle untergeordneten Positionen müssen den gleichen Steuersatz haben
- Bei gemischten Steuersätzen schlägt der Export fehl
---
### Was bedeuten die Validierungs-Warnungen?
Das System prüft, ob die Summe der Positionen mit den Kopfbeträgen übereinstimmt:
- **Toleranz**: ±3,00 Euro
- **Warnung**: Abweichung innerhalb der Toleranz → Export erfolgt, Warnung wird protokolliert
- **Fehler**: Abweichung außerhalb der Toleranz → Export schlägt fehl
Ursachen können sein:
- Rundungsdifferenzen bei vielen Positionen
- Manuelle Korrekturen an Beträgen
- Fehlerhafte Steuerberechnungen
**Lösung**: Rechnung prüfen und ggf. Positionen anpassen
---
### Wie wird der Abrechnungszeitraum ermittelt?
Bei Vertragsabrechnungen:
- **Von**: Frühestes Startdatum aller Vertragspositionen
- **Bis**: Spätestes Enddatum aller Vertragspositionen
Bei normalen Rechnungen:
- Kein Abrechnungszeitraum (nur bei unterschiedlichen Daten relevant)
---
### Welche Mengeneinheiten werden unterstützt?
c-entron verwendet UN/ECE-Codes für Mengeneinheiten:
- **C62**: Stück / Einheit (Standard)
- **HUR**: Stunde
- **MTR**: Meter
- **MTK**: Quadratmeter
- **MTQ**: Kubikmeter
- **KGM**: Kilogramm
- **LTR**: Liter
- Und viele weitere...
Die Mengeneinheit wird aus den Artikelstammdaten übernommen. Falls nicht vorhanden, wird "C62" (Stück) verwendet.
---
### Wie werden Seriennummern / Barcodes exportiert?
c-entron unterstützt den Export von Seriennummern (Barcodes) in ZUGFeRD/XRechnung:
**Automatischer Export:**
- Wenn Sie einer Rechnungsposition Barcodes/Seriennummern zugeordnet haben, werden diese automatisch in die XML-Datei exportiert
- Jede Seriennummer wird als separates Produktmerkmal (`ApplicableProductCharacteristic`) gespeichert
**Mehrere Seriennummern pro Position:**
- Sie können beliebig viele Seriennummern pro Position exportieren
- Jede Seriennummer erhält automatisch eine fortlaufende Bezeichnung:
- "Seriennummer 1"
- "Seriennummer 2"
- "Seriennummer 3"
- usw.
**Titelpositionen:**
- Bei eingeklappten Titelpositionen werden alle Seriennummern der untergeordneten Positionen automatisch zur Titelposition zusammengefasst
- Die Reihenfolge der Seriennummern bleibt erhalten
**XML-Struktur:**
```xml
<ram:ApplicableProductCharacteristic>
<ram:Description>Seriennummer 1</ram:Description>
<ram:Value>SN-12345-ABC</ram:Value>
</ram:ApplicableProductCharacteristic>
<ram:ApplicableProductCharacteristic>
<ram:Description>Seriennummer 2</ram:Description>
<ram:Value>SN-67890-XYZ</ram:Value>
</ram:ApplicableProductCharacteristic>
```
**Anwendungsfall:**
Dies ist besonders nützlich für Produkte mit individuellen Seriennummern, z.B.:
- Elektronikgeräte (Laptops, Smartphones)
- Maschinen und Anlagen
- Fahrzeuge
- Medizinische Geräte
- Alle Produkte mit eindeutiger Identifikation
---
### Was ist der Unterschied zwischen ZUGFeRD und XRechnung?
**ZUGFeRD (Comfort):**
- Mehr optionale Felder
- Flexibler bei fehlenden Daten
- Für B2B-Geschäft geeignet
**XRechnung:**
- Pflicht für öffentliche Auftraggeber in Deutschland
- Strengere Validierung
- Leitweg-ID ist Pflichtfeld
- Lieferdatum ist Pflichtfeld
c-entron erstellt automatisch das richtige Format:
- **XRechnung**: Wenn eine Leitweg-ID angegeben ist
- **ZUGFeRD Comfort**: Wenn keine Leitweg-ID angegeben ist
---
## Version
| Version | Datum | Hinweise |
|---------|-------|----------|
| 1.2 | 2025 | Hinzugefügt: Strukturierte Rechnungsempfänger-Daten mit intelligentem Adressaufbau, verbesserte Ländercode-Unterstützung für Handelstyp-Ermittlung |
| 1.1 | 2025 | Hinzugefügt: Barcode/Seriennummern-Export in Rechnungspositionen |
| 1.0 | 2025 | Initiale Anwenderdokumentation für XRechnung 3.0.1 |
---
## Support
Bei Fragen zur ZUGFeRD/XRechnung-Funktionalität in c-entron wenden Sie sich bitte an:
- **c-entron Support**: erp-support@nexoware.com
- **Dokumentation**: Siehe auch die Online-Hilfe in c-entron
@@ -0,0 +1,421 @@
# ZUGFeRD XML Field Mapping
This document describes the complete mapping between ZUGFeRD XML nodes and c-entron database fields/entities.
## Overview
The ZUGFeRD XML generation is implemented in `InvoiceZugferdBL.cs` and supports multiple ZUGFeRD versions:
- ZUGFeRD 1.0 (legacy)
- ZUGFeRD 2.0 / XRechnung 1.2
- ZUGFeRD 2.1 / XRechnung 2.0, 2.2, 2.3.1
- ZUGFeRD 2.1 / XRechnung 3.0.1 (current)
The main data flow is:
1. Load receipt data from `BookKeepingExportBL.LoadReceipt()` → returns `IBookKeepingReceipt`
2. Convert to `ZugferdExportItem` in `GetZugferdExportItem()`
3. Generate XML document with `DoGenerateZugferdXRechnungXmlDocument()`
---
## Document Context & Header
### ExchangedDocumentContext
XML structure related to document context and format identification.
| XML Node | c-entron Source | Description |
|----------|----------------|-------------|
| `rsm:GuidelineSpecifiedDocumentContextParameter/ram:ID` | Derived from `ZugferdKind` enum | Format identifier (e.g., "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0") |
| `ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID` | Static value | For v3.0.1: "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0" |
### ExchangedDocument
Basic invoice/credit voucher information.
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:ID` | `Number` | `IBookKeepingReceipt` | Invoice/receipt number |
| `ram:TypeCode` | Derived from `Type` | `IBookKeepingReceipt` | "380" = Invoice, "381" = Credit voucher |
| `ram:IssueDateTime/udt:DateTimeString` | `Date` | `IBookKeepingReceipt` | Receipt date (format: yyyyMMdd) |
| `ram:IncludedNote` | `PaymentConditionsText` | `IBookKeepingReceipt` | Payment text/notes |
| `ram:IncludedNote[@SubjectCode="REG"]` | Composed string | Multiple sources | Seller information text (Name, address, CEO, HRB) |
| `ram:IncludedNote[@SubjectCode="ABT"]` | `PayeeAssignmentNotice` | Bank info via settings | Assignment notice for bank transfer |
---
## Seller Trade Party (Own Company)
The seller represents the own company (Mandator) or branch.
| XML Node | c-entron Field | Table/Entity | Path in Code |
|----------|----------------|--------------|--------------|
| `ram:SellerTradeParty/ram:ID` | `OwnSupplierNumber` | `IBookKeepingReceipt` | Customer's supplier number for own company |
| `ram:SellerTradeParty/ram:Name` | `Name` or `Name` | `Branch` or `Mandator` | Branch name (or Mandator if setting/no branch) |
| `ram:SellerTradeParty/ram:SpecifiedLegalOrganization/ram:ID` | `HRB` | `Mandator` | Commercial register number |
| `ram:SellerTradeParty/ram:DefinedTradeContact/ram:PersonName` | `CEO` | `Mandator` | Managing director name |
| `ram:SellerTradeParty/ram:DefinedTradeContact/ram:DepartmentName` | `Department.Department` | `ContactPerson` via `Branch` or `Mandator` | Department name |
| `ram:SellerTradeParty/ram:DefinedTradeContact/ram:TelephoneUniversalCommunication/ram:CompleteNumber` | `PhoneNumber` or `Phone` | `Branch` or `Mandator` | Phone number |
| `ram:SellerTradeParty/ram:DefinedTradeContact/ram:EmailURIUniversalCommunication/ram:URIID` | `EMail` | `Branch` or `Mandator` | Email address |
| `ram:SellerTradeParty/ram:PostalTradeAddress/ram:PostcodeCode` | `ZipCode` or `PostCode` | `Branch` or `Mandator` | ZIP code |
| `ram:SellerTradeParty/ram:PostalTradeAddress/ram:LineOne` | `Street` | `Branch` or `Mandator` | Street address |
| `ram:SellerTradeParty/ram:PostalTradeAddress/ram:CityName` | `City` | `Branch` or `Mandator` | City |
| `ram:SellerTradeParty/ram:PostalTradeAddress/ram:CountryID` | `CountryCode` | `Country` | Country code (ISO 2-letter) |
| `ram:SellerTradeParty/ram:URIUniversalCommunication/ram:URIID[@schemeID="EM"]` | `EMail` | `Branch` or `Mandator` | Email (duplicate for compatibility) |
| `ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]` | `TaxIDNumber` | `Mandator` | Tax identification number |
**Internal Fields (not exported to XML):**
- `OwnCountryCode`: Country code of seller (used for trade type determination: inland/EU/export)
**Data Sources:**
- If `BranchI3D` is set: Load from `Branch` table
- Otherwise: Load from `Mandator` table (default mandator)
- Country: From `Branch.CountryI3D` or `Mandator.Country` or default country (default: "DE")
---
## Buyer Trade Party (Customer)
The buyer represents the customer/recipient.
| XML Node | c-entron Field | Table/Entity | Path in Code |
|----------|----------------|--------------|--------------|
| `ram:BuyerTradeParty/ram:ID` | `AddressNumber` | `IBookKeepingReceipt` | Customer number |
| `ram:BuyerTradeParty/ram:Name` | `CompanyName` or fallback to `AddressName` | `ReceiptReceiver` or `IBookKeepingReceipt` | Recipient name |
| `ram:BuyerTradeParty/ram:DefinedTradeContact/ram:PersonName` | `ContactName` | `ReceiptReceiver` | Contact person name |
| `ram:BuyerTradeParty/ram:DefinedTradeContact/ram:DepartmentName` | `ContactDepartment` | `ReceiptReceiver` | Contact department |
| `ram:BuyerTradeParty/ram:DefinedTradeContact/ram:TelephoneUniversalCommunication/ram:CompleteNumber` | `ContactPhone` | `IBookKeepingReceipt` | Phone number |
| `ram:BuyerTradeParty/ram:DefinedTradeContact/ram:EmailURIUniversalCommunication/ram:URIID` | `ContactEMail` | `IBookKeepingReceipt` | Email address |
| `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:PostcodeCode` | `Zip` | `ReceiptReceiver` | ZIP code |
| `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:LineOne` | Structured from `ReceiptReceiver` | `ReceiptReceiver` | First address line (see below) |
| `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:LineTwo` | Structured from `ReceiptReceiver` | `ReceiptReceiver` | Second address line (see below) |
| `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:LineThree` | Structured from `ReceiptReceiver` | `ReceiptReceiver` | Third address line (see below) |
| `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:CityName` | `City` | `ReceiptReceiver` | City |
| `ram:BuyerTradeParty/ram:PostalTradeAddress/ram:CountryID` | `CountryCode` | `Country` via `ReceiptReceiver.CountryI3D` | Country code (ISO 2-letter) |
| `ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID[@schemeID="EM"]` | `ContactEMail` | `IBookKeepingReceipt` | Email (duplicate for compatibility) |
| `ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]` | `SalesTaxIdentificationNumber` | `IBookKeepingReceipt` | VAT identification number |
**Internal Fields (not exported to XML):**
- `OwnCountryCode`: Country code of buyer (used for trade type determination: inland/EU/export)
- `IsEUTrade`: Flag indicating if buyer country is EU member
**Structured Address Line Building:**
The system intelligently builds address lines from `ReceiptReceiver` with a maximum of 3 lines:
1. **CompanyName** - Company name (first priority if available)
2. **AdditionalAddressSupplement** - Additional address supplement
3. **Department/ContactDepartment** - Department information (order configurable via `ReceiverContactDepartmentFirst` setting)
4. **ContactName** - Contact person name
5. **Street/HouseNumber or PostOfficeBox** - Either street address or P.O. Box (formatted as shown in PostOfficeBox)
The first item becomes the `Name`, subsequent items fill `AddressLine1`, `AddressLine2`, and `AddressLine3` (maximum 3 lines).
**Special Handling:**
- Post office box: If `HasPostOfficeBox` is true, uses `PostOfficeBox` instead of street address
- Street formatting: Combines `Street` and `HouseNumber` with proper trimming
- Department order: Configurable via `ReceiverContactDepartmentFirst` setting (ApplicationSettingID 10370)
- Invoice address: If alternative invoice address is used (`UsedAlternativeInvoiceAddress`), name is loaded from `Kunden` table
- Fallback: If `ReceiptReceiver` is null, falls back to legacy `IBookKeepingReceipt` fields
---
## Header Trade Agreement
Purchase order and party references.
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:ApplicableHeaderTradeAgreement/ram:BuyerReference` | `ExternalPurchaseOrderNumber` or `leitwegID` parameter | `IBookKeepingReceipt` | Leitweg-ID for XRechnung, otherwise purchase order number |
| `ram:ApplicableHeaderTradeAgreement/ram:BuyerOrderReferencedDocument/ram:IssuerAssignedID` | `ExternalPurchaseOrderNumber` | `IBookKeepingReceipt` | Purchase order number (only if not XInvoice) |
---
## Header Trade Delivery
Delivery information.
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:ApplicableHeaderTradeDelivery/ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString` | `DeliveryDate` or `Date` | `IBookKeepingReceipt` | Delivery date (fallback to receipt date if not set) |
---
## Header Trade Settlement
Payment, banking, and monetary information.
### Banking & SEPA
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:ApplicableHeaderTradeSettlement/ram:CreditorReferenceID` | `SepaIdentificationNumber` | `Mandator` | SEPA creditor reference ID (only for direct debit) |
| `ram:ApplicableHeaderTradeSettlement/ram:InvoiceCurrencyCode` | `CurrencyISOCode` | `IBookKeepingReceipt` | Currency code (e.g., "EUR") |
| `ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode` | `Untdid4461` or "ZZZ" | `IBookKeepingReceipt` | Payment means type code (UN/EDIFACT 4461) |
| `ram:SpecifiedTradeSettlementPaymentMeans/ram:Information` | `PaymentConditionsText` | `IBookKeepingReceipt` | Payment information text |
| `ram:SpecifiedTradeSettlementPaymentMeans/ram:PayerPartyDebtorFinancialAccount/ram:IBANID` | `Iban` | `BankAccount` via `BankAccountI3D` | Debtor IBAN (for direct debit) |
| `ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeePartyCreditorFinancialAccount/ram:IBANID` | Selected bank IBAN | `Mandator` bank info (Bank1-4) | Creditor IBAN (for bank transfer) |
| `ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeePartyCreditorFinancialAccount/ram:AccountName` | Selected bank holder or `Name` | `Mandator` bank info or `Mandator` | Account holder name |
| `ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeeSpecifiedCreditorFinancialInstitution/ram:BICID` | Selected bank BIC | `Mandator` bank info (Bank1-4) | Bank BIC code |
**Bank Selection Logic:**
1. Check `ReceiptInvoiceSettings.UseMandatorBankForInvoice` (1-4)
2. For customer receipts, check `AccountCustomer.MandatorBank` override
3. Load bank details from `Mandator` (Bank1Iban/Bic, Bank2Iban/Bic, Bank3Iban/Bic, Bank4Iban/Bic)
### Tax Information
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:ApplicableTradeTax/ram:CalculatedAmount` | Calculated from positions | `BookKeepingReceiptItem` | Tax amount per tax rate |
| `ram:ApplicableTradeTax/ram:TypeCode` | Static "VAT" | - | Tax type |
| `ram:ApplicableTradeTax/ram:ExemptionReason` | Derived from tax scenario | Logic | Tax exemption reason text |
| `ram:ApplicableTradeTax/ram:BasisAmount` | Calculated from positions | `BookKeepingReceiptItem` | Net basis amount for tax |
| `ram:ApplicableTradeTax/ram:CategoryCode` | Derived from tax scenario | Logic | Tax category (S, E, K, G, AE) |
| `ram:ApplicableTradeTax/ram:RateApplicablePercent` | Grouped tax rates | `BookKeepingReceiptItem.TaxRate` | VAT percentage |
**Tax Category Codes:**
- `S` (Standard): Normal VAT rate
- `E` (Exempt): 0% VAT for domestic tax-free transactions
- `K` (Intra-community): 0% VAT for EU intra-community supply
- `G` (Export): 0% VAT for export outside EU
- `AE` (Reverse charge): Reverse charge scenario (`IsReverseCharge = true`)
**Tax Exemption Reasons:**
- Reverse charge: "Steuerschuldnerschaft des Leistungsempfängers gem. §13B Abs 2 Nr. 10 UStG."
- Tax-free domestic: "Steuerfrei"
- Intra-community: "Kein Ausweis der Umsatzsteuer bei innergemeinschaftlichen Lieferungen"
- Export: "Steuer nicht erhoben aufgrund von Export außerhalb der EU"
### Billing Period
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:BillingSpecifiedPeriod/ram:StartDateTime/udt:DateTimeString` | Min of position billing periods | `ContractBillingInfo` | Earliest billing start date |
| `ram:BillingSpecifiedPeriod/ram:EndDateTime/udt:DateTimeString` | Max of position billing periods | `ContractBillingInfo` | Latest billing end date |
### Payment Terms
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:SpecifiedTradePaymentTerms/ram:Description` | `PaymentConditionFull` + Skonto info | `IBookKeepingReceipt` + derived | Full payment condition text with line breaks |
| `ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString` | `DueDate` or `Date` | `IBookKeepingReceipt` | Payment due date |
| `ram:SpecifiedTradePaymentTerms/ram:DirectDebitMandateID` | `AuthorizationNumber` | `BankAccount` via `BankAccountI3D` | SEPA mandate ID (only for direct debit) |
**Skonto Information (BR-DE-18):**
- Generated via `AssetConditionBL.GetPaymentConditionSkontoInBR_DE_18Format()`
- Appended to description with proper XML line breaks (`&#xD;`)
### Monetary Summation
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:LineTotalAmount` | `NetPriceFCComplete` | `IBookKeepingReceipt` | Total net amount |
| `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:ChargeTotalAmount` | Static 0 | - | Total charges |
| `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:AllowanceTotalAmount` | Static 0 | - | Total allowances |
| `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxBasisTotalAmount` | `NetPriceFCComplete` | `IBookKeepingReceipt` | Tax basis total |
| `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxTotalAmount[@currencyID]` | `TaxPriceFCComplete` | `IBookKeepingReceipt` | Total tax amount |
| `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount` | Net + Tax | Calculated | Gross total |
| `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TotalPrepaidAmount` | Static 0 | - | Prepaid amount |
| `ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:DuePayableAmount` | Net + Tax | Calculated | Due payable amount |
**Validation:**
- System validates that sum of position net prices equals header net price (tolerance: ±3.00)
- System validates that sum of position gross prices equals header gross price (tolerance: ±3.00)
- Warnings are logged if differences are within tolerance, errors if exceeding
### Invoice Referenced Document (Credit Vouchers Only)
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:InvoiceReferencedDocument/ram:IssuerAssignedID` | `Number` | `ReceiptInvoice` (origin) | Original invoice number |
| `ram:InvoiceReferencedDocument/ram:FormattedIssueDateTime/qdt:DateTimeString` | `Date` | `ReceiptInvoice` (origin) | Original invoice date |
**Logic:**
- Only for credit vouchers (`Type = CreditVoucher`)
- Only if exactly one origin invoice exists
- Loaded via `ReceiptItem.OriginReceiptI3D` where `OriginKind = Invoice`
---
## Line Items (Positions)
Each invoice/credit voucher position.
### Line Item Document
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:AssociatedDocumentLineDocument/ram:LineID` | Sequential counter | Generated | Position number (1, 2, 3, ...) |
### Trade Product
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:SpecifiedTradeProduct/ram:GlobalID[@schemeID="0160"]` | `EANCode` | `BookKeepingReceiptItem` | EAN/GTIN barcode (if not disabled in settings) |
| `ram:SpecifiedTradeProduct/ram:SellerAssignedID` | `Articlecode` | `BookKeepingReceiptItem` | Article code (if not disabled in settings) |
| `ram:SpecifiedTradeProduct/ram:Name` | `Text` | `BookKeepingReceiptItem` (via compact entity) | Position text/description |
| `ram:SpecifiedTradeProduct/ram:ApplicableProductCharacteristic/ram:Description` | Static "Seriennummer {index}" | Generated | Serial number label (1, 2, 3, ...) |
| `ram:SpecifiedTradeProduct/ram:ApplicableProductCharacteristic/ram:Value` | `Barcodes[i]` | `IReceiptItemWithBarcodes` | Barcode value (serial number) |
**Barcode Handling:**
- Barcodes are loaded from receipt items that implement `IReceiptItemWithBarcodes`
- Multiple barcodes per position are supported (indexed sequentially)
- For title positions, barcodes from child items are aggregated into the parent position
- Each barcode is exported as a separate `ApplicableProductCharacteristic` node
- Code reference: `InvoiceZugferdBL.cs:625-626` (loading), `InvoiceZugferdBL.cs:648-651` (title aggregation), `InvoiceZugferdBL.cs:881-898` (XML export)
**Settings Flags:**
- `ReceiptInvoiceSettings.ZugferdExportDontExportEanCode`: Suppresses EAN export
- `ReceiptInvoiceSettings.ZugferdExportDontExportArticleCode`: Suppresses article code export
### Line Trade Agreement
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:NetPriceProductTradePrice/ram:ChargeAmount` | `NetPrice` | `InvoiceItemCompact` or `CreditVoucherItemCompact` | Unit net price (always positive) |
**Note:** If `NetPrice` is negative, the price is made positive and quantity is negated.
### Line Trade Delivery
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:BilledQuantity[@unitCode]` | `QuantityComplete` | `InvoiceItemCompact` or `CreditVoucherItemCompact` | Quantity (negated if price was negative) |
| `@unitCode` | `UNECECode` | `Article.ArticleUnit` | UN/ECE unit code (default: "C62" = piece) |
**UN/ECE Code Logic:**
- Only for article items (`Kind = Article` and `ArticleI3D` is set)
- Loaded from `Article.ArticleUnit.UNECECode`
- Fallback: "C62" (one/piece)
### Line Trade Settlement
| XML Node | c-entron Field | Table/Entity | Description |
|----------|----------------|--------------|-------------|
| `ram:ApplicableTradeTax/ram:TypeCode` | Static "VAT" | - | Tax type |
| `ram:ApplicableTradeTax/ram:CategoryCode` | Derived from tax scenario | Logic | Tax category code (see header tax) |
| `ram:ApplicableTradeTax/ram:RateApplicablePercent` | `TaxRate` or 0 | `InvoiceItemCompact`/`CreditVoucherItemCompact` | VAT percentage (0 if `ExclusiveOfVAT`) |
| `ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount` | `NetPriceTotalFCComplete` | `BookKeepingReceiptItem` | Line net total amount |
**Title Position Handling:**
- Title positions (`Kind = TitlePosition`, `Expanded = false`) are exported as collapsed items
- Child items (`Visible != Visible` or `Indent > 0`) are aggregated into parent title position
- Title position quantity is always 1
- Title position net price equals net total
- Mixed tax rates in title positions cause error
**Item Filtering:**
- Only exports: `Article`, `CustomerDiscount`, or collapsed `TitlePosition` items
- Must be `Visible = Visible` or `Indent = 0`
- Ordered by `InternalPosition`
---
## Data Sources Summary
### Primary Tables
| Table/Entity | Purpose | Key Fields |
|--------------|---------|------------|
| `IBookKeepingReceipt` | Receipt header | Number, Date, AddressNumber, DueDate, CurrencyISOCode, NetPriceFCComplete, TaxPriceFCComplete |
| `ReceiptReceiver` | Structured receiver address | CompanyName, AdditionalAddressSupplement, Department, ContactDepartment, ContactName, Street, HouseNumber, PostOfficeBox, Zip, City, CountryI3D |
| `BookKeepingReceiptItem` | Receipt line items | Position, Text, Articlecode, EANCode, NetPrice, QuantityComplete, TaxRate |
| `InvoiceItemCompact` | Invoice item details | NetPrice, NetPriceTotalFCComplete, TaxPriceTotalFCComplete, VATRate, QuantityComplete |
| `CreditVoucherItemCompact` | Credit voucher item details | NetPrice, NetPriceTotalFC, TaxPriceTotalFC, VATRate, QuantityComplete |
| `Mandator` | Own company | Name, CEO, TaxIDNumber, HRB, Street, City, PostCode, Phone, EMail, SepaIdentificationNumber |
| `Branch` | Own branch | Name, Street, City, ZipCode, PhoneNumber, EMail, CountryI3D |
| `Country` | Country data | CountryCode, EUMember |
| `BankAccount` | Bank account | Iban, AuthorizationNumber |
| `Article` | Article master data | ArticleUnit (for UN/ECE code) |
| `ReceiptInvoice` | Invoice entity | For credit voucher references |
| `ReceiptCreditVoucher` | Credit voucher entity | For origin tracking |
### Derived/Calculated Fields
| Concept | Calculation | Source |
|---------|-------------|--------|
| Tax category codes | Based on tax rate, reverse charge, country flags | Logic in `GetTaxCategoryCode()` |
| Tax exemption reasons | Based on tax scenario | Logic in `GetTaxExemptionReason()` |
| Structured address lines | Intelligent building from ReceiptReceiver | Logic in receiver address building (max 3 lines) |
| OwnCountryCode | Country code for trade type | Loaded from `Country` via `CountryI3D` (default: "DE") |
| Billing period | Min/Max from contract billing info | `ContractBL.GetContractBillingPeriodForInvoiceItem()` |
| Bank selection | Settings + customer override | `ReceiptInvoiceSettings.UseMandatorBankForInvoice` + `AccountCustomer.MandatorBank` |
| Skonto text | Payment condition formatting | `AssetConditionBL.GetPaymentConditionSkontoInBR_DE_18Format()` |
| UN/ECE code | Article unit lookup | `Article.ArticleUnit.UNECECode` |
---
## Special Cases & Business Logic
### Structured Receiver Address
The system uses the structured `ReceiptReceiver` entity to build multi-line addresses:
- **Maximum 3 lines** for Name and AddressLine1-3
- **Intelligent building** based on available fields (CompanyName, AdditionalAddressSupplement, Department, ContactDepartment, ContactName, Street)
- **Configurable department order** via `ReceiverContactDepartmentFirst` setting
- **Post office box handling** with `HasPostOfficeBox` flag
- **Fallback support** to legacy `IBookKeepingReceipt` fields if `ReceiptReceiver` is null
### Negative Prices
ZUGFeRD does not support negative prices. The system:
1. Makes `NetPrice` positive
2. Negates `Quantity` instead
3. Maintains correct calculation
### Title Positions
- Collapsed title positions are exported with aggregated child item values
- Expanded title positions are not supported (error)
- Mixed tax rates in title positions are not supported (error)
- Default tax rate for title positions without items: 19%
- Barcodes from child items are aggregated into the parent title position
### Payment Condition
Determined by SEPA active status:
- `IsSepaActive = true` → `DirectDebit` (BG-19)
- `IsSepaActive = false` → `BankTransfer` (BG-17)
### Country Trade Flags
Used for tax category determination:
- `IsInlandTrade`: Buyer country = Seller country
- `IsEUTrade`: Buyer country is EU member
### Validation Tolerances
- Net price difference tolerance: ±3.00 (between header and sum of positions)
- Gross price difference tolerance: ±3.00
- Within tolerance: Warning logged, value corrected
- Exceeding tolerance: Error, export fails
---
## Implementation Notes
### File Encoding
- XML files are generated with UTF-8 encoding
- BOM (Byte Order Mark) is removed from export (first 3 bytes stripped)
### Date Formats
- Standard date: `yyyyMMdd` (format code "102")
- Long date: `yyyy-MM-ddThh:mm:ss`
### Amount Formatting
- All amounts use "F2" format (2 decimal places)
- Culture: en-US (decimal separator: dot)
### Namespace Prefixes
- `rsm`: CrossIndustryInvoice
- `ram`: ReusableAggregateBusinessInformationEntity
- `udt`: UnqualifiedDataType
- `qdt`: QualifiedDataType
### Code References
- Main generator: `InvoiceZugferdBL.cs:117-158` (GenerateZugferdFile)
- Export item builder: `InvoiceZugferdBL.cs:237-434` (GetZugferdExportItem)
- XML document generator: `InvoiceZugferdBL.cs:727-750` (DoGenerateZugferdXRechnungXmlDocument)
- Tax logic: `InvoiceZugferdBL.cs:1369-1400` (GetTaxCategoryCode, GetTaxExemptionReason)
---
## Version History
| Version | Notes |
|---------|-------|
| 1.2 | Added: Structured `ReceiptReceiver` support for buyer address, `OwnCountryCode` for trade type determination |
| 1.1 | Added: Barcode/serial number export in line items via `ApplicableProductCharacteristic` |
| 1.0 | Initial documentation covering XRechnung 3.0.1 implementation |