Correct Sage finance calculations
This commit is contained in:
@@ -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()));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user