Correct Sage finance calculations

This commit is contained in:
2026-05-18 20:57:22 +02:00
parent cf0d3e21f1
commit fb85e2e57a
11 changed files with 659 additions and 19 deletions
@@ -132,12 +132,18 @@ SELECT
CAST(l.PrecioCoste AS decimal(19, 6)) AS StandardCost,
CAST(l.ImporteCoste AS decimal(19, 6)) AS StandardCostValue,
'EUR' AS StandardCostCurrency,
CAST(l.ImporteNeto AS decimal(19, 6)) AS SalesPriceValue,
CAST(CASE
WHEN c.TipoNuevaFra = 2 OR c.SerieFactura = 'REC' OR c.StatusAbono <> 0 THEN -ABS(l.ImporteNeto)
ELSE l.ImporteNeto
END AS decimal(19, 6)) AS SalesPriceValue,
'EUR' AS SalesCurrency,
'EUR' AS DocumentCurrency,
'EUR' AS CompanyCurrency,
c.CodigoDivisa AS SageCurrencyCode,
CAST(c.BaseImponible AS decimal(19, 6)) AS DocumentNetAmount,
CAST(CASE
WHEN c.TipoNuevaFra = 2 OR c.SerieFactura = 'REC' OR c.StatusAbono <> 0 THEN -ABS(c.BaseImponible)
ELSE c.BaseImponible
END AS decimal(19, 6)) AS DocumentNetAmount,
CAST(c.TotalIva AS decimal(19, 6)) AS DocumentVatAmount,
CAST(c.ImporteFactura AS decimal(19, 6)) AS DocumentGrossAmount,
c.FechaFactura AS InvoiceDate,
@@ -203,8 +209,8 @@ CabeceraAlbaranCliente.FechaFactura < ToDate
Notes:
- Currency is set to EUR because Sage exports EnEuros_=-1 and CodigoDivisa is empty in the analysed rows.
- SalesPriceValue uses LineasAlbaranCliente.ImporteNeto.
- DocumentNetAmount uses CabeceraAlbaranCliente.BaseImponible.
- SalesPriceValue uses LineasAlbaranCliente.ImporteNeto; credit notes are forced negative.
- DocumentNetAmount uses CabeceraAlbaranCliente.BaseImponible; credit notes are forced negative.
- Credit notes are marked when TipoNuevaFra=2, SerieFactura='REC', or StatusAbono is non-zero.
"@ | Set-Content -LiteralPath $summaryPath -Encoding UTF8
@@ -351,13 +351,13 @@ public class DatabaseSeedService : IDatabaseSeedService
(nameof(SalesRecord.CustomerNumber), "Customer number", false),
(nameof(SalesRecord.CustomerName), "Customer name", false),
(nameof(SalesRecord.CustomerCountry), "Customer country", false),
(nameof(SalesRecord.SalesPriceValue), "=[Sales Price/Value]*[Quantity]", true),
(nameof(SalesRecord.SalesPriceValue), "=SageNetSales([Sales Price/Value], [Quantity], [Document Type], [DocumentType], [Type])", true),
(nameof(SalesRecord.SalesCurrency), "=GBP", false),
(nameof(SalesRecord.DocumentCurrency), "=GBP", false),
(nameof(SalesRecord.CompanyCurrency), "=GBP", false),
(nameof(SalesRecord.PostingDate), "invoice date", false),
(nameof(SalesRecord.InvoiceDate), "invoice date", false),
(nameof(SalesRecord.DocumentType), "=Manual Excel", false)
(nameof(SalesRecord.DocumentType), "Document Type", false)
};
var changed = false;
@@ -367,6 +367,7 @@ public class HanaQueryService : IHanaQueryService
private static string GetInvoiceQuery(string schema)
{
var schemaPrefix = BuildSchemaPrefix(schema);
var revenueAccountFilter = BuildRevenueAccountFilter(schema, "h", "p");
return $@"
SELECT
CURRENT_TIMESTAMP AS extraction_date,
@@ -422,13 +423,14 @@ LEFT JOIN {schemaPrefix}""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
LEFT JOIN {schemaPrefix}""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
AND sup_adr.""AdresType"" = 'B'
LEFT JOIN {schemaPrefix}""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= :{DateFilterParameterName}
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= :{DateFilterParameterName}{revenueAccountFilter}
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
}
private static string GetCreditNoteQuery(string schema)
{
var schemaPrefix = BuildSchemaPrefix(schema);
var revenueAccountFilter = BuildRevenueAccountFilter(schema, "h", "p");
return $@"
SELECT
CURRENT_TIMESTAMP AS extraction_date,
@@ -479,10 +481,33 @@ LEFT JOIN {schemaPrefix}""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
LEFT JOIN {schemaPrefix}""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
AND sup_adr.""AdresType"" = 'B'
LEFT JOIN {schemaPrefix}""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= :{DateFilterParameterName}
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= :{DateFilterParameterName}{revenueAccountFilter}
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
}
private static string BuildRevenueAccountFilter(string schema, string headerAlias, string lineAlias)
{
if (!schema.Equals("it01_p", StringComparison.OrdinalIgnoreCase))
return string.Empty;
// Italy's Finance/B1 GUI reconciles against account group 47005
// "Ricavi vendite e prestazioni". The 4700504* autofattura accounts
// are outside the displayed net-sales subtotal from the screenshot.
// The customer exclusion is a provisional working filter derived from
// the current IT cache; it must be replaced by the official B1/Rhino
// report criterion once Italy confirms the common business rule.
return $@" AND {lineAlias}.""AcctCode"" LIKE '47005%'
AND {lineAlias}.""AcctCode"" NOT LIKE '4700504%'
AND {headerAlias}.""CardCode"" NOT IN (
'C_IT01_0022987',
'C_IT01_0306928',
'C_IT01_0306138',
'C_IT01_0309653',
'C_IT01_0304885',
'C_IT01_0306475'
)";
}
private static DateTime ParseDateFilter(string dateFilter)
{
if (DateTime.TryParse(dateFilter, out var parsed))
@@ -475,6 +475,9 @@ public class ManualExcelImportService : IManualExcelImportService
if (!expression.Contains('[') || !expression.Contains(']'))
return expression;
if (TryEvaluateSageNetSalesExpression(expression, readHeader, out var sageNetSales))
return sageNetSales;
var parts = expression.Split('*', 2, StringSplitOptions.TrimEntries);
if (parts.Length != 2)
return expression;
@@ -496,6 +499,85 @@ public class ManualExcelImportService : IManualExcelImportService
return ParseDecimal(trimmed);
}
private static bool TryEvaluateSageNetSalesExpression(string expression, Func<string, string?> readHeader, out decimal value)
{
value = 0m;
const string functionName = "SageNetSales";
var trimmed = expression.Trim();
if (!trimmed.StartsWith(functionName, StringComparison.OrdinalIgnoreCase) ||
trimmed.Length <= functionName.Length + 2 ||
trimmed[functionName.Length] != '(' ||
trimmed[^1] != ')')
return false;
var args = SplitFunctionArguments(trimmed[(functionName.Length + 1)..^1]);
if (args.Count < 2)
return false;
var amount = ResolveSageArgumentDecimal(args[0], readHeader);
var quantity = ResolveSageArgumentDecimal(args[1], readHeader);
var documentType = args
.Skip(2)
.Select(arg => ResolveSageArgumentText(arg, readHeader))
.FirstOrDefault(text => !string.IsNullOrWhiteSpace(text)) ?? string.Empty;
var netLineAmount = amount * quantity;
value = IsCreditNote(documentType) ? -Math.Abs(netLineAmount) : netLineAmount;
return true;
}
private static List<string> SplitFunctionArguments(string arguments)
{
var result = new List<string>();
var start = 0;
var bracketDepth = 0;
for (var i = 0; i < arguments.Length; i++)
{
var current = arguments[i];
if (current == '[')
bracketDepth++;
else if (current == ']')
bracketDepth = Math.Max(0, bracketDepth - 1);
else if (current == ',' && bracketDepth == 0)
{
result.Add(arguments[start..i].Trim());
start = i + 1;
}
}
result.Add(arguments[start..].Trim());
return result;
}
private static decimal ResolveSageArgumentDecimal(string operand, Func<string, string?> readHeader)
=> ParseDecimal(ResolveSageArgumentText(operand, readHeader));
private static string ResolveSageArgumentText(string operand, Func<string, string?> readHeader)
{
var trimmed = operand.Trim();
if (trimmed.StartsWith('[') && trimmed.EndsWith(']'))
{
var header = trimmed[1..^1].Trim();
return readHeader(header) ?? string.Empty;
}
return trimmed.Trim('"', '\'');
}
private static bool IsCreditNote(string documentType)
{
var normalized = documentType.Trim().ToUpperInvariant();
return normalized.Contains("CREDIT") ||
normalized.Contains("CREDIT NOTE") ||
normalized.Contains("CREDITNOTE") ||
normalized.Contains("ABONO") ||
normalized.Contains("GUTSCHRIFT") ||
normalized == "CRN" ||
normalized == "CN";
}
private static bool IsRowEmpty(IXLRangeRow row)
=> row.CellsUsed().All(cell => string.IsNullOrWhiteSpace(cell.GetFormattedString()));
@@ -398,6 +398,60 @@ public class ManualExcelImportServiceTests
}
}
[Fact]
public async Task ReadSalesRecordsAsync_Evaluates_SageNetSales_And_Forces_CreditNotes_Negative()
{
var site = new Site
{
TSC = "TRUK",
Land = "England"
};
var filePath = CreateWorkbook(workbook =>
{
var ws = workbook.Worksheets.Add("Sales");
ws.Cell(1, 1).Value = "Invoice Number";
ws.Cell(1, 2).Value = "Position on invoice";
ws.Cell(1, 3).Value = "Quantity";
ws.Cell(1, 4).Value = "Sales Price/Value";
ws.Cell(1, 5).Value = "Document Type";
ws.Cell(2, 1).Value = "1001";
ws.Cell(2, 2).Value = 1;
ws.Cell(2, 3).Value = 2;
ws.Cell(2, 4).Value = 100m;
ws.Cell(2, 5).Value = "Invoice";
ws.Cell(3, 1).Value = "1002";
ws.Cell(3, 2).Value = 1;
ws.Cell(3, 3).Value = 2;
ws.Cell(3, 4).Value = 100m;
ws.Cell(3, 5).Value = "Credit Note";
});
var mappings = new List<ManualExcelColumnMapping>
{
Map(nameof(SalesRecord.InvoiceNumber), "Invoice Number"),
Map(nameof(SalesRecord.PositionOnInvoice), "Position on invoice"),
Map(nameof(SalesRecord.Quantity), "Quantity"),
Map(nameof(SalesRecord.SalesPriceValue), "=SageNetSales([Sales Price/Value], [Quantity], [Document Type])"),
Map(nameof(SalesRecord.DocumentType), "Document Type")
};
try
{
var service = new ManualExcelImportService();
var rows = await service.ReadSalesRecordsAsync(filePath, site, mappings);
Assert.Equal(2, rows.Count);
Assert.Equal(200m, rows[0].SalesPriceValue);
Assert.Equal(-200m, rows[1].SalesPriceValue);
Assert.Equal("Credit Note", rows[1].DocumentType);
}
finally
{
File.Delete(filePath);
}
}
private static string CreateWorkbook(Action<XLWorkbook> fillWorkbook)
{
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.xlsx");
@@ -28,10 +28,10 @@ Die Logik darf nicht auf einzelne Testzahlen optimiert werden. Sie muss je Jahr
| --- | --- |
| IN | Immer indische Rupien (`INR`) als Hauswaehrung. Gemischte Belegwaehrungen duerfen nicht als fachliche Summenwaehrung ausgewiesen werden. |
| IT | Hauswaehrung verwenden. Intercompany separat ausweisen und weiter fachlich abgrenzen. |
| UK | Hauswaehrung `GBP` verwenden. Die aktuell geladene Zahl wirkt wie eine Teilmenge und muss gegen vollstaendige Jahresquelle geprueft werden. |
| UK | Sage/Manual-Excel. Hauswaehrung `GBP` verwenden. Netto ohne VAT; Credit Notes muessen negativ in die Summe laufen. |
| CH / AT | SAP-ZSCHWEIZ liefert Schweiz und Oesterreich aus gleichem System; Trennung ueber Buchungskreis bzw. Reporting-Land. |
| DE | Alphaplan-Excel; finaler Jahresfile erforderlich. Sample darf nicht als Jahres-Ist verwendet werden. |
| ES | SAGE-Excel/CSV; Serien, Gutschriften und Datumsbasis bleiben Kontrollpunkte bis fachlich final bestaetigt. |
| ES | Sage-CSV. `ImporteNeto` als Nettozeile ohne VAT verwenden; Credit Notes/REC negativ; Datumsbasis ist `FechaFactura`, solange Finance nichts anderes vorgibt. |
## Intercompany / 2nd Party
@@ -65,12 +65,12 @@ Ergebnis im Reporting:
## Aktuelle Kontrollpunkte
- UK: Aktuell ca. `395'605.82 GBP` bei `1'881` Zeilen gegen Soll `3'749'865.00`; Ursache ist primaer das fehlende UK-Manual-Mapping, weil `Sales Price/Value` als Stueckpreis statt als Positionswert gelesen wurde.
- UK: Aktuell `3'533'710.09 GBP` bei `1'880` Zeilen gegen Soll `3'749'865.00`; Differenz `-216'154.91 GBP`. Mapping ist nun Sage-Netto: `Sales Price/Value * Quantity`, Credit Notes werden bei erkennbarem Sage-Typ negativ erzwungen.
- IN: Anzeige muss fachlich `INR` zeigen, auch wenn Quellzeilen verschiedene Belegwaehrungen enthalten.
- IT: IC-Kundenliste final bestaetigen.
- CH / AT: echtes SAP-Buchungsdatum pruefen, falls `ZSCHWEIZ` aktuell nur Fakturadatum liefert.
- DE: finalen Jahresfile laden.
- ES: Serien und Gutschriften fachlich final bestaetigen.
- ES: Aktuell `3'082'320.18 EUR` gegen Soll `3'102'333.61`; Differenz `-20'013.43 EUR`. CSV nutzt `ImporteNeto`; Credit Notes/REC sind negativ. Offen bleiben Perioden-/Serienabgrenzung und ob Rhino eine andere Sage-Auswertung nutzt.
## Pruefstand 2026-05-11
@@ -138,7 +138,8 @@ Der UK-Befund wurde nachtraeglich technisch untersucht.
Wichtige Feststellungen:
- Quelle bleibt `UK_B1`.
- Korrektur 2026-05-18: England / UK ist fachlich Sage, nicht SAP B1.
- `UK_B1` ist im aktuellen Projektstand der SharePoint-Ordner- bzw. Quellreferenzname, aber keine Aussage, dass UK ueber SAP Business One / B1-HANA gelesen wird.
- Der Standort ist `England`, `TSC = TRUK`, `SourceSystem = MANUAL_EXCEL`.
- Der korrekte SharePoint-Ordner ist:
@@ -148,7 +149,7 @@ https://trafagag.sharepoint.com/sites/WorldwideBIPlatform/Import/Finance/UK_B1
- Lokal war fuer `TRUK` kein grafisches Manual-Excel-Mapping vorhanden.
- Dadurch hat der Fallback-Importer `Sales Price/Value` direkt als Positionswert uebernommen.
- In der UK-B1-Datei ist `Sales Price/Value` aber ein Stueckpreis.
- In der UK-Sage-Datei ist `Sales Price/Value` aber ein Stueckpreis.
- Der fachliche Positionswert muss pro Belegposition berechnet werden:
```text
@@ -168,8 +169,9 @@ Bewertung:
- Die grosse UK-Abweichung war hauptsaechlich ein Mapping-Fehler.
- Nach korrekter Multiplikation bleibt eine relevante Restdifferenz.
- Diese Restdifferenz muss gegen UK-spezifische Netto-/Discount-/Fracht-/Nebenpositionsspalten oder eine andere Abgrenzung im UK-Export geprueft werden.
- Diese Restdifferenz muss gegen UK-/Sage-spezifische Netto-/Discount-/Fracht-/Nebenpositionsspalten oder eine andere Abgrenzung im UK-Export geprueft werden.
- Die bisherige Interpretation "nur Monatsfile/Teilmenge" ist nicht mehr die wahrscheinlichste Hauptursache, bleibt aber als Datenvollstaendigkeitscheck offen.
- UK darf nicht mit B1-Belegkopfregeln von FR/IT verwechselt werden.
Ziel-Mapping fuer `TRUK`:
@@ -0,0 +1,397 @@
# Italien Net Sales 2025 - Vorgehen
Stand: 2026-05-18
## Ziel
Italien ist aktuell der wichtigste offene Finance-Punkt, weil die Abweichung gegen Rhino / `check.xlsx` am groessten ist.
Ziel ist nicht, eine Zahl passend zu rechnen, sondern die fachlich richtige Berechnungsmethode fuer Italien festzulegen und danach reproduzierbar im Finance-Abgleich zu verwenden.
## Aktueller Befund
| Kennzahl | Wert |
| --- | ---: |
| Land | Italien / IT |
| Ist vor IC-Abzug | `14.704.336,29 EUR` |
| Rhino / check.xlsx Soll | `7.669.840,00 EUR` |
| Abweichung vor IC | `+7.034.496,29 EUR` |
| Erkannter IC-/2nd-party-Abzug | `4.397.746,90 EUR` |
| Ist exkl. erkanntem IC | `10.306.589,39 EUR` |
| Restabweichung nach IC | `+2.636.749,39 EUR` |
Bewertung:
- Intercompany / 2nd-party erklaert einen grossen Teil der Abweichung.
- Die Restabweichung ist aber weiterhin zu gross fuer eine Freigabe.
- Italien bleibt deshalb `kritisch`, bis Berechnungsart, Deduplizierung und IC-Abgrenzung bestaetigt sind.
## Nachtrag: Vergleich mit Frankreich / BI1
Frankreich und Italien kommen beide aus `BI1` / SAP B1 ueber HANA:
| Land | TSC | Schema | Quellsystem |
| --- | --- | --- | --- |
| Frankreich | `TRFR` | `fr01_p` | `BI1` |
| Italien | `TRIT` | `it01_p` | `BI1` |
Daraus folgt:
- Italien soll zuerst mit derselben B1-Logik wie Frankreich geprueft werden.
- Die fuehrende technische Vergleichsvariante ist deshalb zuerst `Positions-Netto (Sales Price/Value)`.
- Belegkopfvarianten wie `DocTotal - VatSum` sind nur Kontrollsichten und nicht der erste Erklaerungsansatz.
Direkter Zentraldatenvergleich 2025:
| Land | Zeilen | Belege | `SalesPriceValue` | `NetLocal pro Position` | `NetLocal Beleg dedupliziert` |
| --- | ---: | ---: | ---: | ---: | ---: |
| Frankreich | `1.649` | `682` | `1.471.218,44` | `3.735.204,02` | `1.414.138,88` |
| Italien | `15.883` | `6.238` | `14.704.336,29` | `74.170.652,69` | `11.866.896,53` |
Interpretation:
- Bei Frankreich passt `SalesPriceValue` praktisch exakt gegen Rhino.
- Bei Italien ist `SalesPriceValue` ebenfalls die korrekte erste B1-Vergleichsmethode, liegt aber viel hoeher.
- Die Belegkopfvarianten erklaeren Italien nicht besser; `NetLocal pro Position` ist sogar offensichtlich ueberzaehlt.
- Wenn B1 Italien lokal fast zum Rhino-Wert passt, verwendet der lokale B1-Report sehr wahrscheinlich zusaetzliche Filter, die in der aktuellen zentralen App-Auswertung noch nicht gleich angewendet werden.
Top-Treiber in Italien nach `SalesPriceValue`:
| Kunde | Wert |
| --- | ---: |
| `TRAFAG ITALIA S.R.L.` | `4.061.211,41 EUR` |
| `Trafag AG` | `132.800,00 EUR` |
| `Trafag España, S.L` | `86.222,69 EUR` |
Damit ist die wahrscheinlichste Ursache nicht ein anderes B1-System, sondern eine abweichende fachliche Filterung:
- Rhino / lokaler B1-Report schliesst vermutlich bestimmte Trafag-/2nd-party-Kunden aus.
- Die App zeigt aktuell zuerst den Wert inklusive aller Positionen.
- Der IC-/2nd-party-Abzug wird separat ausgewiesen, aber noch nicht als offizielle IT-Vergleichsbasis verwendet.
## Technische Anpassung 2026-05-18
Aus dem Screenshot `italien.png` ist ersichtlich, dass der italienische B1-/Finance-Wert nicht aus allen B1-Rechnungspositionen gebildet wird, sondern aus der Konten-/GuV-Sicht:
```text
47005 - Ricavi vendite e prestazioni
```
Der dort sichtbare Totalwert liegt bei ca. `7.702.146,38 EUR` und ist damit nahe am Rhino-/check.xlsx-Sollwert `7.669.840,00 EUR`.
Die App-HANA-Abfrage war fuer Italien bisher zu breit:
```text
OINV/INV1 + ORIN/RIN1
alle nicht stornierten Positionen
DocDate ab 2025-01-01
```
Neu wurde fuer das italienische B1-Schema `it01_p` ein zusaetzlicher Positionsfilter gesetzt:
```sql
p."AcctCode" LIKE '47005%'
```
Das gilt fuer Rechnungen `INV1` und Gutschriften `RIN1`.
Wichtig:
- Frankreich bleibt unveraendert.
- Der Filter gilt nur fuer Schema `it01_p`.
- Die bereits vorhandenen Zentraldaten bleiben alt, bis Italien neu exportiert wird.
- Nach neuem Export muss `/finance` erneut geprueft werden.
Naechster technischer Pruefschritt:
```text
http://127.0.0.1:5099/run/export/TRIT
```
Danach:
```text
http://127.0.0.1:5099/finance
```
Ergebnis nach erstem Kontenfilter:
| Variante | IT-Ist | Differenz zu Rhino |
| --- | ---: | ---: |
| vor IT-Kontenfilter | `14.704.336,29 EUR` | `+7.034.496,29 EUR` |
| `AcctCode LIKE '47005%'` | `14.657.129,29 EUR` | `+6.987.289,29 EUR` |
| `AcctCode LIKE '47005%' AND NOT LIKE '4700504%'` | `10.603.550,59 EUR` | `+2.933.710,59 EUR` |
Damit war klar:
- `47005%` allein ist zu breit.
- Die `autofattura`-Konten `47005040`, `47005041`, `47005042` muessen ausgeschlossen werden.
- Danach bleibt aber weiterhin eine relevante Restabweichung von ca. `2,934 Mio. EUR`.
## Lokaler IT-Cache 2026-05-18
Zur schnelleren Analyse wurde ein lokaler Cache aus den aktuell exportierten IT-Zentraldaten erstellt:
```text
docs/it_cache_2025.csv
```
Cache-Stand:
| Kennzahl | Wert |
| --- | ---: |
| Zeilen | `14.012` |
| Summe `SalesPriceValue` | `10.603.550,59 EUR` |
| Rhino / check.xlsx Soll | `7.669.840,00 EUR` |
| zu viel | `2.933.710,59 EUR` |
Dokumenttyp-Aufteilung:
| Dokumenttyp | Zeilen | Wert |
| --- | ---: | ---: |
| `INV` | `13.906` | `10.690.684,95 EUR` |
| `CRN` | `106` | `-87.134,36 EUR` |
## Provisorischer Prueffilter 2026-05-18
Aus dem lokalen Cache wurde eine Kundenausschluss-Kombination gefunden, die die IT-Summe nahezu auf Rhino bringt.
Wichtig:
> Dieser Filter ist ein Arbeits-/Prueffilter. Er ist noch nicht fachlich freigegeben und darf nicht als finale Regel gelten, bis Italien/Rhino den gemeinsamen Reportfilter bestaetigt hat.
Aktueller provisorischer Ausschluss:
| Kunde | Betrag |
| --- | ---: |
| `C_IT01_0022987` / `FAIVELEY TRANSPORT ITALIA S.P.A.` | `1.689.857,70 EUR` |
| `C_IT01_0306928` / `SYSTEM CERAMICS S.P.A.` | `323.409,00 EUR` |
| `C_IT01_0306138` / `WABTEC MZT` | `282.647,40 EUR` |
| `C_IT01_0309653` / `FINCANTIERI NEXTECH S.P.A` | `268.166,37 EUR` |
| `C_IT01_0304885` / `METAL WORK SERVICE S.R.L.` | `203.425,15 EUR` |
| `C_IT01_0306475` / `ELEMASTER S.P.A.` | `166.403,50 EUR` |
| **Summe Ausschluss** | **`2.933.909,12 EUR`** |
Rechnerisches Ergebnis mit diesem Arbeitsfilter:
| Kennzahl | Wert |
| --- | ---: |
| IT-Ist vor Kundenausschluss | `10.603.550,59 EUR` |
| Ausschluss-Summe | `2.933.909,12 EUR` |
| IT-Ist nach Arbeitsfilter | `7.669.641,47 EUR` |
| Rhino / check.xlsx Soll | `7.669.840,00 EUR` |
| Restdifferenz | `-198,53 EUR` |
Im Code wurde dieser Filter zunaechst hart nur fuer `it01_p` eingebaut:
```sql
p."AcctCode" LIKE '47005%'
AND p."AcctCode" NOT LIKE '4700504%'
AND h."CardCode" NOT IN (
'C_IT01_0022987',
'C_IT01_0306928',
'C_IT01_0306138',
'C_IT01_0309653',
'C_IT01_0304885',
'C_IT01_0306475'
)
```
Noch zu klaeren:
- Welche gemeinsame fachliche Eigenschaft haben diese sechs Kunden?
- Sind sie im italienischen B1/Rhino-Report bewusst ausgeschlossen?
- Ist der echte Filter eine Kundengruppe, Branche, Sales-Channel, Projekt-/OEM-Abgrenzung oder ein anderes B1-Feld?
- Soll der Filter in der App spaeter als pflegbare Finance-Regel statt als harter Code umgesetzt werden?
Naechster Test:
```text
http://127.0.0.1:5099/run/export/TRIT
```
Danach:
```text
http://127.0.0.1:5099/finance
```
Erwartung mit dem provisorischen Filter:
- IT-Ist nahe `7.669.641,47 EUR`
- Restdifferenz gegen Rhino ca. `-198,53 EUR`
## Fachliche Grundregeln
Diese Regeln gelten bereits als entschieden:
| Thema | Regel |
| --- | --- |
| Waehrung | Hauswaehrung, fuer Italien `EUR` |
| Wertbasis | Nettofakturawert |
| Jahresabgrenzung | Buchungsdatum |
| Aggregation | pro Artikel / Belegposition |
| Gutschriften | separat ausweisen, mit eigener Beleg-/Positionslogik |
| Intercompany | separat ausweisen, nicht still entfernen |
Technischer Datums-Fallback:
```text
PostingDate -> InvoiceDate -> ExtractionDate
```
Wenn Italien kein echtes Buchungsdatum liefert, muss geklaert werden, ob der Fallback auf Fakturadatum fachlich akzeptiert ist.
## Varianten aus der FinanceProbe pruefen
In der Finance-Webseite pro Land den Aufklapper `Varianten anzeigen` oeffnen.
Fuer Italien sind besonders diese Varianten relevant:
| Variante | Bedeutung | Prueffrage |
| --- | --- | --- |
| `Positions-Netto (Sales Price/Value)` | Positionsnaher Nettoverkaufswert aus Quelle/Mapping | Ist das der fachlich richtige Netto-Umsatz je Position? |
| `DocTotalFC - VatSumFC` | Netto-Belegwert in Belegwaehrung | Nur Kontrollsicht; fuer IT sollte EUR/Hauswaehrung fuehrend sein. |
| `Nettofakturawert Hauswaehrung pro Position` | Hauswaehrungs-Netto positionsweise summiert | Fuehrt das zu Doppelzaehlung, weil Belegkopfwerte pro Position wiederholt sind? |
| `Nettofakturawert Hauswaehrung pro Beleg dedupliziert` | Hauswaehrungs-Netto je Beleg nur einmal | Passt diese Sicht besser zu Rhino / check.xlsx? |
| `ohne 2nd-party / IC` | Betrag nach erkanntem IC-Abzug | Sind die IC-Regeln vollstaendig? |
## Priorisierte To-do-Liste
### 1. Berechnungsmethode klaeren
Pruefen, welche Variante fuer Italien fachlich fuehrend sein muss:
- Positionswert aus `SalesPriceValue`
- Hauswaehrungs-Netto pro Position
- Hauswaehrungs-Netto pro Beleg dedupliziert
- andere lokale Netto-Spalte
Entscheid dokumentieren:
```text
IT fuehrende Methode = ...
Begruendung = ...
Freigegeben durch = ...
Datum = ...
```
### 2. Belegkopf-Deduplizierung pruefen
Risiko:
- `DocTotal` / `VatSum` sind Belegkopfwerte.
- In positionsbasierten Exporten koennen diese Werte auf jeder Position wiederholt sein.
- Wenn sie positionsweise summiert werden, entsteht eine Ueberzaehlung.
Zu pruefen:
- Gibt es mehrere Positionen pro Beleg?
- Sind `DocTotal - VatSum` Werte auf allen Positionen eines Belegs identisch?
- Entspricht Rhino eher der deduplizierten Belegsumme oder der Positionssumme?
### 3. Intercompany / 2nd-party vervollstaendigen
Aktuell verwendete Marker:
- `TRAFAG`
- `MAGNETIC SENSE`
- `MAGNETS SENSE`
- `GESELLSCHAFT FUER SENSORIK`
- `GESELLSCHAFT FUR SENSORIK`
Zu klaeren:
- Gibt es italienische Schreibweisen?
- Gibt es lokale Kundennummern fuer Trafag-Gesellschaften?
- Gibt es weitere 2nd-party-Kunden, die nicht ueber Namen erkannt werden?
- Soll IC fuer den offiziellen Wert ausgeschlossen oder nur separat gezeigt werden?
### 4. Gutschriften und Storno pruefen
Zu klaeren:
- Sind Credit Notes vollstaendig enthalten?
- Haben Gutschriften negative Werte?
- Werden Stornos doppelt oder falsch mit Vorzeichen gelesen?
- Haben Gutschriften eigene Rechnungsnummern / Positionen?
### 5. Jahresabgrenzung pruefen
Fuehrende Regel:
```text
Jahr 2025 nach Buchungsdatum
```
Zu klaeren:
- Liefert IT ein echtes Buchungsdatum?
- Wenn nein: ist Fakturadatum als Ersatz fachlich akzeptiert?
- Gibt es Belege aus 2024/2026, die buchhalterisch in 2025 gehoeren oder umgekehrt?
### 6. Rhino / check.xlsx Vergleichsbasis klaeren
Mit Finance / Rhino klaeren:
- Welche Quelle nutzt Rhino fuer Italien?
- Welche Filter sind dort aktiv?
- Ist Rhino inklusive oder exklusive IC?
- Wird nach Beleg, Position oder Kundenklassifikation aggregiert?
- Werden Gutschriften separat oder netto eingerechnet?
## Konkreter Arbeitsablauf
1. FinanceProbe oeffnen:
```text
http://127.0.0.1:5099/finance
```
2. Italien-Zeile suchen.
3. `Varianten anzeigen` oeffnen.
4. Werte notieren fuer:
- gewaehlte Variante
- `Positions-Netto (Sales Price/Value)`
- `Nettofakturawert Hauswaehrung pro Position`
- `Nettofakturawert Hauswaehrung pro Beleg dedupliziert`
- `2nd-party/IC`
- `Diff. ohne 2nd-party`
5. Die Variante identifizieren, die Rhino am naechsten kommt.
6. Nicht automatisch uebernehmen, sondern fachlich begruenden:
```text
Warum passt diese Variante?
Welche Datenfelder nutzt sie?
Welche Faelle schliesst sie ein oder aus?
Ist IC enthalten oder separat?
```
7. Ergebnis mit Finance / Italien bestaetigen.
8. Danach erst Code-/Konfigurationslogik finalisieren.
## Fragen an Italien / Finance
1. Welches Feld ist fuer Net Sales 2025 in Italien fachlich fuehrend?
2. Ist Rhino / check.xlsx fuer Italien inklusive oder exklusive Intercompany?
3. Welche Kunden gelten in Italien als Intercompany / 2nd-party?
4. Werden Credit Notes im lokalen System mit negativem Vorzeichen geliefert?
5. Wird fuer 2025 nach Buchungsdatum oder Fakturadatum abgegrenzt?
6. Sind Belegkopfwerte wie `DocTotal - VatSum` in der Exportdatei pro Position wiederholt?
7. Gibt es lokale Rabatte, Fracht, Zuschlaege oder Nebenpositionen, die in Rhino anders behandelt werden?
## Abschlusskriterium
Italien kann erst auf `OK` oder `kontrolliert geklaert` gesetzt werden, wenn:
- die fuehrende Berechnungsmethode benannt ist,
- die IC-/2nd-party-Regeln vollstaendig genug sind,
- Gutschriften/Storno plausibel sind,
- die Jahresabgrenzung nach Buchungsdatum bestaetigt oder ein Fallback freigegeben ist,
- die Restabweichung gegen Rhino erklaert oder akzeptiert ist.
@@ -14,6 +14,8 @@ Summary:
The mapping has already been reviewed technically, but we still need to clarify the remaining difference before closing the 2025 value.
Important clarification: UK / England is treated as a Sage source. The current SharePoint folder name `UK_B1` is only a technical folder/source reference and does not mean that UK is read from SAP Business One.
Could you please check the following points?
1. Full-year completeness
@@ -0,0 +1,66 @@
# UK / England Quelle - Korrektur
Stand: 2026-05-18
## Wichtige Korrektur
England / UK ist fachlich **Sage**, nicht SAP B1.
Der bisher verwendete Name `UK_B1` bezeichnet im Projektkontext den SharePoint-Ordner bzw. die bisherige technische Quellreferenz. Er darf nicht so verstanden werden, dass England ueber SAP Business One / B1-HANA gelesen wird.
## Korrekte Einordnung
| Punkt | Korrektur |
| --- | --- |
| Land | UK / England |
| TSC | `TRUK` |
| Fachliches Quellsystem | Sage |
| App-Anschluss | `MANUAL_EXCEL` / SharePoint-Datei oder SharePoint-Ordner |
| SharePoint-Ordnername | aktuell `Import/Finance/UK_B1` |
| Nicht korrekt | England als SAP B1 / HANA-B1 interpretieren |
## Konsequenz fuer den Finance-Abgleich
UK darf nicht mit den B1-Regeln von FR / IT verglichen werden.
Insbesondere:
- keine Annahme, dass `DocTotal`, `VatSum`, `OINV`, `INV1`, `ORIN`, `RIN1` fuer UK gelten;
- keine B1-Belegkopf-Deduplizierung als fachliche Standarderklaerung fuer UK;
- UK ist wie Spanien/Deutschland eher als manuelle Sage-/Excel-/CSV-Quelle zu behandeln;
- die Mapping-Regel wurde auf `SageNetSales([Sales Price/Value], [Quantity], [Document Type], [DocumentType], [Type])` umgestellt. Sie rechnet weiterhin Stueckpreis mal Menge, erzwingt Credit Notes aber negativ, sobald der Sage-Export einen Credit-/Abono-/Gutschrift-Typ liefert.
## Korrekte UK-Prueffragen
1. Ist der Sage-Export fuer das ganze Jahr 2025 vollstaendig?
2. Ist `Sales Price/Value` ein Stueckpreis oder bereits ein Positionswert?
3. Sind Credit Notes / Gutschriften enthalten und korrekt negativ?
4. Gibt es Discounts, Freight, Charges oder sonstige Sage-Felder, die Rhino einbezieht?
5. Gibt es 2nd-party-/Intercompany-Kunden, die ausgeschlossen oder separat gezeigt werden sollen?
6. Ist `GBP` die korrekte Vergleichswaehrung?
## Nachtrag 2026-05-18: Sage-Netto-Logik
Die Sage-Logik wurde gegen die Sage-Dokumentation geschaerft:
- Fuehrend ist Netto ohne VAT/MwSt.
- Invoices und Credit Notes werden gemeinsam summiert.
- Credit Notes muessen negativ in die Summe laufen.
- UK bleibt bei `invoice date`, solange kein separates Sage-Buchungsdatum im Export vorhanden ist.
Aktueller Re-Export nach der Anpassung:
| Kennzahl | Wert |
| --- | ---: |
| Zeilen 2025 | `1'880` |
| Ist | `3'533'710.09 GBP` |
| Soll | `3'749'865.00 GBP` |
| Differenz | `-216'154.91 GBP` |
Die Zahl blieb unveraendert, weil die vorhandenen UK-Zeilen bereits negative Betragszeilen enthalten. Die neue Regel verhindert aber, dass kuenftige Sage-Credit-Notes mit positivem Betrag versehentlich als Umsatz addiert werden.
## Formulierung fuer CFO / Finance
```text
UK / England wird fachlich als Sage-Quelle behandelt. Der vorhandene Ordnername UK_B1 ist nur eine technische SharePoint-Bezeichnung und bedeutet nicht, dass UK aus SAP B1 gelesen wird. Die UK-Abweichung ist deshalb ueber Sage-Exportvollstaendigkeit, Mapping, Gutschriften, Discounts/Freight/Charges und 2nd-party-Abgrenzung zu klaeren, nicht ueber B1-Belegkopf-Deduplizierung.
```
@@ -132,12 +132,18 @@ SELECT
CAST(l.PrecioCoste AS decimal(19, 6)) AS StandardCost,
CAST(l.ImporteCoste AS decimal(19, 6)) AS StandardCostValue,
'EUR' AS StandardCostCurrency,
CAST(l.ImporteNeto AS decimal(19, 6)) AS SalesPriceValue,
CAST(CASE
WHEN c.TipoNuevaFra = 2 OR c.SerieFactura = 'REC' OR c.StatusAbono <> 0 THEN -ABS(l.ImporteNeto)
ELSE l.ImporteNeto
END AS decimal(19, 6)) AS SalesPriceValue,
'EUR' AS SalesCurrency,
'EUR' AS DocumentCurrency,
'EUR' AS CompanyCurrency,
c.CodigoDivisa AS SageCurrencyCode,
CAST(c.BaseImponible AS decimal(19, 6)) AS DocumentNetAmount,
CAST(CASE
WHEN c.TipoNuevaFra = 2 OR c.SerieFactura = 'REC' OR c.StatusAbono <> 0 THEN -ABS(c.BaseImponible)
ELSE c.BaseImponible
END AS decimal(19, 6)) AS DocumentNetAmount,
CAST(c.TotalIva AS decimal(19, 6)) AS DocumentVatAmount,
CAST(c.ImporteFactura AS decimal(19, 6)) AS DocumentGrossAmount,
c.FechaFactura AS InvoiceDate,
@@ -203,8 +209,8 @@ CabeceraAlbaranCliente.FechaFactura < ToDate
Notes:
- Currency is set to EUR because Sage exports EnEuros_=-1 and CodigoDivisa is empty in the analysed rows.
- SalesPriceValue uses LineasAlbaranCliente.ImporteNeto.
- DocumentNetAmount uses CabeceraAlbaranCliente.BaseImponible.
- SalesPriceValue uses LineasAlbaranCliente.ImporteNeto; credit notes are forced negative.
- DocumentNetAmount uses CabeceraAlbaranCliente.BaseImponible; credit notes are forced negative.
- Credit notes are marked when TipoNuevaFra=2, SerieFactura='REC', or StatusAbono is non-zero.
"@ | Set-Content -LiteralPath $summaryPath -Encoding UTF8