From fb85e2e57a2efa959e167f678933a64b261905ee Mon Sep 17 00:00:00 2001 From: metacube Date: Mon, 18 May 2026 20:57:22 +0200 Subject: [PATCH] Correct Sage finance calculations --- .../Export-SageSpainSalesCsv.ps1 | 14 +- .../Services/DatabaseSeedService.cs | 4 +- .../Services/HanaQueryService.cs | 29 +- .../Services/ManualExcelImportService.cs | 82 ++++ .../ManualExcelImportServiceTests.cs | 54 +++ ...INANCE_AMPEL_LAENDER_2026-05-18_20-55.xlsx | Bin 0 -> 10952 bytes .../docs/FINANCE_ENTSCHEIDE.md | 16 +- .../docs/FINANCE_IT_VORGEHEN_2026-05-18.md | 397 ++++++++++++++++++ .../FINANCE_UK_MAIL_ABWEICHUNG_2026-05-15.md | 2 + .../FINANCE_UK_QUELLE_KORREKTUR_2026-05-18.md | 66 +++ .../scripts/Export-SageSpainSalesCsv.ps1 | 14 +- 11 files changed, 659 insertions(+), 19 deletions(-) create mode 100644 TrafagSalesExporter/docs/FINANCE_AMPEL_LAENDER_2026-05-18_20-55.xlsx create mode 100644 TrafagSalesExporter/docs/FINANCE_IT_VORGEHEN_2026-05-18.md create mode 100644 TrafagSalesExporter/docs/FINANCE_UK_QUELLE_KORREKTUR_2026-05-18.md diff --git a/TrafagSalesExporter/SageSpainFinalExportPackage/Export-SageSpainSalesCsv.ps1 b/TrafagSalesExporter/SageSpainFinalExportPackage/Export-SageSpainSalesCsv.ps1 index 0caebec..2204d88 100644 --- a/TrafagSalesExporter/SageSpainFinalExportPackage/Export-SageSpainSalesCsv.ps1 +++ b/TrafagSalesExporter/SageSpainFinalExportPackage/Export-SageSpainSalesCsv.ps1 @@ -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 diff --git a/TrafagSalesExporter/Services/DatabaseSeedService.cs b/TrafagSalesExporter/Services/DatabaseSeedService.cs index e8cb8a8..8314059 100644 --- a/TrafagSalesExporter/Services/DatabaseSeedService.cs +++ b/TrafagSalesExporter/Services/DatabaseSeedService.cs @@ -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; diff --git a/TrafagSalesExporter/Services/HanaQueryService.cs b/TrafagSalesExporter/Services/HanaQueryService.cs index 8a45cd6..2fb93c6 100644 --- a/TrafagSalesExporter/Services/HanaQueryService.cs +++ b/TrafagSalesExporter/Services/HanaQueryService.cs @@ -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)) diff --git a/TrafagSalesExporter/Services/ManualExcelImportService.cs b/TrafagSalesExporter/Services/ManualExcelImportService.cs index e9823d7..966fa0b 100644 --- a/TrafagSalesExporter/Services/ManualExcelImportService.cs +++ b/TrafagSalesExporter/Services/ManualExcelImportService.cs @@ -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 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 SplitFunctionArguments(string arguments) + { + var result = new List(); + 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 readHeader) + => ParseDecimal(ResolveSageArgumentText(operand, readHeader)); + + private static string ResolveSageArgumentText(string operand, Func 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())); diff --git a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManualExcelImportServiceTests.cs b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManualExcelImportServiceTests.cs index a621019..f8c5db6 100644 --- a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManualExcelImportServiceTests.cs +++ b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManualExcelImportServiceTests.cs @@ -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 + { + 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 fillWorkbook) { var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.xlsx"); diff --git a/TrafagSalesExporter/docs/FINANCE_AMPEL_LAENDER_2026-05-18_20-55.xlsx b/TrafagSalesExporter/docs/FINANCE_AMPEL_LAENDER_2026-05-18_20-55.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..beb8eb6fb7d59bf2da6b1e72ec4df6f8f6179e62 GIT binary patch literal 10952 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 literal 0 HcmV?d00001 diff --git a/TrafagSalesExporter/docs/FINANCE_ENTSCHEIDE.md b/TrafagSalesExporter/docs/FINANCE_ENTSCHEIDE.md index a2aeabe..874cfd9 100644 --- a/TrafagSalesExporter/docs/FINANCE_ENTSCHEIDE.md +++ b/TrafagSalesExporter/docs/FINANCE_ENTSCHEIDE.md @@ -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`: diff --git a/TrafagSalesExporter/docs/FINANCE_IT_VORGEHEN_2026-05-18.md b/TrafagSalesExporter/docs/FINANCE_IT_VORGEHEN_2026-05-18.md new file mode 100644 index 0000000..a11db5c --- /dev/null +++ b/TrafagSalesExporter/docs/FINANCE_IT_VORGEHEN_2026-05-18.md @@ -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. diff --git a/TrafagSalesExporter/docs/FINANCE_UK_MAIL_ABWEICHUNG_2026-05-15.md b/TrafagSalesExporter/docs/FINANCE_UK_MAIL_ABWEICHUNG_2026-05-15.md index 9e1dff2..a4758be 100644 --- a/TrafagSalesExporter/docs/FINANCE_UK_MAIL_ABWEICHUNG_2026-05-15.md +++ b/TrafagSalesExporter/docs/FINANCE_UK_MAIL_ABWEICHUNG_2026-05-15.md @@ -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 diff --git a/TrafagSalesExporter/docs/FINANCE_UK_QUELLE_KORREKTUR_2026-05-18.md b/TrafagSalesExporter/docs/FINANCE_UK_QUELLE_KORREKTUR_2026-05-18.md new file mode 100644 index 0000000..420c045 --- /dev/null +++ b/TrafagSalesExporter/docs/FINANCE_UK_QUELLE_KORREKTUR_2026-05-18.md @@ -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. +``` diff --git a/TrafagSalesExporter/scripts/Export-SageSpainSalesCsv.ps1 b/TrafagSalesExporter/scripts/Export-SageSpainSalesCsv.ps1 index 0caebec..2204d88 100644 --- a/TrafagSalesExporter/scripts/Export-SageSpainSalesCsv.ps1 +++ b/TrafagSalesExporter/scripts/Export-SageSpainSalesCsv.ps1 @@ -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