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
154 lines
6.3 KiB
C#
154 lines
6.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Centron.BusinessLogic;
|
|
using Centron.BusinessLogic.Sales.CustomerAssets.TimerBilling;
|
|
using Centron.BusinessLogic.WebServices.Employee;
|
|
using Centron.Interfaces.BL;
|
|
using Centron.Tests.EndToEnd.Infrastructure;
|
|
using CentronSoftware.Centron.WebServices.Entities.Sales.CustomerAssets.TimerBilling;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace Centron.Tests.EndToEnd.Tests.Administration.Employees;
|
|
|
|
/// <summary>
|
|
/// Regression tests for ticket 168634.
|
|
/// Deactivating an employee used to delete the employee-article assignment (Mitarbeiterartikel).
|
|
/// That assignment is the only link between a recorded time and its employee in the billing modules,
|
|
/// so the deactivation silently made all already recorded times of that employee unbillable - and a
|
|
/// reactivation could not restore it.
|
|
/// These tests cover both halves of the fix: the assignment now survives a deactivation (only the
|
|
/// articles are marked as end-of-life), and the billing search falls back to the employee who
|
|
/// recorded the time when the assignment is missing.
|
|
/// </summary>
|
|
public class EmployeeDeactivationBillingTests : EndToEndTest
|
|
{
|
|
public EmployeeDeactivationBillingTests(ITestOutputHelper testOutputHelper)
|
|
: base(testOutputHelper)
|
|
{
|
|
}
|
|
|
|
public override void Execute()
|
|
{
|
|
var appUserI3D = this.GetAppUserWithBillableTimers();
|
|
var employeeI3D = this.Sql<int>($"SELECT Personal FROM dbo.Sichbenu WHERE I3D = {appUserI3D}");
|
|
|
|
this.DeactivationKeepsAssignmentAndMarksArticlesEndOfLife(appUserI3D);
|
|
this.ReactivationRevokesEndOfLife(appUserI3D);
|
|
this.TimersStayAttributedWhenAssignmentIsMissing(appUserI3D, employeeI3D);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds an app-user that has employee articles which are used by open (not yet billed) timers
|
|
/// recorded by that same employee.
|
|
/// </summary>
|
|
private int GetAppUserWithBillableTimers()
|
|
{
|
|
var appUserI3D = this.Sql<int>(@"
|
|
SELECT TOP 1 SB.I3D
|
|
FROM dbo.hlpdsk_timer HT
|
|
INNER JOIN dbo.Mitarbeiterartikel MA ON MA.ArtikelI3D = HT.ArtikelI3D
|
|
INNER JOIN dbo.Sichbenu SB ON SB.I3D = MA.MitarbeiterI3D
|
|
WHERE ISNULL(HT.RechPosI3D, 0) = 0
|
|
AND ISNULL(HT.LiefPosI3D, 0) = 0
|
|
AND ISNULL(HT.AufPosI3D, 0) = 0
|
|
AND HT.PersonalI3D = SB.Personal
|
|
GROUP BY SB.I3D
|
|
ORDER BY COUNT(*) DESC, SB.I3D");
|
|
|
|
if (appUserI3D <= 0)
|
|
throw new InvalidOperationException("No app-user with employee articles and open timers found in the test database.");
|
|
|
|
return appUserI3D;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deactivating an employee must keep the employee-article assignment and only mark the
|
|
/// assigned articles as end-of-life, so the articles disappear from new selections while
|
|
/// the recorded times stay attributable.
|
|
/// </summary>
|
|
private void DeactivationKeepsAssignmentAndMarksArticlesEndOfLife(int appUserI3D)
|
|
{
|
|
// Reset the end-of-life flag first, so the snapshot is independent of the backup's state.
|
|
this.Sql($@"
|
|
UPDATE A SET A.EOL = 0
|
|
FROM dbo.ARTIK A
|
|
INNER JOIN dbo.Mitarbeiterartikel MA ON MA.ArtikelI3D = A.I3D
|
|
WHERE MA.MitarbeiterI3D = {appUserI3D}");
|
|
|
|
this.SetEmployeeArticlesEndOfLife(appUserI3D, true);
|
|
|
|
this.VerifyEmployeeArticles("Deactivation_AssignmentKeptAndArticlesEndOfLife", appUserI3D);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reactivating an employee must revoke the end-of-life mark that the deactivation has set.
|
|
/// </summary>
|
|
private void ReactivationRevokesEndOfLife(int appUserI3D)
|
|
{
|
|
this.SetEmployeeArticlesEndOfLife(appUserI3D, false);
|
|
|
|
this.VerifyEmployeeArticles("Reactivation_ArticlesUsableAgain", appUserI3D);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Databases that were damaged by the old deactivation behaviour have no employee-article
|
|
/// assignment left. The billing search must still attribute those times to the employee who
|
|
/// recorded them, otherwise the times can never be billed again.
|
|
/// Times that merely use one of the employee's articles but were recorded by somebody else
|
|
/// move to that other employee, which is the correct attribution once the assignment is gone.
|
|
/// </summary>
|
|
private void TimersStayAttributedWhenAssignmentIsMissing(int appUserI3D, int employeeI3D)
|
|
{
|
|
var timersBefore = this.SearchTimersForEmployee(employeeI3D);
|
|
|
|
if (timersBefore.Any() == false)
|
|
throw new InvalidOperationException($"No open timers found for employee {employeeI3D} in the test database.");
|
|
|
|
// Simulate a database that was damaged by the old deactivation behaviour.
|
|
this.Sql($"DELETE FROM dbo.Mitarbeiterartikel WHERE MitarbeiterI3D = {appUserI3D}");
|
|
|
|
var timersAfter = this.SearchTimersForEmployee(employeeI3D);
|
|
|
|
this.Verifier.Verify("TimerBillingSearch_WithoutEmployeeArticleAssignment", new
|
|
{
|
|
AssignmentsLeft = this.Sql<int>($"SELECT COUNT(*) FROM dbo.Mitarbeiterartikel WHERE MitarbeiterI3D = {appUserI3D}"),
|
|
TimersFoundBefore = timersBefore.Count,
|
|
TimersRecordedByEmployeeBefore = timersBefore.Count(timer => timer.CreatorEmployeeI3D == employeeI3D),
|
|
TimersFoundAfter = timersAfter.Count,
|
|
TimersRecordedByEmployeeAfter = timersAfter.Count(timer => timer.CreatorEmployeeI3D == employeeI3D)
|
|
});
|
|
}
|
|
|
|
private void SetEmployeeArticlesEndOfLife(int appUserI3D, bool isEndOfLife)
|
|
{
|
|
using (var session = new BLSession())
|
|
{
|
|
session.GetBL<EmployeeArticleWebServiceBL>()
|
|
.SetEmployeeArticlesEndOfLife(this.GetLoggedInUser(), appUserI3D, isEndOfLife)
|
|
.ThrowIfError();
|
|
}
|
|
}
|
|
|
|
private void VerifyEmployeeArticles(string name, int appUserI3D)
|
|
{
|
|
this.Verifier.VerifySql(name, $@"
|
|
SELECT ArticleCode = A.Artikelcode,
|
|
IsEndOfLife = CAST(ISNULL(A.EOL, 0) AS bit)
|
|
FROM dbo.Mitarbeiterartikel MA
|
|
INNER JOIN dbo.ARTIK A ON A.I3D = MA.ArtikelI3D
|
|
WHERE MA.MitarbeiterI3D = {appUserI3D}
|
|
ORDER BY A.Artikelcode");
|
|
}
|
|
|
|
private List<TimerForTimerBillingDTO> SearchTimersForEmployee(int employeeI3D)
|
|
{
|
|
using (var session = new BLSession())
|
|
{
|
|
return session.GetBL<TimerBillingBL>()
|
|
.SearchTimers(new TimerBillingFilter { EmployeeI3Ds = new List<int> { employeeI3D } })
|
|
.ToList();
|
|
}
|
|
}
|
|
}
|