fix(invoices): Stornorechnung save failed on negative totals
Customer log: /do/inv/dsave failed with "Error converting data type float to
numeric" followed by "Cannot insert the value NULL into column 'InvId'".
- Bump OCORE: SQL_Float/SQL_Decimal string overloads parsed negatives via the
de-DE server culture ("-19.95" -> -1995); float noise overflowed
numeric(10,3), so fds__setInvoice never ran.
- InvoiceService: the header save batch now stops with a clear THROW when
fds__createInvoice/fds__setInvoice yields no @Id, instead of failing again
in fds__createInvoice_Details and burying the real error. Batch SQL
extracted to BuildRegisterInvoiceSql for testing.
- Tests: signed/culture parsing under de-DE, Storno param build against the
proc numeric limits, and the batch guard ordering.
- hlp: read-only script to find invoice/reminder amounts stored scaled up
by the bug.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c741535aec
commit
0a8384ab33
@@ -151,6 +151,27 @@ public class InvoiceService : IInvoiceService
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The header save batch for <see cref="RegisterInvoiceAsync"/>. If the create/set proc fails
|
||||
/// (e.g. a parameter conversion error) the batch would otherwise carry on with a NULL
|
||||
/// <c>@Id</c> and fail again in <c>fds__createInvoice_Details</c> ("Cannot insert NULL into
|
||||
/// InvId"), burying the real error; the guard stops the batch right after the first failure.
|
||||
/// </summary>
|
||||
internal static string BuildRegisterInvoiceSql(bool change, bool hasProvisionLocation)
|
||||
{
|
||||
var sqlParts = new List<string> { "DECLARE @Id varchar(10);" };
|
||||
sqlParts.Add(change
|
||||
? "EXECUTE [dbo].[fds__setInvoice] @InvId, @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @SendToEmailCc, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT, @SendToAddressJson;"
|
||||
: "EXECUTE [dbo].[fds__createInvoice] @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @SendToEmailCc, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT, @SendToAddressJson;");
|
||||
sqlParts.Add(change
|
||||
? "IF @Id IS NULL THROW 50001, N'fds__setInvoice did not return an invoice id - invoice header not saved.', 1;"
|
||||
: "IF @Id IS NULL THROW 50001, N'fds__createInvoice did not return an invoice id - invoice header not saved.', 1;");
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
|
||||
if (hasProvisionLocation)
|
||||
sqlParts.Add("UPDATE [dbo].[fds__invoices] SET [ProvisionLocation] = LEFT(@ProvisionLocation,1000) WHERE [Id] = @Id AND [isFinal] = 0;");
|
||||
return string.Join("\n", sqlParts);
|
||||
}
|
||||
|
||||
public async Task<FdsInvoiceData> RegisterInvoiceAsync(FdsInvoiceData invoice, bool change, string invId,
|
||||
string userAccountId, DatabaseSecurity dbSec)
|
||||
{
|
||||
@@ -160,26 +181,13 @@ public class InvoiceService : IInvoiceService
|
||||
var pl = new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId) };
|
||||
pl.AddRange(invoice.BuildInvoiceParams(change, invId));
|
||||
|
||||
var sqlParts = new List<string> { "DECLARE @Id varchar(10);" };
|
||||
if (!change)
|
||||
{
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice] @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @SendToEmailCc, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT, @SendToAddressJson;");
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
|
||||
}
|
||||
else
|
||||
{
|
||||
pl.Add(SQL_VarChar("@InvId", invId));
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__setInvoice] @InvId, @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @SendToEmailCc, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT, @SendToAddressJson;");
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
|
||||
}
|
||||
if (invoice.RawProvisionLocation.Length > 0)
|
||||
{
|
||||
if (change) pl.Add(SQL_VarChar("@InvId", invId));
|
||||
bool hasProvisionLocation = invoice.RawProvisionLocation.Length > 0;
|
||||
if (hasProvisionLocation)
|
||||
pl.Add(SQL_NVarChar("@ProvisionLocation",
|
||||
string.Join("\n", invoice.RawProvisionLocation).LeftToFirst("<!--", emptyIfNotFound: false)));
|
||||
sqlParts.Add("UPDATE [dbo].[fds__invoices] SET [ProvisionLocation] = LEFT(@ProvisionLocation,1000) WHERE [Id] = @Id AND [isFinal] = 0;");
|
||||
}
|
||||
|
||||
var invdset = await getSQLDataSet_async(string.Join("\n", sqlParts),
|
||||
var invdset = await getSQLDataSet_async(BuildRegisterInvoiceSql(change, hasProvisionLocation),
|
||||
Conn, pl, tablenames: new[] { "inv", "det" },
|
||||
Security: dbSec, options: new FIS_SQLOptions());
|
||||
if (!string.IsNullOrEmpty(invdset.Exception))
|
||||
|
||||
Reference in New Issue
Block a user