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 0000000..beb8eb6 Binary files /dev/null and b/TrafagSalesExporter/docs/FINANCE_AMPEL_LAENDER_2026-05-18_20-55.xlsx differ 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