stephanstapel/ZUGFeRD-csharp

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:
image

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()

diff.txt

.. 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;
    }
}

broken down into
#255, #256, #257, #258, #259, #260, #261, #262

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?