X-Rechnung 3.1 UBL for Deutsche Bahn
Closed this issue · 7 comments
First a big THANK you for your work! Amazing...
I was playing around to send some invoices to the DB, validating with https://xre.deutschebahn.com... I succeeded doing some changes with you possibily have alread in mind, so I list them here.
Interesting: the Kosit validator gives some errors; if I fix them, the DB validator does not run OK:
The DB validator gives only a warning about payment means, but this seems to be a bug of the validator, the invoices are accepted.
Dont kill me not sending a pull request; I'm not sure if my issue is of public interest, so before boring you, I'd like you to have a short overview to the following changes I had to do. Possibly they are completely against your best practice... or already on your to-do list... or I'm inventing the UBL format for you :-)
Just let me know, if I can contribute, I'd be more than happy!
I send some lines with comments an I attach a current diff.
Hopefully this helps in any way... Take care,
Roger
InvoiceDescriptor22Writer.cs:
263
// BT-148
if (tradeLineItem.GrossUnitPrice.HasValue || (tradeLineItem.GetTradeAllowanceCharges().Count > 0)
&& !tradeLineItem.NetUnitPrice.HasValue // roger
268
//_writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.GrossUnitPrice, 4);
_writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.GrossUnitPrice, 2); //roger
285
#region ChargePercentage
/*
//roger not allowed
if (tradeAllowanceCharge.ChargePercentage.HasValue)
{
Writer.WriteStartElement("ram:CalculationPercent", profile: Profile.Extended | Profile.XRechnung);
Writer.WriteValue(_formatDecimal(tradeAllowanceCharge.ChargePercentage.Value, 2));
Writer.WriteEndElement();
}
*/
#endregion
340
//_writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.NetUnitPrice, 4);
_writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.NetUnitPrice, 2);//roger
(I had to group by VAT, otherwise I had one VAT line per allowance on the invoice...)
404
if (tradeLineItem.TaxCategoryCode != TaxCategoryCodes.O) // notwendig, damit die Validierung klappt
{
Writer.WriteElementString("ram:RateApplicablePercent", _formatDecimal(tradeLineItem.TaxPercent));
}
//roger
if (tradeLineItem.LineTotalAmount.HasValue)
// -->I had to add LineTotalAmount to desc.addtradeLineItem in InvoiceDescriptor.cs
{
Writer.WriteStartElement("ram:BasisAmount", profile: Profile.Extended); // not in XRechnung, according to CII-SR-123
Writer.WriteValue(_formatDecimal(tradeLineItem.LineTotalAmount.Value, 2));
Writer.WriteEndElement();
} // !roger
later...
//_writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.NetUnitPrice, 4);
_writeOptionalAmount(Writer, "ram:ChargeAmount", tradeLineItem.NetUnitPrice, 2);//roger
and
if (tradeLineItem.TaxCategoryCode != TaxCategoryCodes.O) // notwendig, damit die Validierung klappt
{
Writer.WriteElementString("ram:RateApplicablePercent", _formatDecimal(tradeLineItem.TaxPercent));
}
//roger
if (tradeLineItem.LineTotalAmount.HasValue)
{
Writer.WriteStartElement("ram:BasisAmount", profile: Profile.Extended); // not in XRechnung, according to CII-SR-123
Writer.WriteValue(_formatDecimal(tradeLineItem.LineTotalAmount.Value, 2));
Writer.WriteEndElement();
}
-----------------------------------------
private void _writeOptionalTaxesNew(ProfileAwareXmlTextWriter writer)
{
decimal lineTotal = 0m;
List<Tax> taxes = new List<Tax>();
foreach (Tax tax in this.Descriptor.Taxes)
{
Tax t = taxes.Find(x => x.CategoryCode == tax.CategoryCode);
if (t == null) // neu
{
t = new Tax();
t.ExemptionReason = tax.ExemptionReason;
t.Percent = tax.Percent;
t.AllowanceChargeBasisAmount = tax.AllowanceChargeBasisAmount;
t.BasisAmount = tax.BasisAmount; // as is
t.ExemptionReasonCode = tax.ExemptionReasonCode;
t.AllowanceChargeBasisAmount = tax.AllowanceChargeBasisAmount;
t.TypeCode = tax.TypeCode;
taxes.Add(t);
}
else // vorhanden
{
t.AllowanceChargeBasisAmount += tax.AllowanceChargeBasisAmount;
t.BasisAmount += tax.BasisAmount; // addieren
t.AllowanceChargeBasisAmount += tax.AllowanceChargeBasisAmount;
}
}
foreach (Tax tax in taxes)
{
writer.WriteStartElement("ram:ApplicableTradeTax");
writer.WriteStartElement("ram:CalculatedAmount");
writer.WriteValue(_formatDecimal(tax.TaxAmount));
writer.WriteEndElement(); // !CalculatedAmount
writer.WriteElementString("ram:TypeCode", tax.TypeCode.EnumToString());
writer.WriteOptionalElementString("ram:ExemptionReason", tax.ExemptionReason);
writer.WriteStartElement("ram:BasisAmount");
writer.WriteValue(_formatDecimal(tax.BasisAmount));
writer.WriteEndElement(); // !BasisAmount
if (tax.CategoryCode.HasValue)
{
writer.WriteElementString("ram:CategoryCode", tax.CategoryCode?.EnumToString());
}
if (tax.ExemptionReasonCode.HasValue)
{
writer.WriteElementString("ram:ExemptionReasonCode", tax.ExemptionReasonCode?.EnumToString());
}
writer.WriteElementString("ram:RateApplicablePercent", _formatDecimal(tax.Percent));
writer.WriteEndElement(); // !RateApplicablePercent
}
} // !_writeOptionalTaxesNew()
.. I forgot:
in Tax.cs:
return System.Math.Round(0.01m * this.Percent * this.BasisAmount, 2, MidpointRounding.AwayFromZero);
Thanks for sharing.
I will take a deeper look in the coming days. Happy to accept e.g. the Tax correction - did you find the rounding rule in the documentation?
Concerning the validators: I made the same observations. Generally, one would say that the KOSIT validator is key but if your customer doesn't accept your invoice because of his own (different) validator, you are busted.
Hi! Thanks, I was afraid of doing something wrong. However, I saw that my amends shall be corrected, however; as soon I will get correct results, I'll try to apply your mechanisms to write data depending on the profile and doing calcs in the subclasses...
-
The tax correction is only a first step to lead to a valid result, I think that the figures need to be in the round,2 format to correspond to any accounting rules (invoices do not user floats, ususally).
Basically I will try to implement that wherever I put data, and - more important - when calculating VAT from % and amount "("kaufmännisches Runden"). As a matter of fact, this leads to a rounding error (simple math...), which has to be put in BT-114...
I think that implementing a correct rounding could be helpful; in my test environment, I have data coming from an old ERP with many decimal. Currently, I'm rounding the figures before passing them to your prog; this could be avoided if the prog did the rounding... -
Currently, I'm actually desperating trying to write some validator lines to test easier. I paste that hereafter, look at it only if your're interested - it doesnt' work (runs, but does not show errors), doesnt call the validatoreventhandler.
-
Regarding DB: they calcolate some figures in the validator, especially BG-23, even if you send them wrong data, so this is a pitfall.
See you...
{
lastvalidationmessage = "";
try
{
IList<string> messages = new List<string>();
string assemblyFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
string schemaroot = assemblyFolder + "/xsd/" + xrechnungAngaben.xmlInvoiceSchemaroot;
Console.WriteLine("Validating: " + target);
XmlReaderSettings validationSettings = new XmlReaderSettings();
validationSettings.Schemas.Add("urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100", @schemaroot + @"FACTUR-X_EN16931.xsd");
validationSettings.Schemas.Add("urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100", @schemaroot + @"FACTUR-X_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd");
validationSettings.Schemas.Add("urn:un:unece:uncefact:data:standard:QualifiedDataType:100", @schemaroot + @"FACTUR-X_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd");
validationSettings.Schemas.Add("urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100", @schemaroot + @"FACTUR-X_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd");
validationSettings.ValidationType = ValidationType.Schema;
XmlReader reader = XmlReader.Create(target, validationSettings);
XmlDocument document = new XmlDocument();
document.Load(reader);
XPathNavigator navigator = document.CreateNavigator();
ValidationEventHandler validation = new ValidationEventHandler(ValidationEventHandler);
validationSettings.ValidationEventHandler += new ValidationEventHandler(ValidationEventHandler);
validationSettings.ValidationFlags |= XmlSchemaValidationFlags.ProcessInlineSchema;
validationSettings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings;
validationSettings.ValidationFlags |= XmlSchemaValidationFlags.AllowXmlAttributes;
validationSettings.ValidationFlags |= XmlSchemaValidationFlags.ProcessInlineSchema;
document.Validate(validation);
Console.WriteLine();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return 1;
}
return 0;
}
static void ValidationEventHandler(object sender, ValidationEventArgs e)
{
if (e.Severity == XmlSeverityType.Warning)
{
Tools.WriteLog(String.Format("E=>{0}", e.Message), "E");
validateRC = 1;
}
else if (e.Severity == XmlSeverityType.Error)
{
Tools.WriteLog(String.Format("E=>{0}", e.Message), "E");
lastvalidationmessage = e.Message;
validateRC = 2;
}
}
Closing this ticket in favor to more specific tickets.
I'd be more than happy to add the validator code as soon you have it ready!
@goedo : I added some comments to the new "broken down" tickets. Could you please answer there?